'use strict';

// Use luxon for date and time handling
import * as luxon from "../luxon/luxon.es6.js";

// ---------------------------------------------------------
// Helpers

// :: A -> Option[A]
let optional = (x) => x === null ? undefined : x;

// :: A -> Bool
let defined = (x) => x !== undefined;

// :: Option[A] -> (()->B) -> Option[B]
let withDefined = (x, f) => defined(x) ? f() : undefined;

//-:: Option[A] -> (A->B) -> Option[B]
let ifDefined = (x, f) => defined(x) ? f(x) : undefined;

// Note: use only for primitive values ("primary keys")
// :: List[A] -> List[A]
let distinct = (xs) => Array.from(new Set(xs));

// :: List[A] -> List[A] -> List[A]
let intersection = (xs, ys) => xs.filter(v => ys.includes(v));

//-:: String -> String -> Int
let stringCompare = (s1, s2) => {
    if (s1 < s2) { return -1; };
    if (s1 > s2) { return 1; };
    return 0;
};


// ---------------------------------------------------------
// Transform Json into proper Arrays and Maps

//-:: EventAnswerJson -> EventAnswer
let processEventAnswer = (answerJ) => {
    let m = new Map();
    m.set('question', answerJ.question);
    m.set('answer', answerJ.answer);
    return m;
};

//-:: AttachmentJson -> Attachment
let processAttachment = (attachmentJ) => undefined;

//-:: LinkJson -> Link
let processLink = (linkJ) => undefined;

//-:: PersonAnswerJson -> PersonAnswer
let processPersonAnswer = (answerJ) => undefined;

//-:: PersonJson -> Person
let processPerson = (personJ) => {
    let m = new Map();
    // TODO: Hack as id is nil in current export
    m.set('id', personJ.code);
    m.set('code', personJ.code);
    // Renamed field:
    m.set('name', personJ.public_name);
    m.set('biography', optional(personJ.biography));
    //m.set('answers', personJ.answers.map(aj => processPersonAnswer(aj)));
    return m;
};

//-:: EventJson -> DayIndex -> Event
let processEvent = (eventJ, dayIndex) => {
    let start_date = luxon.DateTime.fromISO(eventJ.date);
    let duration = luxon.Duration.fromISOTime(eventJ.duration);
    let end_date = start_date.plus(duration);

    let m = new Map();
    m.set('id', eventJ.id);
    m.set('guid', eventJ.guid);
    m.set('logo', eventJ.logo);
    // Same as processed start date
    //m.set('date',
    //      luxon.DateTime.fromISO(eventJ.date));
    m.set('start_date', start_date);
    // Synthetic field:
    m.set('end_date', end_date);
    m.set('duration', duration);
    // Synthetic field:
    m.set('day_index', dayIndex);
    m.set('room', eventJ.room);
    m.set('slug', eventJ.slug);
    m.set('url', eventJ.url);
    // m.set('title', eventJ.title || "");
    m.set('title', withDefined(eventJ.title, () => eventJ.title));
    m.set('subtitle', withDefined(eventJ.subtitle, () => eventJ.subtitle));
    m.set('track', eventJ.track);
    m.set('type', eventJ.type);
    m.set('language', eventJ.language);
    // TODO: Rethink empty strings
    m.set('abstract', eventJ.abstract || "");
    m.set('description', eventJ.description || "");
    m.set('recording_license', eventJ.recording_license);
    m.set('do_not_record', eventJ.do_not_record);
    m.set('persons', eventJ.persons.map(pj => processPerson(pj)));
    m.set('links', eventJ.links.map(lj => processLink(lj)));
    m.set('attachments', eventJ.attachments.map(aj => processAttachment(aj)));
    m.set('answers', withDefined(eventJ.answers,
                                 () => eventJ.answers.map(aj => processEventAnswer(aj))));
    return m;
};

//-:: RoomJson -> DayIndex -> Map[RoomName, List[Event]]
let processRooms = (roomJ, dayIndex) => {
    let m = new Map(
        Object.entries(roomJ).map(
            item => [item[0], item[1].map(e => processEvent(e, dayIndex))]
        )
    );
    return m;
};

//-:: DayJson -> Day
let processDay = (dayJ) => {
    let main_date = luxon.DateTime.fromISO(dayJ.date);
    let start_date = luxon.DateTime.fromISO(dayJ.day_start);
    let end_date = luxon.DateTime.fromISO(dayJ.day_end);
    let duration = end_date.diff(start_date);

    let m = new Map();
    m.set('index', dayJ.index);
    m.set('date', main_date);
    m.set('start_date', start_date);
    m.set('end_date', end_date);
    // Synthetic field:
    m.set('duration', duration);
    // Note: Pass day index down to rooms and finally events
    m.set('rooms', processRooms(dayJ.rooms, dayJ.index));
    return m;
};

//-:: RoomDefinitionJson -> Room
let processRoomDefinition = (roomJ) => {
    let m = new Map();
    m.set('name', roomJ.name);
    m.set('guid', optional(roomJ.guid));
    m.set('description', optional(roomJ.description));
    m.set('capacity', optional(roomJ.capacity));
    return m;
};

//-:: ConferenceJson -> Conference
let processConference = (conferenceJ) => {
    let start_date = luxon.DateTime.fromISO(conferenceJ.start);
    let end_date = luxon.DateTime.fromISO(conferenceJ.end);
    let duration = end_date.diff(start_date);

    let m = new Map();
    m.set('acronym', conferenceJ.acronym);
    m.set('title', conferenceJ.title);
    m.set('start_date', start_date);
    m.set('end_date', end_date);
    // Synthetic field:
    m.set('duration', duration);
    m.set('days_count', conferenceJ.daysCount);
    m.set('timeslot_duration',
          luxon.Duration.fromISOTime(conferenceJ.timeslot_duration));
    // TODO:
    //m.set('time_zone_name',
    //      luxon.IANAZone.create(conferenceJ.time_zone_name));
    m.set('rooms', conferenceJ.rooms.map(rj => processRoomDefinition(rj)));
    m.set('days', conferenceJ.days.map(dj => processDay(dj)));
    return m;
};

//-:: ScheduleJson -> Schedule
let processSchedule = (scheduleJ) => {
    let m = new Map();
    m.set('version', scheduleJ.version);
    m.set('base_url', scheduleJ.base_url);
    m.set('conference', processConference(scheduleJ.conference));
    return m;
};

// :: ScheduleFile -> ScheduleJson
let processScheduleFile = (scheduleFile) =>
    processSchedule(scheduleFile.schedule);
    //processSchedule(JSON.parse(scheduleFile).schedule);


// ---------------------------------------------------------
// Properties and Accessors ('Data Types')

// :: Schedule -> String
let scheduleVersion = (schedule) => schedule.get('version');

// :: Schedule -> String
let scheduleBaseUrl = (schedule) => schedule.get('base_url');


// :: Conference -> String
let conferenceAcronym = (conference) => conference.get('acronym');

// :: Conference -> String
let conferenceTitle = (conference) => conference.get('title');

// :: Conference -> luxon.DateTime
let conferenceStartDate = (conference) => conference.get('start_date');

// :: Conference -> luxon.DateTime
let conferenceEndDate = (conference) => conference.get('end_date');

// :: Conference -> luxon.Duration
let conferenceDuration = (conference) => conference.get('duration');

// :: Conference -> Int
let conferenceDaysCount = (conference) => conference.get('days_count');

// :: Conference -> luxon.Duration
let conferenceTimeslotDuration = (conference) => conference.get('timeslot_duration');

// :: Conference -> luxon.IANATimeZone
let conferenceTimezoneName = (conference) => conference.get('time_zone_name');


// :: Day -> DayIndex
let dayIndex = (day) => day.get('index');

// :: Day -> luxon.DateTime
let dayDate = (day) => day.get('date');

// :: Day -> luxon.DateTime
let dayStartDate = (day) => day.get('start_date');

// :: Day -> luxon.DateTime
let dayEndDate = (day) => day.get('end_date');

// :: Day -> luxon.Duration
let dayDuration = (day) => day.get('duration');


// :: Room -> String
let roomName = (room) => room.get('name');

// :: Room -> String
let roomGuid = (room) => room.get('guid');
let roomId = (room) => room.get('guid');

// :: Room -> String
let roomDescription = (room) => room.get('description');

// :: Room -> Int
let roomCapacity = (room) => room.get('capacity');


// :: Event -> Int
let eventId = (event) => event.get('id');

// :: Event -> String
let eventGuid = (event) => event.get('guid');

// :: Event -> String
let eventLogo = (event) => event.get('logo');

// :: Event -> luxon.DateTime
let eventStartDate = (event) => event.get('start_date');

// :: Event -> luxon.DateTime
let eventEndDate = (event) => event.get('end_date');

// :: Event -> luxon.Duration
let eventDuration = (event) => event.get('duration');

// :: Event -> Int
let eventDayIndex = (event) => event.get('day_index');

// :: Event -> RoomName
let eventRoomName = (event) => event.get('room');

// :: Event -> String
let eventSlug = (event) => event.get('slug');

// :: Event -> String
let eventUrl = (event) => event.get('url');

// :: Event -> String
let eventTitle = (event) => event.get('title');

// :: Event -> String
let eventSubtitle = (event) => event.get('subtitle');

// :: Event -> TrackName
let eventTrack = (event) => event.get('track');

// :: Event -> EventType
let eventType = (event) => event.get('type');

// :: Event -> Language
let eventLanguage = (event) => event.get('language');

// :: Event -> String
let eventAbstract = (event) => event.get('abstract');

// :: Event -> String
let eventDescription = (event) => event.get('description');

// :: Event -> String
let eventRecordingLicense = (event) => event.get('recording_license');

// :: Event -> Bool
let eventDoNotRecord = (event) => event.get('do_not_record');

// :: Event -> List[Person]
let eventPersons = (event) => event.get('persons');

// :: Event -> Int
let eventPersonCount = (event) => eventPersons(event).length;

// :: Event -> List[AnswerMap]
let eventAnswers = (event) => event.get('answers');


// :: Person -> Int
let personId = (person) => person.get('id');

// :: Person -> String
let personCode = (person) => person.get('code');

// :: Person -> String
let personName = (person) => person.get('name');

// :: Person -> String
let personBiography = (person) => person.get('biography');


// ---------------------------------------------------------
// Filters, Maps and other Pieces of Lisp

// TODO: Use own stable IDs

// :: List[Event] -> DayIndex -> List[Event]
let eventsByDayIndex = (events, dIndex) =>
    events.filter(e => eventDayIndex(e) === dIndex);

// :: List[Event] -> Day -> List[Event]
let eventsByDay = (events, day) =>
    events.filter(e => eventDayIndex(e) === dayIndex(day));


// :: List[Event] -> RoomName -> List[Event]
let eventsByRoomName = (events, rName) =>
    events.filter(e => eventRoomName(e) === rName);

// :: List[Event] -> Room -> List[Event]
let eventsByRoom = (events, room) =>
    eventsByRoomName(events, roomName(room));


// :: List[Event] -> PersonId -> List[Event]
let eventsByPersonId = (events, pId) =>
    events.filter(e => eventPersons(e).some(p => personId(p) === pId));

// :: List[Event] -> PersonName -> List[Event]
let eventsByPersonName = (events, pName) =>
    events.filter(e => eventPersons(e).some(p => personName(p) === pName));

// :: List[Event] -> Person -> List[Event]
let eventsByPerson = (events, person) =>
    eventsByPersonName(events, personName(person));

// :: List[Event] -> TrackName -> List[Event]
let eventsByTrack = (events, eventTrackName) =>
    events.filter(e => eventTrack(e) === eventTrackName);

// :: List[Event] -> EventType -> List[Event]
let eventsByType = (events, eventTypeName) =>
    events.filter(e => eventType(e) === eventTypeName);

// :: List[Event] -> Language -> List[Event]
let eventsByLanguage = (events, language) =>
    events.filter(e => eventLanguage(e) === language);


// :: List[Room] -> GUID -> Option[Room]
let roomById = (rooms, id) => rooms.find(r => roomId(r) === id);

// :: List[Room] -> RoomName -> List[Room]
let roomsByName = (rooms, name) => rooms.filter(r => roomName(r) === name);

// :: List[Day] -> Int -> Option[Day]
let dayByIndex = (days, index) => days.find(d => dayIndex(d) === index);

// :: List[Person] -> Int -> Option[Person]
let personById = (persons, id) => persons.find(p => personId(p) === id);

// :: List[Person] -> PersonName -> List[Person]
let personsByName = (persons, name) => persons.filter(p => personName(p) === name);

// :: List[Event] -> Int -> Option[Event]
let eventById = (events, id) => events.find(e => eventId === id);


// :: List[Room] -> List[Room]
let distinctRooms = (rooms) => {
    //let ids = distinct(rooms.map(roomId));
    //return ids.map(i => rooms.find(r => roomId(r) === i));
    // TODO: Room Id might be optional?
    let rns = distinct(rooms.map(roomName));
    return rns.map(i => rooms.find(r => roomName(r) === i));
};

// :: List[Day] -> List[Day]
let distinctDays =  (days) => {
    let ids = distinct(days.map(dayIndex));
    return ids.map(i => days.find(d => dayIndex(d) === i));
};

// :: List[Event] -> List[Event]
let distinctEvents = (events) => {
    let ids = distinct(events.map(eventId));
    return ids.map(i => events.find(e => eventId(e) === i));
};

// :: List[Person] -> List[Person]
let distinctPersons = (persons) => {
    let ids = distinct(persons.map(personId));
    return ids.map(i => persons.find(p => personId(p) === i));
};

// :: List[Person] -> List[Person]
let sortPersonsByName = (persons) =>
    persons.sort((p1, p2) => stringCompare(personName(p1), personName(p2)));

// :: List[Room] -> List[Room]
let sortRoomsByName = (rooms) =>
    rooms.sort((r1, r2) => stringCompare(roomName(r1), roomName(r2)));

// :: List[Event] -> List[Event]
let sortEventsByTitle = (events) =>
    events.sort((e1, e2) => stringCompare(eventTitle(e1), eventTitle(e2)));


// ---------------------------------------------------------
// Advanced Accessors

// Note: Maybe deprecate and replace with 'eventsByDay'?
// :: Day -> List[Event]
let eventsOfDay = (day) =>
    Array.from(day.get('rooms').values()).flat();

// :: List[Day] -> List[Event]
let eventsOfDays = (days) => days.flatMap(eventsOfDay);


// Note: Can not implement this, needs more information.
//       Use 'eventsByRoom' and 'eventsByRooms' instead.
//-:: Room -> List[Event]
//let eventsOfRoom = (room) =>
//    Array.from(room.values());

//-:: List[Room] -> List[Event]
//let eventsOfRooms = (rooms) => rooms.flatMap(eventsOfRoom);


// :: List[Event] -> List[TrackName]
let tracksOfEvents = (events) =>
    distinct(events.flatMap(eventLanguage));

// :: List[Event] -> List[EventType]
let typesOfEvents = (events) =>
    distinct(events.flatMap(eventType));

// :: List[Event] -> List[Language]
let languagesOfEvents = (events) =>
    distinct(events.flatMap(eventLanguage));


// :: Conference -> List[Day]
let conferenceDays = (conference) =>
    distinctDays(conference.get('days'));

// :: Conference -> List[Room]
let conferenceRooms = (conference) =>
    distinctRooms(Array.from(conference.get('rooms').values()));

// :: Conference -> List[Event]
let conferenceEvents = (conference) =>
    distinctEvents(conferenceDays(conference).flatMap(d => eventsOfDay(d)));

// :: Conference -> List[Person]
let conferencePersons = (conference) =>
    distinctPersons(conferenceEvents(conference).flatMap(eventPersons));

// :: Conference -> List[TrackName]
let conferenceTracks = (conference) =>
    distinct(conferenceEvents(conference).flatMap(eventTrack));

// :: Conference -> List[EventType]
let conferenceTypes = (conference) =>
    distinct(conferenceEvents(conference).flatMap(eventType));

// :: Conference -> List[Language]
let conferenceLanguages = (conference) =>
    distinct(conferenceEvents(conference).flatMap(eventLanguage));


// :: Schedule -> Conference
let conference = (schedule) => schedule.get('conference');


// ---------------------------------------------------------
// Advanced Filters, Maps and Traces of SQL

// TODO: Use own stable IDs

// :: Conference -> List[RoomName] -> DayIndex -> List[RoomName]
let roomNamesByDayIndex = (conference, roomNames, dayIndex) => {
    let evs = eventsByDayIndex(conferenceEvents(conference), dayIndex);
    let rns = distinct(evs.map(eventRoomName));
    return distinct(roomNames.filter(rn => rns.includes(rn)));
};

// :: Conference -> List[RoomName] -> Day -> List[RoomName]
let roomNamesByDay = (conference, roomNames, day) =>
    roomNamesByDayIndex(conference, roomNames, dayIndex(day));

// :: Conference -> List[Room] -> DayIndex -> List[Room]
let roomsByDayIndex = (conference, rooms, dayIndex) => {
    let rns = roomNamesByDayIndex(conference, rooms.map(roomName), dayIndex);
    return rooms.filter(r => rns.includes(roomName(r)));
};

// :: Conference -> List[Room] -> Day => List[Room]
let roomsByDay = (conference, rooms, day) =>
    roomsByDayIndex(conference, rooms, dayIndex(day));


// :: Conference -> List[RoomName] -> PersonId -> List[RoomName]
let roomNamesByPersonId = (conference, roomNames, id) => {
    let evs = eventsByPersonId(conferenceEvents(conference), id);
    let rns = distinct(evs.map(eventRoomName));
    return distinct(roomNames.filter(rn => rns.includes(rn)));
};

// :: Conference -> List[RoomName] -> Person -> List[RoomName]
let roomNamesByPerson = (conference, roomNames, person) =>
    roomNamesByPersonId(conference, roomNames, personId(person));

// :: Conference -> List[Room] -> PersonId -> List[Room]
let roomsByPersonId = (conference, rooms, id) => {
    let rns = roomNamesByPersonId(conference, rooms.map(roomName), id);
    return rooms.filter(r => rns.includes(roomName(r)));
};

// :: Conference -> List[Room] -> Person -> List[Room]
let roomsByPerson = (conference, rooms, person) =>
    roomsByPersonId(conference, rooms, personId(person));


// :: Conference -> List[RoomName] -> Track -> List[RoomName]
let roomNamesByTrack = (conference, roomNames, track) => {
    let evs = eventsByTrack(conferenceEvents(conference), track);
    let rns = evs.map(eventRoomName);
    return distinct(roomNames.filter(rn => rns.includes(rn)));
};

// :: Conference -> List[Room] -> Track -> List[Room]
let roomsByTrack = (conference, rooms, track) =>
    roomNamesByTrack(conference, rooms.map(roomName), track).flatMap(rn =>
        roomsByName(rooms, rn));

// :: Conference -> List[RoomName] -> Type -> List[RoomName]
let roomNamesByType = (conference, roomNames, type) => {
    let evs = eventsByType(conferenceEvents(conference), type);
    let rns = evs.map(eventRoomName);
    return distinct(roomNames.filter(rn => rns.includes(rn)));
};

// :: Conference -> List[Room] -> Type -> List[Room]
let roomsByType = (conference, rooms, type) =>
    roomNamesByType(conference, rooms.map(roomName), type).flatMap(rn =>
        roomsByName(rooms, rn));

// :: Conference -> List[RoomName] -> Language -> List[RoomName]
let roomNamesByLanguage = (conference, roomNames, language) => {
    let evs = eventsByLanguage(conferenceEvents(conference), language);
    let rns = evs.map(eventRoomName);
    return distinct(roomNames.filter(rn => rns.includes(rn)));
};

// :: Conference -> List[Room] -> Language -> List[Room]
let roomsByLanguage = (conference, rooms, language) =>
    roomNamesByLanguage(conference, rooms.map(roomName), language).flatMap(rn =>
        roomsByName(rooms, rn));


// :: Conference -> List[PersonId] -> DayIndex -> List[PersonId]
let personIdsByDayIndex = (conference, personIds, dayIndex) => {
    let evs = eventsByDayIndex(conferenceEvents(conference), dayIndex);
    let pids = distinct(evs.flatMap(eventPersons).map(personId));
    return distinct(personIds.filter(pi => pids.includes(pi)));
};

// :: Conference -> List[PersonId] -> Day -> List[PersonId]
let personIdsByDay = (conference, personIds, day) =>
    personIdsByDayIndex(conference, personIds, dayIndex(day));

// :: Conference -> List[Person] -> DayIndex -> List[Person]
let personsByDayIndex = (conference, persons, dayIndex) =>
    personIdsByDayIndex(conference, persons.map(personId), dayIndex).map(pi =>
        personById(persons, pi));

// :: Conference -> List[Person] -> Day -> List[Person]
let personsByDay = (conference, persons, day) =>
    personsByDayIndex(conference, persons, dayIndex(day));


// :: Conference -> List[PersonId] -> RoomName -> List[PersonId]
let personIdsByRoomName = (conference, personIds, roomName) => {
    let evs = eventsByRoomName(conferenceEvents(conference), roomName);
    let pids = distinct(evs.flatMap(eventPersons).map(personId));
    return distinct(personIds.filter(pi => pids.includes(pi)));
};

// :: Conference -> List[PersonId] -> Room -> List[PersonId]
let personIdsByRoom = (conference, personIds, room) =>
    personIdsByRoomName(conference, personIds, roomName(room));

// :: Conference -> List[Person] -> RoomName -> List[Person]
let personsByRoomName = (conference, persons, roomName) =>
    personIdsByRoomName(conference, persons.map(personId), roomName).map(pi =>
        personById(persons, pi));

// :: Conference -> List[Person] -> Room -> List[Person]
let personsByRoom = (conference, persons, room) =>
    personsByRoomName(conference, persons, roomName(room));


// :: Conference -> List[PersonId] -> Track -> List[PersonId]
let personIdsByTrack = (conference, personIds, track) => {
    let evs = eventsByTrack(conferenceEvents(conference), track);
    let pids = evs.flatMap(eventPersons).map(personId);
    return distinct(personIds.filter(pi => pids.includes(pi)));
};

// :: Conference -> List[PersonId] -> Track -> List[PersonId]
let personsByTrack = (conference, persons, track) =>
    personIdsByTrack(conference, persons.map(personId), track).map(pi =>
        personById(persons, pi));

// :: Conference -> List[PersonId] -> Type -> List[PersonId]
let personIdsByType = (conference, personIds, type) => {
    let evs = eventsByType(conferenceEvents(conference), type);
    let pids = evs.flatMap(eventPersons).map(personId);
    return distinct(personIds.filter(pi => pids.includes(pi)));
};

// :: Conference -> List[Person] -> Type -> List[Person]
let personsByType = (conference, persons, type) =>
    personIdsByType(conference, persons.map(personId), type).map(pi =>
        personById(persons, pi));

// :: Conference -> List[PersonId] -> Language -> List[PersonId]
let personIdsByLanguage = (conference, personIds, language) => {
    let evs = eventsByLanguage(conferenceEvents(conference), language);
    let pids = evs.flatMap(eventPersons).map(personId);
    return distinct(personIds.filter(pi => pids.includes(pi)));
};

// :: Conference -> List[Person] -> Language -> List[Person]
let personsByLanguage = (conference, persons, language) =>
    personIdsByLanguage(conference, persons.map(personId), language).map(pi =>
        personById(persons, pi));


// :: Conference -> List[DayIndex] -> RoomName -> List[DayIndex]
let dayIndicesByRoomName = (conference, dayIndices, roomName) => {
    let evs = eventsByRoomName(conferenceEvents(conference), roomName);
    let dis = distinct(evs.map(eventDayIndex));
    return distinct(dayIndices.filter(di => dis.includes(di)));
};

// :: Conference -> List[DayIndex] -> Room -> List[DayIndex]
let dayIndicesByRoom = (conference, dayIndices, room) =>
    dayIndicesByRoomName(conference, dayIndices, roomName(room));

// :: Conference -> List[Day] -> RoomName -> List[Day]
let daysByRoomName = (conference, days, roomName) =>
    dayIndicesByRoomName(conference, days.map(dayIndex), roomName).map(di =>
        dayByIndex(days, di));

// :: Conference -> List[Day] -> Room -> List[Day]
let daysByRoom = (conference, days, room) =>
    daysByRoomName(conference, days, roomName(room));


// :: Conference -> List[DayIndex] -> PersonId -> List[DayIndex]
let dayIndicesByPersonId = (conference, dayIndices, id) => {
    let evs = eventsByPersonId(conferenceEvents(conference), id);
    let dis = distinct(evs.map(eventDayIndex));
    return distinct(dayIndices.filter(di => dis.includes(di)));
};

// :: Conference -> List[DayIndex] -> Person -> List[DayIndex]
let dayIndicesByPerson = (conference, dayIndices, person) =>
    dayIndicesByPersonId(conference, dayIndices, personId(person));

// :: Conference -> List[Day] -> PersonId -> List[Day]
let daysByPersonId = (conference, days, id) =>
    dayIndicesByPersonId(conference, days.map(dayIndex), id).map(di =>
        dayByIndex(days, di));

// :: Conference -> List[Day] -> Person -> List[Day]
let daysByPerson = (conference, days, person) =>
    daysByPersonId(conference, days, personId(person));


// :: Conference -> List[DayIndex] -> Track -> List[DayIndex]
let dayIndicesByTrack = (conference, dayIndices, track) => {
    let evs = eventsByTrack(conferenceEvents(conference), track);
    let dis = evs.map(eventDayIndex);
    return distinct(dayIndices.filter(di => dis.includes(di)));
};

// :: Conference -> List[Day] -> Track -> List[Day]
let daysByTrack = (conference, days, track) =>
    dayIndicesByTrack(conference, days.map(dayIndex), track).map(di =>
        dayByIndex(days, di));

// :: Conference -> List[DayIndex] -> Type -> List[DayIndex]
let dayIndicesByType = (conference, dayIndices, type) => {
    let evs = eventsByType(conferenceEvents(conference), type);
    let dis = evs.map(eventDayIndex);
    return distinct(dayIndices.filter(di => dis.includes(di)));
};

// :: Conference -> List[Day] -> Type -> List[Day]
let daysByType = (conference, days, type) =>
    dayIndicesByType(conference, days.map(dayIndex), type).map(di =>
        dayByIndex(days, di));

// :: Conference -> List[DayIndex] -> Language -> List[DayIndex]
let dayIndicesByLanguage = (conference, dayIndices, language) => {
    let evs = eventsByLanguage(conferenceEvents(conference), language);
    let dis = evs.map(eventDayIndex);
    return distinct(dayIndices.filter(di => dis.includes(di)));
};

// :: Conference -> List[Day] -> Language -> List[Day]
let daysByLanguage = (conference, days, language) =>
    dayIndicesByLanguage(conference, days.map(dayIndex), language).map(di =>
        dayByIndex(days, di));


// :: Conference -> List[Track] -> DayIndex -> List[Track]
let tracksByDayIndex = (conference, tracks, dayIndex) =>
    tracksOfEvents(eventsByDayIndex(conferenceEvents(conference), dayIndex));

// :: Conference -> List[Track] -> Day -> List[Track]
let tracksByDay = (conference, tracks, day) =>
    tracksOfEvents(eventsByDay(conferenceEvents(conference), day));

// :: Conference -> List[Track] -> RoomName -> List[Track]
let tracksByRoomName = (conference, tracks, roomName) =>
    tracksOfEvents(eventsByRoomName(conferenceEvents(conference), roomName));

// :: Conference -> List[Track] -> Room -> List[Track]
let tracksByRoom = (conference, tracks, room) =>
    tracksOfEvents(eventsByRoom(conferenceEvents(conference), room));


// :: Conference -> List[Type] -> DayIndex -> List[Type]
let typesByDayIndex = (conference, types, dayIndex) =>
    typesOfEvents(eventsByDayIndex(conferenceEvents(conference), dayIndex));

// :: Conference -> List[Type] -> Day -> List[Type]
let typesByDay = (conference, types, day) =>
    typesOfEvents(eventsByDay(conferenceEvents(conference), day));

// :: Conference -> List[Type] -> RoomName -> List[Type]
let typesByRoomName = (conference, types, roomName) =>
    typesOfEvents(eventsByRoomName(conferenceEvents(conference), roomName));

// :: Conference -> List[Type] -> Room -> List[Type]
let typesByRoom = (conference, types, room) =>
    typesOfEvents(eventsByRoom(conferenceEvents(conference), room));


// :: Conference -> List[Language] -> DayIndex -> List[Language]
let languagesByDayIndex = (conference, languages, dayIndex) =>
    languagesOfEvents(eventsByDayIndex(conferenceEvents(conference), dayIndex));

// :: Conference -> List[Language] -> Day -> List[Language]
let languagesByDay = (conference, languages, day) =>
    languagesOfEvents(eventsByDay(conferenceEvents(conference), day));

// :: Conference -> List[Language] -> RoomName -> List[Language]
let languagesByRoomName = (conference, languages, roomName) =>
    languagesOfEvents(eventsByRoomName(conferenceEvents(conference), roomName));

// :: Conference -> List[Language] -> Room -> List[Language]
let languagesByRoom = (conference, languages, room) =>
    languagesOfEvents(eventsByRoom(conferenceEvents(conference), room));


// ---------------------------------------------------------
// Timing of Events and Days

// :: List[Event] -> luxon.DateTime -> List[Event]
let futureEvents = (events, now) =>
    events.filter(e => now.toMillis() < eventStartDate(e).toMillis());

// :: List[Event] -> luxon.DateTime -> List[Event]
let currentEvents = (events, now) =>
    events.filter(e =>
        (eventStartDate(e).toMillis() <= now.toMillis() &&
         now.toMillis() <= eventEndDate(e).toMillis()));

// :: List[Event] -> luxon.DateTime -> List[Event]
let pastEvents = (events, now) =>
    events.filter(e => eventEndDate(e).toMillis() < now.toMillis());


// :: List[Event] -> luxon.DateTime -> luxon.Duration -> List[Event]
let startingEvents = (events, now, dt) =>
    events.filter(e =>
        (eventStartDate(e).minus(dt).toMillis() <= now.toMillis() &&
         now.toMillis() < eventStartDate(e).toMillis()));

// :: List[Event] -> luxon.DateTime -> luxon.Duration -> List[Event]
let startedEvents = (events, now, dt) =>
    events.filter(e =>
        (eventStartDate(e).toMillis() <= now.toMillis() &&
         now.toMillis() <= eventStartDate(e).plus(dt).toMillis()));

// :: List[Event] -> luxon.DateTime -> luxon.Duration -> List[Event]
let endingEvents = (events, now, dt) =>
    events.filter(e =>
        (eventEndDate(e).minus(dt).toMillis() <= now.toMillis() &&
         now.toMillis() <= eventEndDate(e).toMillis()));

// :: List[Event] -> luxon.DateTime -> luxon.Duration -> List[Event]
let endedEvents = (events, now, dt) =>
    events.filter(e =>
        (eventEndDate(e).toMillis() < now.toMillis() &&
         now.toMillis() <= eventStartDate(e).plus(dt).toMillis()));


// :: List[Event] -> List[Event]
let sortEventsByStartDate = (events) =>
    events.sort((e1, e2) =>
        eventStartDate(e1).toMillis() - eventStartDate(e2).toMillis());

// :: List[Event] -> List[Event]
let sortEventsByEndDate = (events) =>
    events.sort((e1, e2) =>
        eventEndDate(e1).toMillis() - eventEndDate(e2).toMillis());

// :: List[Event] -> List[Event]
let sortEventsByDuration = (events) =>
    events.sort((e1, e2) =>
        eventDuration(e1).toMillis() - eventDuration(e2).toMillis());


// :: List[Day] -> luxon.DateTime -> List[Day]
let futureDays = (days, now) =>
    days.filter(d => now.toMillis() < dayStartDate(d).toMillis());

// :: List[Day] -> luxon.DateTime -> List[Day]
let currentDays = (days, now) =>
    days.filter(d =>
        (dayStartDate(d).toMillis() <= now.toMillis() &&
         now.toMillis() <= dayEndDate(d).toMillis()));

// :: List[Day] -> luxon.DateTime -> List[Day]
let pastDays = (days, now) =>
    days.filter(d => dayEndDate(d).toMillis() < now.toMillis());


// :: List[Day] -> List[Day]
let sortDaysByStartDate = (days) =>
    days.sort((d1, d2) =>
        dayStartDate(d1).toMillis() - dayStartDate(d2).toMillis());

// :: List[Day] -> List[Day]
let sortDaysByEndDate = (days) =>
    days.sort((d1, d2) =>
        dayEndDate(d1).toMillis() - dayEndDate(d2).toMillis());

// :: List[Day] -> List[Day]
let sortDaysByDuration = (days) =>
    days.sort((d1, d2) =>
        dayDuration(d1).toMillis() - dayDuration(d2).toMillis());


// ---------------------------------------------------------
// Extractors: Shortcut entry points

// :: Schedule -> List[Day]
let allDays = (schedule) => conferenceDays(conference(schedule));

// :: Schedule -> List[Room]
let allRooms = (schedule) => conferenceRooms(conference(schedule));

// :: Schedule -> List[Event]
let allEvents = (schedule) => conferenceEvents(conference(schedule));

// :: Schedule -> List[Person]
let allPersons = (schedule) => conferencePersons(conference(schedule));

// :: Schedule -> List[TrackName]
let allTracks = (schedule) => conferenceTracks(conference(schedule));

// :: Schedule -> List[EventType]
let allTypes = (schedule) => conferenceTypes(conference(schedule));

// :: Schedule -> List[Language]
let allLanguages = (schedule) => conferenceLanguages(conference(schedule));


// ---------------------------------------------------------
// Public API Exports

export {
    optional,
    defined,
    withDefined,
    distinct,
    intersection,
    processScheduleFile,
    scheduleVersion,
    scheduleBaseUrl,
    conferenceAcronym,
    conferenceTitle,
    conferenceStartDate,
    conferenceEndDate,
    conferenceDuration,
    conferenceDaysCount,
    conferenceTimeslotDuration,
    conferenceTimezoneName,
    dayIndex,
    dayDate,
    dayStartDate,
    dayEndDate,
    dayDuration,
    roomName,
    roomGuid,
    roomDescription,
    roomCapacity,
    eventId,
    eventGuid,
    eventLogo,
    eventStartDate,
    eventEndDate,
    eventDuration,
    eventDayIndex,
    eventRoomName,
    eventSlug,
    eventUrl,
    eventTitle,
    eventSubtitle,
    eventTrack,
    eventType,
    eventLanguage,
    eventAbstract,
    eventDescription,
    eventRecordingLicense,
    eventDoNotRecord,
    eventPersons,
    eventPersonCount,
    eventAnswers,
    personId,
    personCode,
    personName,
    personBiography,
    eventsByDayIndex,
    eventsByDay,
    eventsByRoomName,
    eventsByRoom,
    eventsByPersonId,
    eventsByPersonName,
    eventsByPerson,
    eventsByTrack,
    eventsByType,
    eventsByLanguage,
    roomById,
    roomsByName,
    dayByIndex,
    personById,
    personsByName,
    eventById,
    distinctRooms,
    distinctDays,
    distinctEvents,
    distinctPersons,
    sortPersonsByName,
    sortRoomsByName,
    sortEventsByTitle,
    eventsOfDay,
    eventsOfDays,
    tracksOfEvents,
    typesOfEvents,
    languagesOfEvents,
    conferenceDays,
    conferenceRooms,
    conferenceEvents,
    conferencePersons,
    conferenceTracks,
    conferenceTypes,
    conferenceLanguages,
    conference,
    roomNamesByDayIndex,
    roomNamesByDay,
    roomsByDayIndex,
    roomsByDay,
    roomNamesByPersonId,
    roomNamesByPerson,
    roomsByPersonId,
    roomsByPerson,
    roomNamesByTrack,
    roomsByTrack,
    roomNamesByType,
    roomsByType,
    roomNamesByLanguage,
    roomsByLanguage,
    personIdsByDayIndex,
    personIdsByDay,
    personsByDayIndex,
    personsByDay,
    personIdsByRoomName,
    personIdsByRoom,
    personsByRoomName,
    personsByRoom,
    personIdsByTrack,
    personsByTrack,
    personIdsByType,
    personsByType,
    personIdsByLanguage,
    personsByLanguage,
    dayIndicesByRoomName,
    dayIndicesByRoom,
    daysByRoomName,
    daysByRoom,
    dayIndicesByPersonId,
    dayIndicesByPerson,
    daysByPersonId,
    daysByPerson,
    dayIndicesByTrack,
    daysByTrack,
    dayIndicesByType,
    daysByType,
    dayIndicesByLanguage,
    daysByLanguage,
    tracksByDayIndex,
    tracksByDay,
    tracksByRoomName,
    tracksByRoom,
    typesByDayIndex,
    typesByDay,
    typesByRoomName,
    typesByRoom,
    languagesByDayIndex,
    languagesByDay,
    languagesByRoomName,
    languagesByRoom,
    futureEvents,
    currentEvents,
    pastEvents,
    startingEvents,
    startedEvents,
    endingEvents,
    endedEvents,
    sortEventsByStartDate,
    sortEventsByEndDate,
    sortEventsByDuration,
    futureDays,
    currentDays,
    pastDays,
    sortDaysByStartDate,
    sortDaysByEndDate,
    sortDaysByDuration,
    allDays,
    allRooms,
    allEvents,
    allPersons,
    allTracks,
    allTypes,
    allLanguages
};

// Command to generate API exports:
// grep -A 1 -e '// :: ' sol.js | sed -e '/^--$/d' -e '/ :: /d' -e 's/^let /    /g' -e 's/ = .*$/,/g' > names.txt
// Note: Type signatures starting with '//-:: ' instead of '// :: ' denote unexported internal functions.
