/* Function to synchronise with tfl - blocks here until tfl site is first refreshed
   Use with caution as tfl doesn't seem to adhere strictly to update interval */
func SynchroniseWithTflArrivals(line string) {
	site := fmt.Sprintf("https://api.tfl.gov.uk/Line/%v/Arrivals?", line)
	var oldBody []byte
	for range time.Tick(time.Second) {
		body := webconn.GetBodyFromTfl(site)

		if len(oldBody) > 0 && !bytes.Equal(oldBody, body) {
			logger.GetLogger().Info("Synchronised with tfl arrivals for line %v", line)
			return
		}
		oldBody = body
	}
}
/* Returns map from naptan_id to ics code.
   Error is returned if failure to obtain ics code for any stop */
func GetIcsCodes(naptanIds []string) (map[string]string, error) {
	site := fmt.Sprintf("https://api.tfl.gov.uk/StopPoint/%v?", strings.Join(naptanIds, ","))
	body := webconn.GetBodyFromTfl(site)

	type StopPoint struct {
		Children []json.RawMessage `json:"children"`
	}

	var stopPoints []StopPoint
	err := json.Unmarshal(body, &stopPoints)

	/* Sometimes Tfl returns "Technical error".
	   In such case we wait a few seconds and retry */
	if err != nil {
		time.Sleep(10 * time.Second)
		return GetIcsCodes(naptanIds)
	}

	icsCodes := make(map[string]string)

	/* Convert naptanIds to a map for easy membership testing later on */
	naptanIdsMap := make(map[string]bool)
	for _, naptanId := range naptanIds {
		naptanIdsMap[naptanId] = true
	}

	for _, stopPoint := range stopPoints {
		for _, child := range stopPoint.Children {
			/* Find out which naptanId from our slice does this stop point correspond to
			   (Recursion may be required)
			   If fail skip and continue*/
			naptanId, ics, err := recurseAndGetChildIcs(&child, naptanIdsMap)
			if err == nil {
				icsCodes[naptanId] = ics
				break
			}
		}
	}

	/* Final check: make sure all naptan ids in the original request are covered */
	for _, naptanId := range naptanIds {
		_, exists := icsCodes[naptanId]
		if !exists {
			return nil, errors.New(
				fmt.Sprintf("Failed to find ics code for stop %v", naptanId))
		}
	}
	return icsCodes, nil
}
func getDirectionsFromTfl(fromLat, fromLng, toLat, toLng string, isDeparting bool, targetTime time.Time) TflDirections {
	/* Configure parameters to fit URL */
	timeIsStr := "departing"
	if !isDeparting {
		timeIsStr = "arriving"
	}

	dateStrFormat := "20060102"
	dateStr := targetTime.Format(dateStrFormat)

	timeStrFormat := "1504"
	timeStr := targetTime.Format(timeStrFormat)

	site := fmt.Sprintf("https://api.tfl.gov.uk/Journey/JourneyResults/%v,%v/to/%v,%v?timeIs=%v&mode=bus&date=%v&time=%v",
		fromLat, fromLng, toLat, toLng, timeIsStr, dateStr, timeStr)
	return parseDirectionsFromBody(webconn.GetBodyFromTfl(site))
}
/* Returns whether there was a bus that departed,
   and if yes, its registration number.
   Also new departure schedule.
   Time of departure.
   Error if no service operating at that time e.g. night buses

   site - tfl API website to query from to obtain json response body
   departureSchedule - map from vehicle id to expected departure time */
func monitorForDeparture(site string, departureSchedule map[string]string, line, bound string) (bool, string, map[string]string, time.Time, error) {
	body := webconn.GetBodyFromTfl(site)

	type BusInfo struct {
		VehicleId         string `json:"vehicleId"`
		ExpectedDeparture string `json:"expectedArrival"`
	}

	var busInfo []BusInfo

	err := json.Unmarshal(body, &busInfo)

	/* Sometimes Tfl returns "Technical error".
	   In such case we retry after a few seconds */
	if err != nil {
		logger.GetLogger().Error("Tfl failed to give proper arrivals response: %v", string(body))
		time.Sleep(10 * time.Second)
		return monitorForDeparture(site, departureSchedule, line, bound)
	}

	if len(busInfo) == 0 {
		return false, "", nil, time.Time{}, errors.New(
			fmt.Sprintf("No bus operating for line %v (%v) now", line, bound))
	}

	newDepartureSchedule := make(map[string]string)
	for _, bus := range busInfo {
		newDepartureSchedule[bus.VehicleId] = bus.ExpectedDeparture
	}

	for vehicleId, _ := range departureSchedule {
		_, exists := newDepartureSchedule[vehicleId]
		if !exists {
			departureTime := departureSchedule[vehicleId]
			if timeStampHasPassed(departureTime) {
				return true, vehicleId, nil, parseTflTimeStamp(departureTime), nil
			} else {
				return false, "", departureSchedule, time.Time{}, nil
			}

		}
	}

	return false, "", newDepartureSchedule, time.Time{}, nil
}
/* Returns map of journey times (in minutes) (to_id => journey time between from_id and to_id) */
func getJourneyTimes(line, bound, startingPoint string) map[string]int {
	site := fmt.Sprintf("https://api.tfl.gov.uk/Line/%v/Timetable/%v?direction=%v", line, startingPoint, bound)
	cumulativeJourneyTimes := parseCumulativeJourneyTimesFromBody(line, bound, webconn.GetBodyFromTfl(site))

	journeyTimes := make(map[string]int)
	for i, m := range cumulativeJourneyTimes {
		for to_id, cumTime := range m {
			if i > 0 {
				prevMap := cumulativeJourneyTimes[i-1]
				for _, prevCumTime := range prevMap {
					cumTime = cumTime - prevCumTime
				}
			}
			journeyTimes[to_id] = cumTime
		}
	}
	return journeyTimes
}
func getNewStopInfos(stops []string) map[string]map[string]string {
	commaDelimitedStops := strings.Join(stops, ",")
	site := fmt.Sprintf("https://api.tfl.gov.uk/StopPoint/%v?", commaDelimitedStops)

	/* Create map out of stops for fast lookup later */
	stopsMap := make(map[string]bool)
	for _, v := range stops {
		stopsMap[v] = true
	}

	// Retry if got 'technical error' from tfl
	var stopInfos map[string]map[string]string
	var err error

	for ok := true; ok; ok = (err != nil) {
		stopInfos, err = parseStopsFromBody(webconn.GetBodyFromTfl(site), stopsMap)
	}
	return stopInfos
}
/* Returns whether the bus arrived,
   and if yes, its time of arrival.
   Also new departure schedule

   site - tfl API website to query from to obtain json response body
   vehicleId - the vehicle id wait for until appearance and disappearance
   departureSchedule - map from vehicle id to expected departure time */
func monitorForArrival(site, vehicleId string, departureSchedule map[string]string) (bool, time.Time, map[string]string) {
	body := webconn.GetBodyFromTfl(site)

	type BusInfo struct {
		VehicleId         string `json:"vehicleId"`
		ExpectedDeparture string `json:"expectedArrival"`
	}

	var busInfo []BusInfo

	err := json.Unmarshal(body, &busInfo)

	/* Sometimes Tfl returns "Technical error".
	   In such case we skip and return gracefully */
	if err != nil {
		logger.GetLogger().Error("Tfl failed to give proper arrivals response: %v", string(body))
		time.Sleep(10 * time.Second)
		return monitorForArrival(site, vehicleId, departureSchedule)
	}

	newDepartureSchedule := make(map[string]string)
	for _, bus := range busInfo {
		if bus.VehicleId == vehicleId {
			newDepartureSchedule[bus.VehicleId] = bus.ExpectedDeparture
		}
	}

	// If target bus has just disappeared arrivals board, it is assumed arrived
	expectedDeparture, existsOnPreviousBoard := departureSchedule[vehicleId]
	_, existsOnCurrentBoard := newDepartureSchedule[vehicleId]
	if existsOnPreviousBoard && !existsOnCurrentBoard {
		if timeStampHasPassed(expectedDeparture) {
			return true, parseTflTimeStamp(expectedDeparture), nil
		} else {
			return false, time.Time{}, departureSchedule
		}
	}

	return false, time.Time{}, newDepartureSchedule
}
/* Returns the expected journey time (in minutes) returned from tfl journey planner */
func getTflExpectedJourneyTime(line, bound, origin, destination, vehicleId string) (time.Duration, error) {
	icsCodes, err := geography.GetIcsCodes([]string{origin, destination})
	if err != nil {
		logger.GetLogger().Panic(err)
	}

	site := fmt.Sprintf("https://api.tfl.gov.uk/Journey/JourneyResults/%v/to/%v?mode=bus&journeyPreference=leastinterchange",
		icsCodes[origin], icsCodes[destination])
	body := webconn.GetBodyFromTfl(site)

	type JourneyResult struct {
		Journeys []json.RawMessage `json:"journeys"`
	}

	var journeyResult JourneyResult
	err = json.Unmarshal(body, &journeyResult)

	/* Sometimes Tfl returns "Technical error".
	   In such case we retry after a few seconds */
	if err != nil {
		time.Sleep(10 * time.Second)
		return getTflExpectedJourneyTime(line, bound, origin, destination, vehicleId)
	}

	// Assume the first journey in the results are the one we want
	if len(journeyResult.Journeys) == 0 {
		return 0, errors.New(
			fmt.Sprintf("No journey found for line %v (%v) %v -> %v. Aborting",
				line, bound, origin, destination))
	}
	for _, potentialJourney := range journeyResult.Journeys {
		journeyTime, err := extractTimeOnBusFromJourney(&potentialJourney, line, bound, origin, destination)
		if err == nil {
			return time.Duration(journeyTime) * time.Minute, nil
		}
	}
	return 0, err
}
func pollTflArrivals(line string, cache map[string]map[string]string) {
	site := fmt.Sprintf("https://api.tfl.gov.uk/Line/%v/Arrivals?", line)
	arrivalsBundle := parseArrivalsFromBody(webconn.GetBodyFromTfl(site))
	updateCacheDBWithArrivalsBundle(line, arrivalsBundle, cache, nil)
}
func pollAllTflArrivals(lines map[string]bool, numPredictionsPerStop *int,
	arrivalsChannels map[string]chan map[string]map[string]string) {

	site := fmt.Sprintf("https://api.tfl.gov.uk/Mode/bus/Arrivals?count=%v", *numPredictionsPerStop)
	parseAllArrivalsFromBody(webconn.GetBodyFromTfl(site), arrivalsChannels)
}
func pollAllTflTerminusDepartures(numPredictionsPerStop *int) {

	site := fmt.Sprintf("https://api.tfl.gov.uk/Mode/bus/Arrivals?count=%v", *numPredictionsPerStop)
	parseAllTerminusDeparturesFromBody(webconn.GetBodyFromTfl(site), numPredictionsPerStop)
}
/* Returns 2 maps,
   the first from naptan_id to longitude, and
   the second from naptan_id to latitude.
   Error is returned if failure to obtain coordinates for any stop */
func GetLongLat(naptanIds []string) (map[string]float64, map[string]float64, error) {
	site := fmt.Sprintf("https://api.tfl.gov.uk/StopPoint/%v?", strings.Join(naptanIds, ","))
	body := webconn.GetBodyFromTfl(site)

	type LineGroup struct {
		NaptanIdReference string `json:"naptanIdReference"`
	}

	type StopPoint struct {
		CommonName string  `json:"commonName"`
		Longitude  float64 `json:"lon"`
		Latitude   float64 `json:"lat"`
		LineGroup  []LineGroup
	}

	var stopPoints []StopPoint
	err := json.Unmarshal(body, &stopPoints)

	/* Sometimes Tfl returns "Technical error".
	   In such case we wait a few seconds and retry */
	if err != nil {
		time.Sleep(10 * time.Second)
		return GetLongLat(naptanIds)
	}

	longitudes := make(map[string]float64)
	latitudes := make(map[string]float64)

	/* Convert naptanIds to a map for easy membership testing later on */
	naptanIdsMap := make(map[string]bool)
	for _, naptanId := range naptanIds {
		naptanIdsMap[naptanId] = true
	}

	for _, stopPoint := range stopPoints {
		/* Find out which naptanId from our slice does this stop point correspond to
		   If fail skip and continue*/
		naptanId := ""
		for _, lineGroup := range stopPoint.LineGroup {
			if naptanIdsMap[lineGroup.NaptanIdReference] {
				naptanId = lineGroup.NaptanIdReference
				break
			}
		}
		if naptanId == "" {
			logger.GetLogger().Error("Failed to find original naptan id for stop %v", stopPoint.CommonName)
			continue
		}

		longitudes[naptanId] = stopPoint.Longitude
		latitudes[naptanId] = stopPoint.Latitude
	}

	/* Final check: make sure all naptan ids in the original request are covered */
	for _, naptanId := range naptanIds {
		_, longitudeExists := longitudes[naptanId]
		_, latitudeExists := latitudes[naptanId]
		if !longitudeExists || !latitudeExists {
			return nil, nil, errors.New(
				fmt.Sprintf("Failed to find longitude/latitude for stop %v", naptanId))
		}
	}
	return longitudes, latitudes, nil
}
func getNewLineStopPoints(line, bound string) ([]string, map[string]map[string]string) {
	site := fmt.Sprintf("https://api.tfl.gov.uk/Line/%v/Route/Sequence/%v?", line, bound)
	return parseLineStopPointsFromBody(webconn.GetBodyFromTfl(site))
}
func getTflEstimate(line, bound, originIcs, destIcs string) time.Duration {
	site := fmt.Sprintf("https://api.tfl.gov.uk/Journey/JourneyResults/%v/to/%v?mode=bus&journeyPreference=leastinterchange",
		originIcs, destIcs)
	return parseTflEstimateFromBody(line, webconn.GetBodyFromTfl(site))
}
func getNewLines() mapset.Set {
	site := "https://api.tfl.gov.uk/Line/Route?"
	return mapset.NewSetFromSlice(parseLinesFromBody(webconn.GetBodyFromTfl(site)))
}
func getNewTimetable(line, bound, stationNaptan string) (map[time.Weekday]map[selectdb.LocalTime]bool, error) {
	site := fmt.Sprintf("https://api.tfl.gov.uk/Line/%v/Timetable/%v?direction=%v", line, stationNaptan, bound)
	return parseTimetableFromBody(webconn.GetBodyFromTfl(site), line, bound, stationNaptan)
}