"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
var PositionsManager_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.PositionsManager = void 0;
const DateTimeUtils_1 = require("../../../../../helpers/DateTimeUtils");
const VPContainerToken_1 = require("../../../ioc/VPContainerToken");
const tsyringe_1 = require("@golemio/core/dist/shared/tsyringe");
const turf = __importStar(require("@turf/turf"));
const const_1 = require("../../../../../const");
const RouteTypeEnums_1 = require("../../../../../helpers/RouteTypeEnums");
const BasePositionsManager_1 = require("./BasePositionsManager");
const DPPUtils_1 = __importDefault(require("./DPPUtils"));
const PositionHandlerEnum_1 = require("./PositionHandlerEnum");
const ValidToCalculator_1 = require("./ValidToCalculator");
const ComputeDelayHelper_1 = __importDefault(require("./compute-positions/ComputeDelayHelper"));
const PositionStateDelayManager = __importStar(require("./state-position/PositionStateDelayManager"));
const CoreToken_1 = require("@golemio/core/dist/helpers/ioc/CoreToken");
let PositionsManager = PositionsManager_1 = class PositionsManager extends BasePositionsManager_1.BasePositionsManager {
    constructor(logger, validToCalculator) {
        super({ useEndTimestamp: true, allowCancelState: true });
        this.logger = logger;
        this.validToCalculator = validToCalculator;
        /**
         * Takes position one by one, set proper handler for type of position, and do the process of position
         *
         * @param {IUpdatePositionsIteratorOptions} options - Initial options
         * @param {number} cb - Callback function of iterator
         * @returns {IProcessedPositions} - Returns computed/updated positions
         */
        this.updatePositions = (options, schedule) => {
            const { tripPositions, startDayTimestamp, startTimestamp, endTimestamp, context, computedPositions, gtfsRouteType } = options;
            let isBacktrackingDetected = false;
            let lastPositionStaysCanceled = false;
            const positionsSource = tripPositions.positions;
            positionsSource.sort(PositionsManager_1.sortByOriginTimestamp);
            for (const position of positionsSource) {
                let positionToUpdate = null;
                // situations
                try {
                    switch (this.setPositionUpdateHandler(position)) {
                        case PositionHandlerEnum_1.PositionHandlerEnum.TRACKING: {
                            const currentPosition = turf.point([position.lng, position.lat], {
                                id: position.id,
                                origin_time: position.origin_time,
                                origin_timestamp: position.origin_timestamp,
                                scheduled_timestamp: position.scheduled_timestamp,
                                this_stop_id: position.this_stop_id,
                                tcp_event: position.tcp_event,
                            });
                            positionToUpdate = this.getEstimatedPoint(tripPositions.gtfsData, currentPosition, context, startDayTimestamp, tripPositions.gtfs_route_type);
                            if (positionToUpdate.state_position !== const_1.StatePositionEnum.MISMATCHED) {
                                positionToUpdate = ComputeDelayHelper_1.default.updatePositionToUpdate(context, positionToUpdate, position, gtfsRouteType);
                            }
                            positionToUpdate.bearing = position.bearing ?? positionToUpdate.bearing;
                            break;
                        }
                        case PositionHandlerEnum_1.PositionHandlerEnum.NOT_TRACKING: {
                            // if there is no previous positions with tracking status, set position as before_track
                            const firstShapesAnchorPoint = tripPositions.gtfsData.shapes_anchor_points[0];
                            const invisibleByDpp = DPPUtils_1.default.isInvisible(tripPositions.agency_name_scheduled, position.origin_timestamp, startTimestamp, [position.lng, position.lat], firstShapesAnchorPoint.coordinates, schedule, tripPositions.gtfs_trip_id, context.lastPositionTracking, position.is_tracked);
                            if (this.isFreshNewVehicleInSystem(context, position, endTimestamp)) {
                                const firstStopTime = tripPositions.gtfsData.stop_times[0];
                                // if there is propagated delay we can use it for new position
                                const lastPositionDelayed = context.lastPositionBeforeTrackDelayed;
                                // if there is no delay to duplicate and DPP trip is far from start then invisible
                                const setAsInvisible = !lastPositionDelayed && invisibleByDpp;
                                positionToUpdate = this.defaultNonTrackingPositionToUpdate(position, firstStopTime, startDayTimestamp, setAsInvisible, lastPositionDelayed);
                            }
                            else {
                                // if there is tracking 2 position with same origin_timestamp then this position is duplicate
                                const statePosition = this.detectDuplicatePosition(position, context.lastPositionOriginTimestamp, positionsSource);
                                const lastShapesAnchorPoint = tripPositions.gtfsData.shapes_anchor_points[tripPositions.gtfsData.shapes_anchor_points.length - 1];
                                const lastStopTime = tripPositions.gtfsData.stop_times[tripPositions.gtfsData.stop_times.length - 1];
                                // set as invisible if there are some AFTER_TRACK before
                                const setAsInvisible = invisibleByDpp && statePosition === const_1.StatePositionEnum.AFTER_TRACK;
                                const lastShapesAnchorPointTimeScheduled = DateTimeUtils_1.DateTimeUtils.getStopDateTimeForDayStart(lastShapesAnchorPoint.time_scheduled_seconds, startDayTimestamp);
                                positionToUpdate = {
                                    id: position.id,
                                    last_stop_arrival_time: lastShapesAnchorPointTimeScheduled,
                                    last_stop_departure_time: lastShapesAnchorPointTimeScheduled,
                                    last_stop_id: lastStopTime.stop_id,
                                    last_stop_sequence: lastShapesAnchorPoint.last_stop_sequence,
                                    shape_dist_traveled: lastShapesAnchorPoint.shape_dist_traveled,
                                    state_position: setAsInvisible ? const_1.StatePositionEnum.INVISIBLE : statePosition,
                                    state_process: const_1.StateProcessEnum.PROCESSED,
                                    delay: position.delay ?? null,
                                    ...(lastStopTime.stop_headsign && {
                                        last_stop_headsign: lastStopTime.stop_headsign,
                                    }),
                                };
                                // if state was changed in last 30 seconds, then it is AFTER_TRACK_DELAYED
                                // with delay computation (copied from delay at stop)
                                if (positionToUpdate.state_position === const_1.StatePositionEnum.AFTER_TRACK &&
                                    PositionStateDelayManager.PositionStateDelayManager.shouldPropagateAfterTrackDelay(context)) {
                                    positionToUpdate.state_position = const_1.StatePositionEnum.AFTER_TRACK_DELAYED;
                                }
                                positionToUpdate = ComputeDelayHelper_1.default.updatePositionToUpdate(context, positionToUpdate, position, gtfsRouteType);
                                if (positionToUpdate.state_position === const_1.StatePositionEnum.AFTER_TRACK_DELAYED) {
                                    positionToUpdate.delay = positionToUpdate.delay_stop_arrival ?? null;
                                }
                            }
                            break;
                        }
                        case PositionHandlerEnum_1.PositionHandlerEnum.CANCELED: {
                            positionToUpdate = {
                                id: position.id,
                                lat: position.lat ?? context.lastPositionLat ?? null,
                                lng: position.lng ?? context.lastPositionLng ?? null,
                                state_position: const_1.StatePositionEnum.CANCELED,
                                state_process: const_1.StateProcessEnum.PROCESSED,
                            };
                            positionToUpdate.lat = positionToUpdate.lat || 0;
                            positionToUpdate.lng = positionToUpdate.lng || 0;
                            break;
                        }
                        case PositionHandlerEnum_1.PositionHandlerEnum.DO_NOTHING: {
                            break;
                        }
                        default: {
                            break;
                        }
                    }
                    const detection = this.detectBacktracking(position, context, positionToUpdate, gtfsRouteType, tripPositions, startTimestamp, isBacktrackingDetected);
                    positionToUpdate = detection.positionToUpdate;
                    isBacktrackingDetected = detection.isBacktrackingDetected;
                    computedPositions.push(positionToUpdate);
                    // sets last known BEFORE_TRACK_DELAYED position
                    // sets last position tracking (only for at_stop and on_track)
                    this.setLastPositionTracking(position, context, positionToUpdate);
                    // set new first position at stop streak if this stop seqence is set and it is not same as before
                    this.setFirstUniqueStopSequence(positionToUpdate, context, position);
                    // IF currently valid updated position / position was processed before
                    // and it is NOT AT_STOP
                    // then disrupt atStopStreak
                    if (this.disruptAtStopStreak(position, positionToUpdate)) {
                        context.atStopStreak.stop_sequence = null;
                    }
                    // based on the fact that current position has some tcp_event
                    // allows the canceled boolean to be changed, otherwise use the prev.value
                    lastPositionStaysCanceled = this.hasTcpEventOrCancel(position) ?? lastPositionStaysCanceled;
                    // duplicated or mismatched position should not be considered at all
                    if (positionToUpdate &&
                        positionToUpdate.state_position !== const_1.StatePositionEnum.DUPLICATE &&
                        positionToUpdate.state_position !== const_1.StatePositionEnum.INVISIBLE &&
                        positionToUpdate.state_position !== const_1.StatePositionEnum.MISMATCHED) {
                        context.lastPositionId = position.id;
                        context.lastPositionLat = position.lat;
                        context.lastPositionLng = position.lng;
                        context.lastPositionCanceled = lastPositionStaysCanceled || position.is_canceled;
                        context.lastPositionOriginTimestamp = position.origin_timestamp.getTime();
                        context.lastStopSequence = positionToUpdate.last_stop_sequence ?? context.lastStopSequence ?? null;
                        if (context.lastPositionState !== positionToUpdate.state_position) {
                            context.lastPositionState = positionToUpdate.state_position;
                            context.lastPositionStateChange = new Date().toISOString();
                        }
                        ComputeDelayHelper_1.default.updateContext(context, positionToUpdate, position, gtfsRouteType);
                    }
                }
                catch (error) {
                    this.logger.error({
                        error,
                        context,
                        positionId: position.id,
                        lat: position.lat,
                        lng: position.lng,
                    }, `${this.constructor.name}: Error while processing position`);
                    continue;
                }
            }
            return {
                context,
                positions: computedPositions,
                isBacktrackingDetected: isBacktrackingDetected,
            };
        };
        /**
         * Picks only one closest point for multiple possible points based on delay of last position and stop times
         *
         * @param {Feature<Point, ICurrentPositionProperties>} currentPosition - Feature Point of current position
         * @param {IVPTripsLastPositionContext | null} context - Context state, holds information about previous positions
         * @param {number} startDayTimestamp - Unix timestamp of start of the day
         * @param {IShapeAnchorPoint[]} closestPts - All closest points of possible segments
         * @param {IComputationTrip} tripGtfsData - GTFS data and all set of known positions
         * @returns {IPositionToUpdate} - Result point as position to update in DB
         */
        this.getClosestPoint = (currentPosition, context, startDayTimestamp, thisClosestPoint, tripGtfsData, gtfsRouteType) => {
            // want to find minimum difference of our prediction, where the bus should be
            const tripShapePoints = tripGtfsData.shapes_anchor_points;
            const tripStopTimes = tripGtfsData.stop_times;
            const firstTripShapePoint = tripShapePoints[0];
            const lastTripShapePoint = tripShapePoints[tripShapePoints.length - 1];
            const closestShapeDistTraveled = thisClosestPoint.shape_dist_traveled;
            const closestBearing = thisClosestPoint.bearing;
            const timeScheduledTimestamp = DateTimeUtils_1.DateTimeUtils.getStopDateTimeForDayStart(
            // take stop arrival time if is point at stop
            thisClosestPoint.this_stop_sequence
                ? tripStopTimes[thisClosestPoint.this_stop_sequence - 1].arrival_time_seconds
                : thisClosestPoint.time_scheduled_seconds, startDayTimestamp);
            let timeDelayInSeconds = Math.round((currentPosition.properties.origin_timestamp.getTime() - timeScheduledTimestamp.getTime()) / 1000);
            const isBusOrTram = gtfsRouteType === RouteTypeEnums_1.GTFSRouteTypeEnum.BUS ||
                gtfsRouteType === RouteTypeEnums_1.GTFSRouteTypeEnum.TROLLEYBUS ||
                gtfsRouteType === RouteTypeEnums_1.GTFSRouteTypeEnum.TRAM;
            // push trip into nearest at_stop shape anchor point if trip "P" event  (TCP tram or bus)
            if (isBusOrTram &&
                currentPosition.properties.tcp_event === const_1.TCPEventEnum.ARRIVAL_ANNOUNCED &&
                thisClosestPoint.this_stop_sequence === null) {
                if (thisClosestPoint.last_stop_sequence === 1 && !context?.lastPositionTracking) {
                    thisClosestPoint = firstTripShapePoint;
                }
                else {
                    let movingShapePointIndex = thisClosestPoint.index;
                    while (thisClosestPoint.this_stop_sequence === null && movingShapePointIndex < lastTripShapePoint.index) {
                        movingShapePointIndex++;
                        thisClosestPoint = tripShapePoints[movingShapePointIndex];
                    }
                }
            }
            // lets correct delay if it is at stop
            if (thisClosestPoint.this_stop_sequence) {
                const thisStopSequence = thisClosestPoint.this_stop_sequence;
                timeDelayInSeconds = this.getCorrectedTimeDelay(timeDelayInSeconds, context, currentPosition, thisStopSequence, {
                    departureTime: tripStopTimes[thisStopSequence - 1].departure_time_seconds,
                    arrivalTime: tripStopTimes[thisStopSequence - 1].arrival_time_seconds,
                });
                // delay can not be negative if the vehicle is at the first stop
                if (thisStopSequence === firstTripShapePoint.this_stop_sequence) {
                    timeDelayInSeconds = Math.max(timeDelayInSeconds, 0);
                }
            }
            let { statePosition, thisStopSequence, lastStopSequence, nextStopSequence } = this.getStateAndStopSequences(gtfsRouteType, thisClosestPoint, currentPosition.properties, context);
            // test if TCP trip with DEPARTURED event should not be at stop (not applied for last stop)
            if (currentPosition.properties.tcp_event === const_1.TCPEventEnum.DEPARTURED &&
                thisStopSequence !== lastTripShapePoint.this_stop_sequence) {
                statePosition = const_1.StatePositionEnum.ON_TRACK;
            }
            if (isBusOrTram && this.isAfterTrack(lastStopSequence, lastTripShapePoint)) {
                statePosition = const_1.StatePositionEnum.AFTER_TRACK;
                if (PositionStateDelayManager.PositionStateDelayManager.shouldPropagateAfterTrackDelay(context)) {
                    statePosition = const_1.StatePositionEnum.AFTER_TRACK_DELAYED;
                }
                else {
                    // normal after track positions (without propagation) should always have delay null
                    timeDelayInSeconds = null;
                }
                thisStopSequence = lastTripShapePoint.this_stop_sequence;
                lastStopSequence = lastTripShapePoint.last_stop_sequence;
                nextStopSequence = null;
            }
            const isTracked = statePosition === const_1.StatePositionEnum.AT_STOP || statePosition === const_1.StatePositionEnum.ON_TRACK;
            // save it for result
            const estimatedPoint = {
                id: currentPosition.properties.id,
                // always use shape_dist_traveled and bearing from the closest anchor point
                // even if the vehicle announces arrival (TCP event 'P') outside of the stop
                bearing: closestBearing,
                shape_dist_traveled: closestShapeDistTraveled,
                next_stop_id: nextStopSequence ? tripStopTimes[nextStopSequence - 1].stop_id : null,
                last_stop_id: tripStopTimes[lastStopSequence - 1].stop_id,
                next_stop_name: nextStopSequence ? tripStopTimes[nextStopSequence - 1].stop.stop_name : null,
                last_stop_name: tripStopTimes[lastStopSequence - 1].stop.stop_name,
                next_stop_sequence: nextStopSequence,
                last_stop_sequence: lastStopSequence,
                next_stop_arrival_time: nextStopSequence
                    ? DateTimeUtils_1.DateTimeUtils.getStopDateTimeForDayStart(tripStopTimes[nextStopSequence - 1].arrival_time_seconds, startDayTimestamp)
                    : null,
                last_stop_arrival_time: DateTimeUtils_1.DateTimeUtils.getStopDateTimeForDayStart(tripStopTimes[lastStopSequence - 1].arrival_time_seconds, startDayTimestamp),
                next_stop_departure_time: nextStopSequence
                    ? DateTimeUtils_1.DateTimeUtils.getStopDateTimeForDayStart(tripStopTimes[nextStopSequence - 1].departure_time_seconds, startDayTimestamp)
                    : null,
                last_stop_departure_time: DateTimeUtils_1.DateTimeUtils.getStopDateTimeForDayStart(tripStopTimes[lastStopSequence - 1].departure_time_seconds, startDayTimestamp),
                delay: timeDelayInSeconds,
                this_stop_id: (statePosition === const_1.StatePositionEnum.AT_STOP &&
                    thisStopSequence &&
                    tripStopTimes[thisStopSequence - 1].stop_id) ||
                    undefined,
                this_stop_name: (statePosition === const_1.StatePositionEnum.AT_STOP &&
                    thisStopSequence &&
                    tripStopTimes[thisStopSequence - 1].stop.stop_name) ||
                    undefined,
                this_stop_sequence: (statePosition === const_1.StatePositionEnum.AT_STOP && thisStopSequence) || undefined,
                last_stop_headsign: tripStopTimes[lastStopSequence - 1].stop_headsign || undefined,
                state_position: statePosition,
                state_process: const_1.StateProcessEnum.PROCESSED,
                valid_to: this.validToCalculator.getDefaultValidToAttribute(currentPosition.properties.origin_timestamp),
                is_tracked: isTracked,
            };
            return estimatedPoint;
        };
    }
    detectBacktracking(position, context, positionToUpdate, gtfsRouteType, tripPositions, startTimestamp, isBacktrackingDetected) {
        if (positionToUpdate) {
            positionToUpdate.is_tracked ??= position.is_tracked;
            positionToUpdate.valid_to = this.validToCalculator.getValidToAttribute(positionToUpdate, position, gtfsRouteType, tripPositions.gtfsData, startTimestamp);
            // backtracking detection so we can later invalidate old positions
            if (!isBacktrackingDetected && positionToUpdate.last_stop_sequence && context.lastStopSequence) {
                isBacktrackingDetected = positionToUpdate.last_stop_sequence < context.lastStopSequence;
            }
            return { positionToUpdate, isBacktrackingDetected };
        }
        else {
            return { positionToUpdate: null, isBacktrackingDetected };
        }
    }
    getStateAndStopSequences(gtfsRouteType, thisClosestPoint, positionProperties, context) {
        let statePosition = thisClosestPoint.this_stop_sequence ? const_1.StatePositionEnum.AT_STOP : const_1.StatePositionEnum.ON_TRACK;
        let thisStopSequence = thisClosestPoint.this_stop_sequence;
        let lastStopSequence = thisClosestPoint.last_stop_sequence;
        let nextStopSequence = thisClosestPoint.next_stop_sequence;
        // if current positions's TCP event is 'T' and previous position is tracked
        if (gtfsRouteType !== RouteTypeEnums_1.GTFSRouteTypeEnum.METRO &&
            positionProperties.tcp_event === const_1.TCPEventEnum.TIME &&
            context?.lastPositionTracking) {
            const previousPositionTcpEvent = context.lastPositionTracking.properties.tcp_event;
            const previousLastStopSequence = context.lastPositionTracking.properties.last_stop_sequence ?? 0;
            // if previous position was 'P' and current last stop sequence is lower than the previous last stop sequence
            // or if previous position was 'O', current position is at stop
            //    and current last stop sequence is lower than the previous last stop sequence
            // then state is MISMATCHED
            // else if previous position was 'O', current position is at stop
            //    and current last stop sequence is equal to the previous last stop sequence
            // then state is ON_TRACK
            if (previousPositionTcpEvent === const_1.TCPEventEnum.ARRIVAL_ANNOUNCED && lastStopSequence < previousLastStopSequence) {
                statePosition = const_1.StatePositionEnum.MISMATCHED;
            }
            else if (previousPositionTcpEvent === const_1.TCPEventEnum.DEPARTURED && thisStopSequence) {
                if (thisStopSequence === previousLastStopSequence) {
                    statePosition = const_1.StatePositionEnum.ON_TRACK;
                }
                else if (thisStopSequence < previousLastStopSequence) {
                    statePosition = const_1.StatePositionEnum.MISMATCHED;
                }
            }
            if (statePosition === const_1.StatePositionEnum.MISMATCHED || statePosition === const_1.StatePositionEnum.ON_TRACK) {
                thisStopSequence = null;
            }
        }
        return {
            statePosition,
            thisStopSequence,
            lastStopSequence,
            nextStopSequence,
        };
    }
    hasTcpEventOrCancel(position) {
        const tcpEvent = position.tcp_event;
        if (tcpEvent === const_1.TCPEventEnum.TERMINATED) {
            return true;
        }
        else if (tcpEvent === const_1.TCPEventEnum.ARRIVAL_ANNOUNCED || tcpEvent === const_1.TCPEventEnum.DEPARTURED) {
            return false;
        }
        else if (tcpEvent === const_1.RegionalBusEventEnum.DEPARTURED) {
            return false;
        }
        return null;
    }
    isAfterTrack(lastStopSequence, lastTripShapePoint) {
        // Trip is terminated OR the vehicle is close to/in the terminus
        return lastStopSequence >= lastTripShapePoint.last_stop_sequence;
    }
};
exports.PositionsManager = PositionsManager;
exports.PositionsManager = PositionsManager = PositionsManager_1 = __decorate([
    (0, tsyringe_1.injectable)(),
    __param(0, (0, tsyringe_1.inject)(CoreToken_1.CoreToken.Logger)),
    __param(1, (0, tsyringe_1.inject)(VPContainerToken_1.VPContainerToken.ValidToCalculator)),
    __metadata("design:paramtypes", [Object, ValidToCalculator_1.ValidToCalculator])
], PositionsManager);
//# sourceMappingURL=PositionsManager.js.map