/* Generates predictions using past data for journeys taking place at the specified times */
func predictJourneyTimesWithDetails(line, bound, fromStop, toStop string, times []time.Time, method performanceeval.Method,
	realTimeEstimatesCache map[string]map[string]map[time.Time]map[string]int64) (map[time.Time]time.Duration, map[time.Time]map[string]map[string]time.Duration, int) {
	intermediateStopsOrdered := selectdb.SelectIntermediateStops(line, bound, fromStop, toStop)
	intermediateStops := make(map[string]map[string]bool)
	for i := 0; i < len(intermediateStopsOrdered)-1; i++ {
		intermediateStops[intermediateStopsOrdered[i]] = map[string]bool{
			intermediateStopsOrdered[i+1]: true,
		}
	}

	predictions := make(map[time.Time]time.Duration)
	predictionsAdjacentStops := make(map[time.Time]map[string]map[string]time.Duration)

	for _, departureTime := range times {
		prediction, _, predictionAdjacentStops, _, _, _, err := performanceeval.PredictIndividualJourneyWithCache(line, bound, intermediateStops, intermediateStopsOrdered,
			departureTime, method, realTimeEstimatesCache)

		if err != nil {
			// If fail to generate prediction due to lack of historic data, skip
			continue
		}
		predictions[departureTime] = prediction
		predictionsAdjacentStops[departureTime] = predictionAdjacentStops
	}
	return predictions, predictionsAdjacentStops, len(intermediateStopsOrdered) - 1
}
func getRoutePaths(linesToPlot map[string]map[string]map[string]string) map[string][]LatLng {
	routePaths := make(map[string][]LatLng)
	bound := "outbound"

	for line, lineDetails := range linesToPlot {
		fromStop := lineDetails[bound]["fromStop"]
		toStop := lineDetails[bound]["toStop"]
		intermediateStops := selectdb.SelectIntermediateStops(line, bound, fromStop, toStop)

		path := make([]LatLng, len(intermediateStops))

		intermediateStopDetails := selectdb.SelectMultipleStops(intermediateStops)
		for i, stop := range intermediateStops {
			lat, err := strconv.ParseFloat(intermediateStopDetails[stop]["latitude"], 64)
			if err != nil {
				logger.GetLogger().Panic(err)
			}

			lng, err := strconv.ParseFloat(intermediateStopDetails[stop]["longitude"], 64)
			if err != nil {
				logger.GetLogger().Panic(err)
			}

			path[i] = LatLng{
				Lat: lat,
				Lng: lng,
			}
		}
		routePaths[line] = path
	}
	return routePaths
}
/* To handle the situation when stops logged from Tfl are not consecutive,
   i.e. fromStop and toStop are > 1 stop apart
   Applies interpolation to estimate neighbouring journey times, and
   updates the values for insertion into database

   @See interpolateTimes for implementation details */
func interpolateStops(line, bound, vehicleId, fromStop, toStop, oldTimeStamp, newTimeStamp, oldTflTimeStamp, newTflTimeStamp string,
	values, skippedStopsValues, rawArrivalTimes *[]string, cachedEntry map[string]string) {

	// Discard if interpolating too many stops (>= 10 stops)
	interpolationThreshold := 10

	// Find intermediate stops
	intermediateStops := selectdb.SelectIntermediateStops(line, bound, fromStop, toStop)
	intermediateStopsLength := len(intermediateStops)
	if intermediateStopsLength <= 2 {
		logger.GetLogger().Panicf("Expected at least one intermediate stop for line %v (%v), between %v and %v. But got none.",
			line, bound, fromStop, toStop)
	}

	if intermediateStopsLength >= interpolationThreshold {
		logger.GetLogger().Error("Bus %v of line %v (%v) skipped too many stops (%v stops), between %v and %v. Aborting interpolation.",
			vehicleId, line, bound, intermediateStopsLength, fromStop, toStop)
		return
	}

	/* Find out interpolated times */
	interpolatedTimes, err := interpolateTimes(line, bound, vehicleId, intermediateStops, oldTimeStamp, newTimeStamp, cachedEntry, rawArrivalTimes)

	if err != nil {
		logger.GetLogger().Error(err.Error())
		return
	}

	for i := 0; i < intermediateStopsLength-1; i++ {
		if interpolatedTimes[i].IsZero() || interpolatedTimes[i+1].IsZero() {
			// Check that record is not invalidated
			continue
		}
		timeDiff := int(interpolatedTimes[i+1].Sub(interpolatedTimes[i]).Seconds())

		*values = append(*values, fmt.Sprintf("('%v', '%v', '%v', '%v', TIMESTAMP '%v', TIMESTAMP '%v', "+
			"'%v', TIMESTAMP '%v', TIMESTAMP '%v', INTERVAL '%v seconds', TRUE)",
			line, bound, vehicleId,
			intermediateStops[i], timeToPsqlTimeStamp(interpolatedTimes[i]), toPsqlTimeStamp(oldTflTimeStamp),
			intermediateStops[i+1], timeToPsqlTimeStamp(interpolatedTimes[i+1]), toPsqlTimeStamp(newTflTimeStamp),
			timeDiff))
	}

	// Strip end points away from intermediate points before inserting into skipped_stops_history
	intermediateStopsEndPointsExclusive := intermediateStops[1 : intermediateStopsLength-1]
	for i, v := range intermediateStopsEndPointsExclusive {
		intermediateStopsEndPointsExclusive[i] = "'" + v + "'"
	}

	*skippedStopsValues = append(*skippedStopsValues,
		fmt.Sprintf("('%v', '%v', '%v', '%v', TIMESTAMP '%v', TIMESTAMP '%v', '%v', TIMESTAMP '%v', TIMESTAMP '%v', ARRAY[%v], %v)",
			line, bound, vehicleId,
			intermediateStops[0], timeToPsqlTimeStamp(interpolatedTimes[0]), toPsqlTimeStamp(oldTflTimeStamp),
			intermediateStops[intermediateStopsLength-1], timeToPsqlTimeStamp(interpolatedTimes[intermediateStopsLength-1]), toPsqlTimeStamp(newTflTimeStamp),
			strings.Join(intermediateStopsEndPointsExclusive, ","), len(intermediateStopsEndPointsExclusive)))
}
func SimulateJourney2(line, bound string, details map[string]string) {

	/* Extract origin and destination from details (if any), if not assume entire journey */
	origin, exists := details["fromStop"]
	if !exists {
		origin = selectdb.SelectOrigin(line, bound)
	}
	destination, exists := details["toStop"]
	if !exists {
		destination = selectdb.SelectTerminus(line, bound)
	}
	stopPoints := selectdb.SelectIntermediateStops(line, bound, origin, destination)
	if len(stopPoints) == 0 {
		logger.GetLogger().Warning("Direction %v doesn't exist for line %v. Exiting...", bound, line)
		return
	}

	logger.GetLogger().Info("Blocking until line %v (%v) departs origin: %v", line, bound, origin)
	vehicleId, departureTime, err := blockUntilBusDeparts(line, bound, origin)

	/* May happen that night buses are not running during the day, so journey should not be simulated.
	   Wait for 30 minutes and check again. */
	if err != nil {
		logger.GetLogger().Error(err.Error())

		time.Sleep(sleepDuration)
		SimulateJourney2(line, bound, details)
		return
	}

	/* Spawn a new thread and simulate next journey now that bus has departed */
	go SimulateJourney2(line, bound, details)

	tflExpectedTime, err := getTflExpectedJourneyTime(line, bound, origin, destination, vehicleId)
	if err != nil {
		logger.GetLogger().Error("Failed to get tfl expected time for line %v (%v). Cause: %v. Aborting...",
			line, bound, err.Error())
		return
	}
	logger.GetLogger().Info("Tfl expected journey time for line %v (%v) is: %v", line, bound, tflExpectedTime)

	predictedTime, err := getPredictedJourneyTime2(line, bound, stopPoints)
	if err != nil {
		logger.GetLogger().Error("Failed to generate predicted time for line %v (%v). Cause: %v. Aborting",
			line, bound, err.Error())
		return
	}
	logger.GetLogger().Info("Predicted journey time for line %v (%v) is: %v", line, bound, predictedTime)

	actualTime, arrivalTime := getActualJourneyTime(line, bound, destination, vehicleId, departureTime)

	insertIntoPerformanceMeasureDB2(line, bound, vehicleId, origin, destination,
		tflExpectedTime, predictedTime, actualTime, departureTime, arrivalTime)
}
/* Takes average of historic journey times between adjacent stops,
   compare with "real time estimates", and sums up to form prediction of entire journey.

   Returns the total journey times, total standard deviation,
    map of individual chunks of journey times, map of standard deviations of individual chunks,
    expected departure time of from stop,
   and error if any. */
func PredictIndividualJourney(line, bound string, fromStop, toStop string,
	departureTime time.Time, method Method) (time.Duration, time.Duration, map[string]map[string]time.Duration, map[string]map[string]time.Duration, map[string]map[string]time.Time, map[string]map[string]string, error) {

	realTimeEstimatesCache := make(map[string]map[string]map[time.Time]map[string]int64)
	intermediateStopsOrdered := selectdb.SelectIntermediateStops(line, bound, fromStop, toStop)
	intermediateStops := make(map[string]map[string]bool)
	for i := 0; i < len(intermediateStopsOrdered)-1; i++ {
		intermediateStops[intermediateStopsOrdered[i]] = map[string]bool{
			intermediateStopsOrdered[i+1]: true,
		}
	}

	return PredictIndividualJourneyWithCache(line, bound, intermediateStops, intermediateStopsOrdered,
		departureTime, method, realTimeEstimatesCache)
}
/* Attempts to interpolate until terminus, then back to its reported location (toStop) if bus is found to have
   changed direction near the terminus. */
func interpolateIfChangedDirection(line, bound, vehicleId, fromStop, toStop,
	oldTimeStamp, newTimeStamp, oldTflTimeStamp, newTflTimeStamp string,
	values, skippedStopsValues, rawArrivalTimes *[]string, cachedEntry map[string]string) {
	terminus := selectdb.SelectTerminus(line, bound)

	// Nothing to extrapolate if we already at terminus
	if fromStop == terminus {
		return
	}

	/* Check that we have sufficient data to complete interpolation.
	   Interpolation not possible if predictions up till terminus do not exist, or that they are
	   in the future.  In such case return.*/
	intermediateStopsToTerminus := selectdb.SelectIntermediateStops(line, bound, fromStop, terminus)

	for _, stop := range intermediateStopsToTerminus {
		timeStamp, timeStampPresentInCachedEntry := cachedEntry[stop]
		if !timeStampPresentInCachedEntry || !timeStampHasPassed(timeStamp) {
			return
		}
	}
	timeAtTerminus := cachedEntry[terminus]

	/* Specially handle case where terminus and fromStop are only 1 stop apart since interpolateStops
	   won't work  - it assumes stops are at least 2 apart */
	if len(intermediateStopsToTerminus) == 2 {
		*values = append(*values, fmt.Sprintf("('%v', '%v', '%v', '%v', TIMESTAMP '%v', TIMESTAMP '%v', "+
			"'%v', TIMESTAMP '%v', TIMESTAMP '%v', INTERVAL '%v seconds', FALSE)",
			line, bound, vehicleId,
			fromStop, toPsqlTimeStamp(oldTimeStamp), toPsqlTimeStamp(oldTflTimeStamp),
			terminus, toPsqlTimeStamp(timeAtTerminus), toPsqlTimeStamp(newTflTimeStamp),
			timeDifference(oldTimeStamp, timeAtTerminus)))
	} else {
		interpolateStops(line, bound, vehicleId, fromStop, terminus, oldTimeStamp, timeAtTerminus,
			oldTflTimeStamp, newTflTimeStamp, values, skippedStopsValues, rawArrivalTimes, cachedEntry)
	}

	/* Similar to above -- Further interpolation to current reported location if possible */
	reverseBound := getReverseBound(line, bound)

	// Check that to stop is actually on reverse bound, if not return
	_, err := selectdb.SelectStopSeq(line, reverseBound, toStop)
	if err != nil {
		return
	}

	// Continue with interpolation on reverse bound
	reverseOrigin := selectdb.SelectOrigin(line, reverseBound)
	if toStop == reverseOrigin {
		return
	}

	intermediateStopsReverseBound := selectdb.SelectIntermediateStops(line, reverseBound, reverseOrigin, toStop)
	for _, stop := range intermediateStopsReverseBound {
		timeStamp, timeStampPresentInCachedEntry := cachedEntry[stop]
		if !timeStampPresentInCachedEntry || !timeStampHasPassed(timeStamp) {
			return
		}
	}
	timeAtReverseOrigin := cachedEntry[reverseOrigin]

	if len(intermediateStopsReverseBound) == 2 {
		*values = append(*values, fmt.Sprintf("('%v', '%v', '%v', '%v', TIMESTAMP '%v', TIMESTAMP '%v', "+
			"'%v', TIMESTAMP '%v', TIMESTAMP '%v', INTERVAL '%v seconds', FALSE)",
			line, reverseBound, vehicleId,
			reverseOrigin, toPsqlTimeStamp(timeAtReverseOrigin), toPsqlTimeStamp(oldTflTimeStamp),
			toStop, toPsqlTimeStamp(newTimeStamp), toPsqlTimeStamp(newTflTimeStamp),
			timeDifference(timeAtReverseOrigin, newTimeStamp)))
	} else {
		interpolateStops(line, reverseBound, vehicleId, reverseOrigin, toStop, timeAtReverseOrigin, newTimeStamp,
			oldTflTimeStamp, newTflTimeStamp, values, skippedStopsValues, rawArrivalTimes, cachedEntry)
	}
}
/* Find out which vehicle produced the data and list its journey history, together with
   the total actual journey time, total predictions and total standard deviations */
func lookupVehicleHistory2(line, bound, origin, destination string, departureTime time.Time, errorMessage *string) (string, []VehicleRecord2, time.Duration, map[performanceeval.Method]time.Duration, map[performanceeval.Method]time.Duration) {
	vehicleId, arrivalTime, err := selectdb.SelectVehicleAndArrivalTimeFromJourneyHistory(line, bound, origin, destination, departureTime)
	if err != nil {
		logger.GetLogger().Panic(err)
	}
	startTime := departureTime
	endTime := arrivalTime

	vehicleHistory := selectdb.SelectVehicleHistory(vehicleId, startTime, endTime, timeFormatString)

	rawArrivalTimes := selectdb.SelectRawArrivalTimes(vehicleId, startTime, endTime, timeFormatString)

	intermediateStopsOrdered := selectdb.SelectIntermediateStops(line, bound, origin, destination)
	intermediateStops := make(map[string]map[string]bool)
	for i := 0; i < len(intermediateStopsOrdered)-1; i++ {
		intermediateStops[intermediateStopsOrdered[i]] = map[string]bool{
			intermediateStopsOrdered[i+1]: true,
		}
	}

	/* Temporarily discarded */
	// predictionsUsedRealTime := make(map[string]map[string]map[time.Time]map[string]int64)

	totalPredictions := make(map[performanceeval.Method]time.Duration)
	totalStddevs := make(map[performanceeval.Method]time.Duration)
	idvPredictions := make(map[performanceeval.Method]map[string]map[string]time.Duration)
	idvStddevs := make(map[performanceeval.Method]map[string]map[string]time.Duration)
	idvExpectedArrivalTimesShifting := make(map[performanceeval.Method]map[string]map[string]time.Time)
	idvMixedTypes := make(map[performanceeval.Method]map[string]map[string]string)

	for _, method := range performanceeval.Methods() {
		totalPrediction, totalStddev, idvPredictionsMap, idvStddevsMap, edtShifting, mixedTypes := lookupPredictionsUsed(line, bound, intermediateStops, intermediateStopsOrdered, departureTime,
			method, nil, errorMessage)

		totalPredictions[method] = totalPrediction
		totalStddevs[method] = totalStddev
		idvPredictions[method] = idvPredictionsMap
		idvStddevs[method] = idvStddevsMap
		if method.IsShiftingBased() {
			idvExpectedArrivalTimesShifting[method] = edtShifting
		}
		if method.IsMixedBased() {
			idvMixedTypes[method] = mixedTypes
		}

	}

	actualJourneyTime := performanceeval.FetchActualJourneyTime(line, bound, origin, destination, departureTime)

	/* Flag for use later, in case bus switches direction invalidate and prevent travel time details from further generated */
	firstTimeFlag := true

	result := make([]VehicleRecord2, len(vehicleHistory))
	for i, v := range vehicleHistory {
		result[i] = VehicleRecord2{
			Line:           v["line"],
			Bound:          v["bound"],
			StopSeq:        v["stopSeq"],
			StopName:       v["stopName"],
			ArrivalTime:    v["time"],
			RawArrivalTime: rawArrivalTimes[v["naptanId"]],
			NaptanId:       v["naptanId"],
			Details: generateTravelTimeDetails(line, bound, intermediateStops, departureTime, vehicleHistory, i,
				idvPredictions, idvStddevs, idvExpectedArrivalTimesShifting, idvMixedTypes, &firstTimeFlag),
		}
	}

	if !firstTimeFlag {
		*errorMessage += "Bus terminated early\n"
	}

	/*predictions := []time.Duration{
		actualJourneyTime,
		predictionThreeDays,
		predictionWeekly,
		predictionWeeklyDiscardInterpolation,
		predictionWeeklySameRouteOnly,
		predictionWeeklyShifting,
		predictionWeeklyShiftingDiscardInterpolation,
		predictionWeeklyShiftingSameRouteOnly,
		predictionRealTimeMedian,
		predictionRealTimeLastTwo,

		totalStddevThreeDays,
		totalStddevWeekly,
		totalStddevWeeklyDiscardInterpolation,
		totalStddevWeeklySameRouteOnly,
		totalStddevWeeklyShifting,
		totalStddevWeeklyShiftingDiscardInterpolation,
		totalStddevWeeklyShiftingSameRouteOnly,
		totalStddevRealTimeMedian,
		totalStddevRealTimeLastTwo,
	}*/

	return vehicleId, result, actualJourneyTime, totalPredictions, totalStddevs
}