"use strict";
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 __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 MpvMessageTransformation_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.MpvMessageTransformation = void 0;
const RouteTypeEnums_1 = require("../../../../../helpers/RouteTypeEnums");
const vehicle_positions_1 = require("../../../../../schema-definitions/vehicle-positions");
const ProviderSourceTypeEnum_1 = require("../../../../../schema-definitions/vehicle-positions/models/enums/ProviderSourceTypeEnum");
const CoreToken_1 = require("@golemio/core/dist/helpers/ioc/CoreToken");
const AbstractTransformation_1 = require("@golemio/core/dist/helpers/transformation/AbstractTransformation");
const moment_timezone_1 = __importDefault(require("@golemio/core/dist/shared/moment-timezone"));
const tsyringe_1 = require("@golemio/core/dist/shared/tsyringe");
const const_1 = require("../../../../../const");
const DPPUtils_1 = __importDefault(require("../helpers/DPPUtils"));
const MpvStopParser_1 = require("../helpers/mpv-net/MpvStopParser");
var PositionTrackingEnum;
(function (PositionTrackingEnum) {
    PositionTrackingEnum["NOT_TRACKING"] = "0";
    PositionTrackingEnum["TRACKING"] = "2";
})(PositionTrackingEnum || (PositionTrackingEnum = {}));
let MpvMessageTransformation = MpvMessageTransformation_1 = class MpvMessageTransformation extends AbstractTransformation_1.AbstractTransformation {
    constructor(config) {
        super();
        this.skipAgencyNames = [];
        this.trainBeforeTrackMinutesInterval = [-5, 20];
        this.transform = (data) => {
            if (!Array.isArray(data))
                data = [data];
            const transformed = this.transformArray(data).filter((elem) => elem !== null);
            const trips = new Map();
            const stops = new Map();
            const tripsWithCisStops = new Map();
            const positions = [];
            for (const elemRes of transformed) {
                if (elemRes.position) {
                    positions.push(elemRes.position);
                }
                if (elemRes.trip) {
                    trips.set(elemRes.trip.id, elemRes.trip);
                    if (elemRes.tripToCis) {
                        tripsWithCisStops.set(elemRes.trip.id, elemRes.tripToCis);
                    }
                }
                for (const [key, value] of elemRes.stops) {
                    stops.set(key, value);
                }
            }
            return {
                positions,
                trips: Array.from(trips.values()),
                stops: Array.from(stops.values()),
                tripsWithCisStops,
            };
        };
        this.transformInternal = (element) => {
            const attributes = element.$;
            if (this.isBlacklistedAgency(attributes.dopr)) {
                return null;
            }
            if (this.isTrainBeforeTrackWithPlatforms(attributes, element.zast)) {
                const { startTimestamp } = this.deduceTimestamps(element.zast[0], null);
                const id = this.determinePrimaryKey(attributes, startTimestamp.utc().format());
                const stops = this.extractStopsForTrain(id, element.zast);
                const firstStop = this.getFirstStopCisNumber(element.zast);
                return {
                    position: null,
                    stops,
                    trip: this.determineTrip(attributes, parseInt(attributes.spoj, 10), id, startTimestamp, element.zast[0]),
                    tripToCis: firstStop,
                };
            }
            const hasInvalidAttributes = this.determineValidity(attributes);
            const cisTripNumber = parseInt(attributes.spoj, 10);
            // wrong attributes combinations OR wrong "spoj" number... no chance to continue either
            if (hasInvalidAttributes || isNaN(cisTripNumber)) {
                return null;
            }
            attributes.lin = attributes.lin || "none";
            attributes.alias = attributes.alias || "none";
            attributes.cpoz = attributes.cpoz || MpvMessageTransformation_1.currentTimeInPragueFormatted();
            const stops = element.zast;
            const { startTimestamp, positionTimestamp } = this.deduceTimestamps(stops[0], attributes.cpoz);
            // filter out position with future time (set to be far than 5 minutes from now)
            if ((0, moment_timezone_1.default)(positionTimestamp).isAfter((0, moment_timezone_1.default)().add(5, "minutes"))) {
                return null;
            }
            const primaryKey = this.determinePrimaryKey(attributes, startTimestamp.utc().format());
            /*
                MPV route types:
    
                Metro:                           1
                Tramvaj:                         2
                Bus městský:                     3
                Bus regionální:                  4
                Bus noční:                       5
                Noční tramvaj:                   6
                Náhradní autobusová doprava:     7
                Lanovka:                         8
                Školní:                          9
                Invalidní:                      10
                Smluvní:                        11
                Loď:                            12
                Vlak:                           13
                Náhradní doprava za vlak:       14
                Náhradní tramvajová doprava:    15
                Bus noční regionální:           16
                Ostatní:                        17
                Trolejbus:                      18
            */
            const res = {
                position: this.determinePosition(attributes, primaryKey, positionTimestamp, null),
                stops: new Map(),
                trip: this.determineTrip(attributes, cisTripNumber, primaryKey, startTimestamp, element.zast[0]),
                tripToCis: null,
            };
            for (let i = 0; i < stops.length; i++) {
                const stop = stops[i];
                if (!stop.$.zast) {
                    continue;
                }
                const delayDepartureType = stop.$.zpoz_typ_odj;
                const cisStopSequence = i + 1;
                if (res.position.cis_last_stop_id === parseInt(stop.$.zast, 10) &&
                    ((delayDepartureType && parseInt(delayDepartureType, 10) === 3) ||
                        (stop.$.zpoz_typ_prij && parseInt(stop.$.zpoz_typ_prij, 10) === 3))) {
                    res.position.cis_last_stop_sequence = cisStopSequence;
                }
                if (this.isCisPlatformEligible(res.trip)) {
                    res.stops.set(`${primaryKey}_${stop.$.zast}`, {
                        rt_trip_id: primaryKey,
                        cis_stop_group_id: parseInt(stop.$.zast, 10),
                        cis_stop_platform_code: MpvStopParser_1.MpvStopParser.parsePlatformCode(stop),
                    });
                }
            }
            return res;
        };
        /**
         * Returns -1 if start hour is 12-23 and now is 0-12, 1 if start hour is 18-23 and now 0-6, else 0
         *
         * @param {Moment} now - Moment of position
         * @param {Moment} start - Moment of start of trip
         * @returns {number}
         */
        this.checkMidnight = (now, start) => {
            // i.e. 0 - 22 = -22
            // -22 <= -12
            // trip starting in previous day, but never starts before 12:00
            if (now.hour() - start.hour() <= -(24 - 12)) {
                // "backwards" 12 hours
                return -1;
            }
            // i.e. 23 - 1 = +22
            // +22 >= +18
            // trip starting next day, sending positions early, not eariler than 6 hours before start
            else if (now.hour() - start.hour() >= 24 - 6) {
                // "forwards" 6 hours
                return 1;
            }
            return 0; // same day
        };
        this.name = vehicle_positions_1.VehiclePositions.name;
        this.skipAgencyNames = config
            .getValue("module.pid.vehicle-positions.mpvIntegration.skipAgencyNames", DPPUtils_1.default.DPP_AGENCY_NAME)
            .split(",");
        this.trainBeforeTrackMinutesInterval = config
            .getValue("module.pid.vehicle-positions.mpvIntegration.trainBeforeTrackMinutesInterval", "-5,35")
            .split(",")
            .map((s) => parseInt(s, 10));
    }
    /**
     * Deduce UTC timestamp of start of trip and UTC timestamp of current position
     *
     * @param {any} firstStopXML - parsed object of first stop
     * @param {string} positionTimestampXML - XML attribute of local timestamp of position (HH:mm:ss)
     * @returns {{ startTimestamp: Moment; positionTimestamp: Moment }}
     */
    deduceTimestamps(firstStopXML, positionTimestampXML) {
        const now = moment_timezone_1.default.tz("Europe/Prague");
        let isOverMidnight = 0;
        // creating startDate and timestamp from zast[0].prij and cpoz
        const startTimestamp = now.clone();
        let startDatePlain = firstStopXML.$.prij || firstStopXML.$.odj;
        startDatePlain = startDatePlain.split(":");
        // eslint-disable-next-line prettier/prettier
        startTimestamp.hour(parseInt(startDatePlain[0], 10)).minute(parseInt(startDatePlain[1], 10)).second(0).millisecond(0);
        // midnight checking
        isOverMidnight = this.checkMidnight(now, startTimestamp); // returns -1, 1 or 0
        startTimestamp.add(isOverMidnight, "d");
        if (positionTimestampXML == null) {
            return { startTimestamp, positionTimestamp: now.clone() };
        }
        const positionTimestamp = now.clone();
        const timestampPlain = positionTimestampXML.split(":");
        positionTimestamp
            .hour(parseInt(timestampPlain[0], 10))
            .minute(parseInt(timestampPlain[1], 10))
            .second(parseInt(timestampPlain[2], 10))
            .millisecond(0);
        // midnight checking
        isOverMidnight = this.checkMidnight(now, positionTimestamp); // returns -1, 1 or 0
        positionTimestamp.add(isOverMidnight, "d");
        return {
            startTimestamp,
            positionTimestamp,
        };
    }
    static currentTimeInPrague() {
        return moment_timezone_1.default.tz("Europe/Prague");
    }
    static currentTimeInPragueFormatted(now) {
        return (now ?? moment_timezone_1.default.tz("Europe/Prague")).format(MpvMessageTransformation_1.ORIGIN_TIME_FORMAT);
    }
    isCisPlatformEligible(trip) {
        if (trip?.vehicle_type_id == null) {
            return false;
        }
        return MpvMessageTransformation_1.CIS_PLATFORM_ELIGIBLE.includes(trip.vehicle_type_id);
    }
    isBlacklistedAgency(agency) {
        return agency && this.skipAgencyNames.includes(agency);
    }
    isTrainBeforeTrackWithPlatforms(attrs, zast) {
        if (attrs.lat || attrs.lng || attrs.zrus || attrs.cpoz || isNaN(parseInt(attrs.spoj, 10)) || !Array.isArray(zast)) {
            // Should the message has any of these, we skip it here (not processing in this specific flow):
            // - has lat/lng OR
            // - has information about being cancelled (zrus) OR
            // - has time of position (cpoz) OR
            // - has no information about what train-identifier it is (spoj)
            // - has not list of stops (zast)
            return false;
        }
        // further code accepts only unknown or train transport
        const isValidRouteType = this.isCisPlatformEligible({ vehicle_type_id: parseInt(attrs.t || "0") });
        // and accepts only <zast> XML with some "stan" (platform code) information
        const hasStopWithPlatform = zast.some((z) => z.$.stan?.length);
        // all stops have to be in CZ cis number system
        const wrongStopFromNonCzCis = zast.some((z) => {
            return Number(z.$.zast) < 5400000 || Number(z.$.zast) >= 5500000;
        });
        if (!isValidRouteType || !hasStopWithPlatform || wrongStopFromNonCzCis) {
            return false;
        }
        const { startTimestamp } = this.deduceTimestamps(zast[0], null);
        const departureInterval = [
            // allow planned departure X minutes in past
            (0, moment_timezone_1.default)().subtract(Math.abs(this.trainBeforeTrackMinutesInterval[0]), "minutes"),
            // allow up to Y minutes into future for valid trains
            (0, moment_timezone_1.default)().add(this.trainBeforeTrackMinutesInterval[1], "minutes"),
        ];
        if (startTimestamp.isBetween(departureInterval[0], departureInterval[1])) {
            // Seems the train has not yet departed onto the trip, probably good to make it into a position
            // first stop has departure time inside interval of X minutes back till now+Y minutes <now-Xminutes ... now+Ymin>
            return true;
        }
        // For trains that are already on the trip = due to arriving from different / far away public
        // transport network, and we don't receive position(s) yet, AND we won't be creating fake positions,
        // because the vehicle is moving in reality, but MPVNet is not sending us the messages with lat/lng of it yet
        return false;
    }
    determinePrimaryKey(attributes, prefix) {
        // primary key for t = 0 (vehicle type is train) -> start_timestamp, cis_number
        // primary key for t != 0 -> start_timestamp, cis_id, cis_short_name, cis_number
        return attributes.t === "0"
            ? `${prefix}_${attributes.spoj}`
            : `${prefix}_${attributes.lin}_${attributes.alias}_${attributes.spoj}`;
    }
    determineValidity(attributes) {
        // Trips with null cpoz (origin time) and falsy or "false" zrus (cancellation) attributes are excluded
        let hasInvalidAttributes = !attributes.cpoz && (!attributes.zrus || attributes.zrus === "false");
        // Trips without the CIS trip number are excluded
        hasInvalidAttributes ||= typeof attributes.spoj === "undefined" || attributes.spoj === "";
        // Canceled trips with no lat or lng are accepted because we try to later get the coords from the context if possible
        hasInvalidAttributes ||= attributes.zrus !== "true" && (!attributes.lat || !attributes.lng);
        return hasInvalidAttributes;
    }
    /**
     * Fix source negative bearing value due to overflow by adding 256
     *
     * @param {number} bearing
     * @returns {number}
     */
    fixSourceNegativeBearing(bearing) {
        return bearing < 0 ? bearing + 256 : bearing;
    }
    getFirstStopCisNumber(stopsXML) {
        return stopsXML[0]?.$.zast ? parseInt(stopsXML[0].$.zast, 10) : null;
    }
    extractStopsForTrain(id, stopsXML) {
        const stops = new Map();
        for (const stop of stopsXML) {
            if (!stop.$.zast || !stop.$.stan) {
                continue;
            }
            stops.set(`${id}_${stop.$.zast}`, {
                rt_trip_id: id,
                cis_stop_group_id: parseInt(stop.$.zast, 10),
                cis_stop_platform_code: MpvStopParser_1.MpvStopParser.parsePlatformCode(stop),
            });
        }
        return stops;
    }
    positionWithCisStopCoords(id, lat, lng, cpoz, positionTs, validToTs) {
        return this.determinePosition({ lat, lng, cpoz }, id, positionTs, validToTs);
    }
    determinePosition(attributes, id, positionTimestamp, validToTimestamp) {
        const cisLastStopId = attributes.zast;
        const speed = attributes.rychl;
        const bearing = attributes.azimut;
        return {
            asw_last_stop_id: null,
            bearing: bearing ? this.fixSourceNegativeBearing(parseInt(bearing, 10)) : null,
            cis_last_stop_id: cisLastStopId ? parseInt(cisLastStopId, 10) : null,
            cis_last_stop_sequence: null,
            delay_stop_arrival: attributes.zpoz_prij ? parseInt(attributes.zpoz_prij, 10) : null,
            delay_stop_departure: attributes.zpoz_odj ? parseInt(attributes.zpoz_odj, 10) : null,
            is_canceled: attributes.zrus === "true",
            lat: attributes.lat ? parseFloat(attributes.lat) : null,
            lng: attributes.lng ? parseFloat(attributes.lng) : null,
            origin_time: attributes.cpoz || positionTimestamp.format(MpvMessageTransformation_1.ORIGIN_TIME_FORMAT),
            origin_timestamp: positionTimestamp.toDate(),
            speed: speed ? parseInt(speed, 10) : null,
            state_position: const_1.StatePositionEnum.UNKNOWN,
            state_process: const_1.StateProcessEnum.INPUT,
            is_tracked: attributes.sled === PositionTrackingEnum.TRACKING && !!attributes.lat && !!attributes.lng,
            trips_id: id,
            valid_to: validToTimestamp ? new Date(validToTimestamp) : null,
        };
    }
    determineTrip(attributes, cisTripNumber, id, start, zastXML) {
        if (isNaN(cisTripNumber))
            return null;
        return {
            agency_name_real: attributes.doprSkut ? attributes.doprSkut : null,
            agency_name_scheduled: attributes.dopr ? attributes.dopr : null,
            cis_line_id: attributes.lin || "none",
            cis_line_short_name: attributes.alias || "none",
            cis_trip_number: cisTripNumber,
            id,
            is_canceled: attributes.zrus === "true",
            origin_route_name: attributes.kmenl ?? null,
            run_number: attributes.po !== undefined ? parseInt(attributes.po, 10) : null,
            start_asw_stop_id: zastXML.$.asw ?? null,
            start_cis_stop_id: zastXML.$.zast ? parseInt(zastXML.$.zast, 10) : null,
            start_cis_stop_platform_code: MpvStopParser_1.MpvStopParser.parsePlatformCode(zastXML) ?? null,
            start_time: start.tz("Europe/Prague").format("HH:mm"),
            start_timestamp: start.toDate(),
            vehicle_registration_number: attributes.vuzevc ? parseInt(attributes.vuzevc, 10) : null,
            vehicle_type_id: attributes.t ? parseInt(attributes.t, 10) : null,
            wheelchair_accessible: attributes.np === "true",
            provider_source_type: ProviderSourceTypeEnum_1.ProviderSourceTypeEnum.Http,
        };
    }
};
exports.MpvMessageTransformation = MpvMessageTransformation;
MpvMessageTransformation.ORIGIN_TIME_FORMAT = "HH:mm:ss";
MpvMessageTransformation.CIS_PLATFORM_ELIGIBLE = [RouteTypeEnums_1.MPVRouteTypesEnum.UNKNOWN, RouteTypeEnums_1.MPVRouteTypesEnum.TRAIN];
exports.MpvMessageTransformation = MpvMessageTransformation = MpvMessageTransformation_1 = __decorate([
    (0, tsyringe_1.injectable)(),
    __param(0, (0, tsyringe_1.inject)(CoreToken_1.CoreToken.SimpleConfig)),
    __metadata("design:paramtypes", [Object])
], MpvMessageTransformation);
//# sourceMappingURL=MpvMessageTransformation.js.map