"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 __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 __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BasePositionsManager = void 0;
const moment_timezone_1 = __importDefault(require("@golemio/core/dist/shared/moment-timezone"));
const turf = __importStar(require("@turf/turf"));
const const_1 = require("../../../../../const");
const PositionHandlerEnum_1 = require("./PositionHandlerEnum");
const AnchorPointSegmenter_1 = require("./anchor-points/AnchorPointSegmenter");
const DateTimeUtils_1 = require("../../../../../helpers/DateTimeUtils");
const ONE_DAY_IN_SECONDS = 24 * 60 * 60;
const ONE_SECOND_IN_MILLIS = 1000;
class BasePositionsManager {
    constructor({ useEndTimestamp = false, allowCancelState = false }) {
        this.getCurrentContext = (tripPositions) => {
            const context = tripPositions.last_position_context ?? {
                atStopStreak: {
                    stop_sequence: null,
                    firstPositionTimestamp: null,
                    firstPositionDelay: null,
                },
                lastPositionLastStop: {
                    id: null,
                    sequence: null,
                    arrival_time: null,
                    arrival_delay: null,
                    departure_time: null,
                    departure_delay: null,
                },
                lastPositionDelay: null,
                lastPositionId: null,
                lastPositionLat: null,
                lastPositionLng: null,
                lastPositionOriginTimestamp: null,
                lastPositionTracking: null,
                lastPositionCanceled: null,
                lastPositionBeforeTrackDelayed: null,
                lastPositionState: null,
                lastStopSequence: null,
                lastPositionStateChange: null,
                tripId: tripPositions.id,
            };
            return context;
        };
        /**
         * Compute positions and return computed positions
         *
         * @param {ITripPositionsWithGTFS} tripPositions - Trip positions with shape anchors data
         * @returns {IProcessedPositions} - Returns computed/updated positions
         */
        this.computePositions = (tripPositions, schedule) => {
            const startTimestamp = tripPositions.start_timestamp.getTime();
            const startDayTimestamp = this.getStartDayTimestamp(startTimestamp, tripPositions.gtfsData.shapes_anchor_points[0].time_scheduled_seconds);
            const gtfsRouteType = tripPositions.gtfs_route_type;
            const context = this.getCurrentContext(tripPositions);
            const computedPositions = [];
            const endTimestamp = this.useEndTimestamp ? tripPositions.end_timestamp : null;
            return this.updatePositions({
                tripPositions,
                startTimestamp,
                startDayTimestamp,
                endTimestamp,
                context,
                computedPositions,
                gtfsRouteType,
            }, schedule);
        };
        /**
         * Decide how to process input position data
         *
         * @param {IVPTripsPositionAttributes} position - Input vehiclepositions_positions row data
         * @returns {PositionHandlerEnum} - Returns action handler enum
         */
        this.setPositionUpdateHandler = (position) => {
            if (position.state_process === const_1.StateProcessEnum.PROCESSED || position.state_process === const_1.StateProcessEnum.INVALIDATED)
                return PositionHandlerEnum_1.PositionHandlerEnum.DO_NOTHING;
            else if (this.allowCancelState && position.is_canceled)
                return PositionHandlerEnum_1.PositionHandlerEnum.CANCELED;
            else if (position.is_tracked)
                return PositionHandlerEnum_1.PositionHandlerEnum.TRACKING;
            else
                return PositionHandlerEnum_1.PositionHandlerEnum.NOT_TRACKING;
        };
        /**
         * Returns estimate of point on shape, where the trip should be with appropriate delay
         *
         * @param {IShapeAnchorPoint[]} tripShapePoints - Precalculated trip shape equidistant points with scheduled times
         * @param {Feature<Point, ICurrentPositionProperties>} currentPosition - Current position of trip
         * @param {IVPTripsLastPositionContext | null} context - Context state, holds information about previous positions
         * @param {number} startDayTimestamp - Unix timestamp of midnight before trip starts
         * @returns {IPositionToUpdate} - Position object to update
         */
        this.getEstimatedPoint = (tripGtfsData, currentPosition, context, startDayTimestamp, gtfsRouteType) => {
            const anchorPointSegmenter = new AnchorPointSegmenter_1.AnchorPointSegmenter(tripGtfsData.shapes_anchor_points, currentPosition);
            const defaultStatePosition = const_1.StatePositionEnum.OFF_TRACK;
            // Initial value
            let estimatedPoint = {
                id: currentPosition.properties.id,
                state_position: defaultStatePosition,
                state_process: const_1.StateProcessEnum.PROCESSED,
                // origin_timestamp: currentPosition.properties.origin_timestamp,
                tcp_event: currentPosition.properties.tcp_event,
                valid_to: this.validToCalculator.getDefaultValidToAttribute(currentPosition.properties.origin_timestamp),
                ...(context?.lastPositionTracking?.properties.last_stop_sequence && {
                    shape_dist_traveled: context.lastPositionTracking?.properties.shape_dist_traveled,
                    last_stop_arrival_time: context.lastPositionTracking?.properties.last_stop_arrival_time
                        ? new Date(context.lastPositionTracking?.properties.last_stop_arrival_time)
                        : undefined,
                    last_stop_departure_time: context.lastPositionTracking?.properties.last_stop_departure_time
                        ? new Date(context.lastPositionTracking?.properties.last_stop_departure_time)
                        : undefined,
                    last_stop_sequence: context.lastPositionTracking?.properties.last_stop_sequence,
                    last_stop_id: context.lastPositionTracking?.properties.last_stop_id,
                }),
            };
            let lastStopSequence = null;
            if (context?.lastPositionState === const_1.StatePositionEnum.BEFORE_TRACK) {
                lastStopSequence = 1;
            }
            else if (context?.lastPositionTracking) {
                lastStopSequence = context.lastPositionTracking.properties.last_stop_sequence;
            }
            const closestPoint = anchorPointSegmenter.getClosesPoint(lastStopSequence);
            if (!closestPoint) {
                return estimatedPoint;
            }
            return this.getClosestPoint(currentPosition, context, startDayTimestamp, closestPoint, tripGtfsData, gtfsRouteType);
        };
        /**
         * Corrects time delay at stop with dwelling time
         *
         * @param {number} timeDelay - Initial computed delay in seconds, can be negative for trip ahead
         * @param {IVPTripsLastPositionContext | null} context - Context state, holds information about previous positions
         * @param {Feature<Point, ICurrentPositionProperties>} currentPosition - Feature Point of current position
         * @param {IShapeAnchorPoint} thisClosestPoint - Closest point of shape anchors
         * @param { departureTime: number; arrivalTime: number } stopTimes - departure and arrival stop times in seconds
         * @returns {number} - Result delay in seconds, can be negative for trip ahead
         */
        this.getCorrectedTimeDelay = (timeDelay, context, currentPosition, thisStopSequence, stopTimes) => {
            // compute dwell time in stop, most common is zero
            const stopDwellTimeSeconds = stopTimes.departureTime - stopTimes.arrivalTime;
            // if dwell time is sheduled as zero, return initial computed delay
            if (stopDwellTimeSeconds <= 0) {
                return timeDelay;
            }
            // if last position was not in this same stop or there is no last position at all
            if (!context || context.atStopStreak.stop_sequence !== thisStopSequence) {
                // timeDelay >= 0 trip is DELAYED
                // we presume it will lower delay by shortening its scheduled dwell time,
                // cant go under zero of course, trip should not go ahead
                // else trip is AHeAD
                // left computed delay as it was
                return timeDelay >= 0 ? Math.max(timeDelay - stopDwellTimeSeconds, 0) : timeDelay;
            }
            // we presume that first position at same stop is real arrival time
            if (context.atStopStreak.firstPositionDelay >= 0) {
                // trip was DELAYED before
                // we presume it will lower delay by shortening its scheduled dwell time,
                // cant go under zero of course, trip should not go ahead
                return Math.max(timeDelay - stopDwellTimeSeconds, 0);
            }
            // trip was AHEAD before
            // real dwell time so far
            const realDwellTimeSeconds = Math.round((currentPosition.properties.origin_timestamp.getTime() - context.atStopStreak.firstPositionTimestamp) / 1000);
            // if real dwell is longer than scheduled, then add to negative delay time
            return context.atStopStreak.firstPositionDelay + Math.max(realDwellTimeSeconds - stopDwellTimeSeconds, 0);
        };
        /**
         * Compute UTC timestamp of start of day when trip starts
         *
         * @param {number} startTimestamp - Unix timestamp of start of the trip
         * @param {number} firstStopTimeScheduledSeconds - Number of seconds from midnight of first stop departure
         * @returns {number} - Returns unix timestamp in milliseconds.
         */
        this.getStartDayTimestamp = (startTimestamp, firstStopTimeScheduledSeconds) => {
            let startDayTimestamp = moment_timezone_1.default.utc(startTimestamp).tz("Europe/Prague").startOf("day");
            const stopTimeDayOverflow = Math.floor(firstStopTimeScheduledSeconds / ONE_DAY_IN_SECONDS);
            // if trip has 24+ stop times set real startDay to yesterday
            if (stopTimeDayOverflow > 0) {
                startDayTimestamp.subtract(1, "day");
            }
            return startDayTimestamp.valueOf();
        };
        this.useEndTimestamp = useEndTimestamp;
        this.allowCancelState = allowCancelState;
    }
    disruptAtStopStreak(position, positionToUpdate) {
        if (!positionToUpdate && !position.this_stop_sequence) {
            return true;
        }
        return !!(positionToUpdate &&
            positionToUpdate.state_position !== const_1.StatePositionEnum.MISMATCHED &&
            !positionToUpdate.this_stop_sequence);
    }
    setFirstUniqueStopSequence(positionToUpdate, context, position) {
        if (positionToUpdate
            ? positionToUpdate.this_stop_sequence &&
                context.atStopStreak.stop_sequence !== positionToUpdate.this_stop_sequence
            : position.this_stop_sequence && context.atStopStreak.stop_sequence !== position.this_stop_sequence) {
            context.atStopStreak.stop_sequence = positionToUpdate
                ? positionToUpdate.this_stop_sequence
                : position.this_stop_sequence;
            context.atStopStreak.firstPositionTimestamp = position.origin_timestamp.getTime();
            context.atStopStreak.firstPositionDelay = positionToUpdate ? positionToUpdate.delay : position.delay;
        }
    }
    defaultNonTrackingPositionToUpdate(position, firstStopTime, startDayTimestamp, setAsInvisible, lastPositionDelayed) {
        const positionToUpdate = {
            id: position.id,
            next_stop_arrival_time: DateTimeUtils_1.DateTimeUtils.getStopDateTimeForDayStart(firstStopTime.arrival_time_seconds, startDayTimestamp),
            next_stop_departure_time: DateTimeUtils_1.DateTimeUtils.getStopDateTimeForDayStart(firstStopTime.departure_time_seconds, startDayTimestamp),
            next_stop_id: firstStopTime.stop_id,
            next_stop_sequence: firstStopTime.stop_sequence,
            next_stop_name: firstStopTime.stop.stop_name,
            shape_dist_traveled: firstStopTime.shape_dist_traveled,
            state_position: setAsInvisible ? const_1.StatePositionEnum.INVISIBLE : const_1.StatePositionEnum.BEFORE_TRACK,
            state_process: const_1.StateProcessEnum.PROCESSED,
            ...(firstStopTime.stop_headsign && {
                last_stop_headsign: firstStopTime.stop_headsign,
            }),
        };
        if (positionToUpdate.state_position === const_1.StatePositionEnum.BEFORE_TRACK) {
            if (lastPositionDelayed === 0) {
                positionToUpdate.delay = 0;
            }
            else {
                positionToUpdate.delay = this.getDelayBeforeTrack(lastPositionDelayed?.delay ?? position.delay, firstStopTime.departure_time_seconds, position.origin_timestamp.getTime(), startDayTimestamp);
            }
        }
        return positionToUpdate;
    }
    /**
     * Get delay in seconds for positions before track to ensure correct delay propagation
     *
     * @param lastPositionDelay The last position delay in seconds
     * @param firstStopDepartureTime The first stop departure time in seconds. The time is measured from "noon minus 12h" of the
     * service day (effectively midnight except for days on which daylight savings time changes occur). For times occurring after
     * midnight on the service day, enter the time as a value greater than 24:00:00 in seconds. See also
     * <https://gtfs.org/schedule/reference/#:~:text=A%20phone%20number.-,Time,-%2D%20Time%20in%20the>.
     * @param positionOriginUnixTimestamp The Unix Epoch timestamp of the current position, in milliseconds
     * @param startDayUnixTimestamp The Unix Epoch timestamp of the start of the service day, in milliseconds
     */
    getDelayBeforeTrack(lastPositionDelay, firstStopDepartureTime, positionOriginUnixTimestamp, startDayUnixTimestamp) {
        if (typeof lastPositionDelay !== "number") {
            return null;
        }
        const startDayMoment = moment_timezone_1.default.utc(startDayUnixTimestamp).tz(DateTimeUtils_1.DateTimeUtils.TIMEZONE);
        startDayMoment.hours(12).minutes(0).seconds(0).milliseconds(0);
        startDayMoment.subtract(12, "hours");
        startDayMoment.add(firstStopDepartureTime, "seconds");
        const departureTimeInSeconds = Math.floor(startDayMoment.toDate().valueOf() / ONE_SECOND_IN_MILLIS);
        const positionOriginTimeInSeconds = Math.floor(positionOriginUnixTimestamp / ONE_SECOND_IN_MILLIS);
        return Math.max(lastPositionDelay, positionOriginTimeInSeconds - departureTimeInSeconds);
    }
    isFreshNewVehicleInSystem(context, position, endTimestamp) {
        const noPreviousTrackingEver = context.lastPositionTracking === null;
        const endTimestampAfterScheduled = endTimestamp === null || position.scheduled_timestamp === null || endTimestamp > position.scheduled_timestamp;
        return noPreviousTrackingEver && endTimestampAfterScheduled;
    }
    setLastPositionTracking(position, context, positionToUpdate) {
        if (position.state_position === const_1.StatePositionEnum.BEFORE_TRACK_DELAYED) {
            context.lastPositionBeforeTrackDelayed = {
                delay: positionToUpdate?.delay ?? position.delay,
                origin_timestamp: positionToUpdate?.origin_timestamp ?? position.origin_timestamp,
            };
        }
        if (positionToUpdate?.state_position === const_1.StatePositionEnum.AT_STOP ||
            positionToUpdate?.state_position === const_1.StatePositionEnum.ON_TRACK ||
            position.state_position === const_1.StatePositionEnum.AT_STOP ||
            position.state_position === const_1.StatePositionEnum.ON_TRACK) {
            context.lastPositionTracking = turf.point([position.lng, position.lat], {
                ...position,
                ...positionToUpdate,
            });
        }
    }
    detectDuplicatePosition(position, lastPositionOriginTimestamp, positionsSource) {
        return position.origin_timestamp.getTime() === lastPositionOriginTimestamp ||
            positionsSource.findIndex((positionItem) => positionItem.origin_timestamp.getTime() === position.origin_timestamp.getTime() && positionItem.is_tracked) >= 0
            ? const_1.StatePositionEnum.DUPLICATE
            : const_1.StatePositionEnum.AFTER_TRACK;
    }
}
exports.BasePositionsManager = BasePositionsManager;
BasePositionsManager.sortByOriginTimestamp = function (a, b) {
    if (!a.origin_timestamp || !b.origin_timestamp || a.origin_timestamp === b.origin_timestamp) {
        return 0;
    }
    return a.origin_timestamp < b.origin_timestamp ? -1 : 1;
};
//# sourceMappingURL=BasePositionsManager.js.map