// COPYRIGHT 2014 by Péter Gulyás
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this script.  If not, see <http://www.gnu.org/licenses/>.
// 
// 
// This script implements the Hungarian national EVM (EÉVB) type
// train control system functionality. It runs in OpenRails.
// Usage: Copy the file EVM.cs into folder /Trainset/common.script/
//        Copy the file EVM.ini into folder /Trainset/<Locomotive...>/Script/
//        Edit the locomotive's .eng file, by adding the following entries into Engine() section:
//
//        ORTSTrainControlSystem ( "..\\..\\common.script\\EVM.cs" )
//        ORTSTrainControlSystemParameters ( EVM.ini )

using System;
using System.Diagnostics;
using ORTS.Common;
using ORTS.Scripting.Api;

namespace ORTS.Scripting.Script
{
    public class EVM : TrainControlSystem
    {
        Version Version = new Version("2015.03.03");
        
        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
        // Remarks:
        // if (ETCS_STM_HU == true)  { EVM160 = false; EVM120 = true; HasPressureSwitch = true; }
        // if (EVM160 == true)  { EVM120 = true; HasPressureSwitch = true; }
        // if (EVM120 == true)  { AlarmIsDistanceBased = true; }
        // if (EVM160 == false) { HasDecelerationSwitch = false }
        // if (EVM120 == false) { HasPressureSwitch = false; }
        // ETCS_STM_HU is the national Specific Transmission Module for ETCS, implementing EVM functionality.
        // For old four-unit EÉVB equipment set ETCS_STM_HU=false, EVM160=false and EVM120=false
        // In locomotives M62 and V41 the equipment is time based: AlarmIsDistanceBased=false
        // Some EVM-120 equipments have no pressure switch: HasPressureSwitch=false
        // At issue time only Flirt and Talent EMUs have a deceleration switch: HasDecelerationSwitch=true
        // Diesel hydraulic engines have ovespreed protection. Switch this off by setting OverspeedProtectionKMpH=0
        // Typical overspeed protection values: M41--110, M47--76, MDmot--105.6
        // For ETCS_STM_HU the overspeed protection is automatically switched on based on the consist data, but max. 120 (+10) km/h.
        // 
        // The settings can be overridden by an .ini file set in Engine( ORTSTrainControlSystemParameters ( EVM.ini ) )
        // The file is searched for in Script directory.
        // An example .ini file might look like below (without slashes):
        //
        // A beállítások felülírhatók .ini fájllal, ami megadható az .eng-ben: Engine( ORTSTrainControlSystemParameters ( EVM.ini ) )
        // A fájlt a Script könyvtárban keresi az OpenRails.
        // Ezt az .ini opciót nem feltétlenül kell használni, de hasznos, ha a scriptet egy közös helyen akarjuk tárolni,
        // viszont több kölönböző konfigurációban szeretnénk használni a különböző gépeknél.
        // Egy példa .ini fájl így néz ki (persze a per-jeleket ki kell törölni a sorok elől):
        // 
        // [EVM]
        // ETCS_STM_HU=false
        // EVM160=true
        // HasDecelerationSwitch=true
        // EVM120=true
        // HasPressureSwitch=true
        // AlarmIsDistanceBased=true
        // OverspeedProtectionKMpH=0
        //
        bool ETCS_STM_HU = false;
        bool EVM160 = true;
        bool HasDecelerationSwitch = true;
        //
        bool EVM120 = true;
        bool HasPressureSwitch = true;
        //
        bool AlarmIsDistanceBased = true;
        double OverspeedProtectionKMpH = 0;
        // 
        // Az ETCS_STM_HU=true beállítása magában foglalja az EVM160=false, EVM120=true és HasPressureSwitch=true beállítását is.
        // Az EVM160=true beállítása magában foglalja az EVM120=true és HasPressureSwitch=true beállítását is.
        // Az EVM160=false beállítása magában foglalja a HasDecelerationSwitch=false beállítását is.
        // Az EVM120=true beállítása magában foglalja az AlarmIsDistanceBased=true beállítását is.
        // Az EVM120=false beállítása magában foglalja a HasPressureSwitch=false beállítását is.
        // Az EVM-et az ETCS-hez és kijelzőjéhez illesztő Speciális Átviteli Modullal szerelt gépekhez ETCS_STM_HU=true beállítása szükséges.
        // A régi négydobozos EÉVB-hez ETCS_STM_HU=false, EVM160=false és EVM120=false beállítása szükséges.
        // A fenti mellett ráadásul az M62 és V41 mozdonyokban a berendezés időalapon működik: AlarmIsDistanceBased=false
        // Néhány EVM-120 berendezéshez nincs nyomáskapcsoló párosítva: HasPressureSwitch=false
        // A kiadás időpontjában csak a Flirt és Talent motorvonatokban van lassulásérzékelő: HasDecelerationSwitch=true
        // A hidraulikus dízelgépeknek van túlsebbeségvédelme. Ha ilyen nincs a gépen: OverspeedProtectionKMpH=0
        // Tipikus túlsebességvédelmi értékek: M41--110, M47--76, MDmot--105.6
        // ETCS_STM_HU=true esetén a túlseb-védelem automatikusan beállításra kerül a szerelvény adatai alapján, de max. 120 (+10) km/h-ra.
        // 
        // Beállítások vége. End of user settings
        ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////

        // Alarm counters:
        Counter LongAlarm;
        Counter ShortAlarm;
        Counter VeryShortAlarm;
        Counter AlarmAck;
        
        // Alarm states
        bool VigilanceAlarm;
        bool VigilanceEmergency;
        bool OverspeedFullService;
        bool OverspeedEmergency;
        
        // States:
        bool Mode160; // If is in EVM-160 mode
        bool ActiveMode; // If speed is above 15 km/h
        bool ApproachingRestricted; // If true, number of clearances must be counted
        bool PressureSwitchOn = true; // True if brake line pressure is below BrakePressureMaxBar, or if no pressure switch present
        bool DecelerationSwitchOn; // True if deceleration is above DecelerationMinMpS2
        MonitoringStatus Status;
        
        // Variables used by Mode160:
        const int ClearanceCounterMax120 = 2;
        const int ClearanceCounterMax160 = 1;
        const float BrakePressureMax120Bar = 4.75f;
        const float BrakePressureMax160Bar = 3.6f;
        const float DecelerationMin120MpS2 = 0.3f;
        const float DecelerationMin160MpS2 = 0.96f; // Value for Talent EMU. For Flirt would be 1.12, but that seem to be too much for the existing model in OR
        float Mode160OnMpS = MpS.FromKpH(124);
        float Mode160OffMpS = MpS.FromKpH(80);
        float ActiveModeLimitEVMMpS = MpS.FromKpH(15);
        float ActiveModeLimitSTMMpS = MpS.FromKpH(5);

        int ClearanceCounterMax = ClearanceCounterMax120;
        float BrakePressureMaxBar = BrakePressureMax120Bar;
        float DecelerationMinMpS2 = DecelerationMin120MpS2;
        
        // Variables used by ActiveMode:
        float ActiveModeLimitMpS;
        
        // Variables used when ApproachingRestricted:
        float ApproachStopLimitMpS = MpS.FromKpH(40); // Allowed speed to approach STOP
        int ClearanceCounter;
        float NextSpeedLimitMpS;
        float CurrentSpeedLimitMpS;
        float PreviousSpeedMpS;
        float PreviousClockTime;
        bool PreviousPressureSwitch;
        
        // Constants used by alarm distance counters:
        const float LongAlarmM = 1550;
        const float ShortAlarmM = 175;
        const float VeryShortAlarm120M = 50; 
        const float VeryShortAlarm160M = 30; // For vehicles with allowed speed over 120 km/h
        const float AlarmAckM = 130; // older: 150; newer: 130
        
        // Constants used by alarm timers (for M62 and V41 locomotives):
        const float LongAlarmS = 60;
        const float ShortAlarmS = 20;
        const float VeryShortAlarmS = 0;
        const float AlarmAckS = 6;

        // Type EVM-120 (and later) equipments force alarm at signal aspect change
        bool ForcedAlarm;
        
        Aspect PreviousNextSignalAspect;
        float EVMMaxSpeedMpS;
        float OverspeedProtectionMpS;
        
        public EVM()
        {
        }

        public override void Initialize()
        {
            ETCS_STM_HU = GetBoolParameter("EVM", "ETCS_STM_HU", ETCS_STM_HU);
            EVM160 = GetBoolParameter("EVM", "EVM160", EVM160);
            HasDecelerationSwitch = GetBoolParameter("EVM", "HasDecelerationSwitch", HasDecelerationSwitch);
            EVM120 = GetBoolParameter("EVM", "EVM120", EVM120);
            HasPressureSwitch = GetBoolParameter("EVM", "HasPressureSwitch", HasPressureSwitch);
            AlarmIsDistanceBased = GetBoolParameter("EVM", "AlarmIsDistanceBased", AlarmIsDistanceBased);
            OverspeedProtectionKMpH = GetFloatParameter("EVM", "OverspeedProtectionKMpH", (float)OverspeedProtectionKMpH);
            
            if (ETCS_STM_HU)
            {
                EVM160 = false;
                EVM120 = true;
                HasPressureSwitch = true;
                ActiveModeLimitMpS = ActiveModeLimitSTMMpS;
                OverspeedProtectionKMpH = Math.Min(MpS.ToKpH(TrainSpeedLimitMpS()), 120);
            }
            else
                ActiveModeLimitMpS = ActiveModeLimitEVMMpS;
            
            if (EVM160)
            {
                EVM120 = true;
                HasPressureSwitch = true;
                EVMMaxSpeedMpS = MpS.FromKpH(160);
            }
            else
            {
                HasDecelerationSwitch = false;
                EVMMaxSpeedMpS = MpS.FromKpH(120);
            }

            if (EVM120)
                AlarmIsDistanceBased = true;
            else
                HasPressureSwitch = false;

            if (AlarmIsDistanceBased)
            {
                LongAlarm = new OdoMeter(this);
                ShortAlarm = new OdoMeter(this);
                VeryShortAlarm = new OdoMeter(this);
                AlarmAck = new OdoMeter(this);
                
                LongAlarm.Setup(LongAlarmM);
                ShortAlarm.Setup(ShortAlarmM);
                VeryShortAlarm.Setup(EVM160 ? VeryShortAlarm160M : VeryShortAlarm120M);
                AlarmAck.Setup(AlarmAckM);
            }
            else
            {
                LongAlarm = new Timer(this);
                ShortAlarm = new Timer(this);
                VeryShortAlarm = new Timer(this);
                AlarmAck = new Timer(this);
                
                LongAlarm.Setup(LongAlarmS);
                ShortAlarm.Setup(ShortAlarmS);
                VeryShortAlarm.Setup(VeryShortAlarmS);
                AlarmAck.Setup(AlarmAckS);
            }
            
            OverspeedProtectionMpS = ETCS_STM_HU
                ? EVMMaxSpeedMpS
                : Math.Max(MpS.FromKpH((float)OverspeedProtectionKMpH), 0);
            Activated = true;
            
            Console.WriteLine("EVM script v{0} initialized successfully. Type: {1}", Version, GetType());
        }
        
        private string GetType()
        {
            string type;
            if (ETCS_STM_HU) type = "ETCS_STM_HU";
            else if (EVM160) type = "EVM160";
            else if (EVM120) type = "EVM120";
            else type = "EÉVB";
            type += AlarmIsDistanceBased ? " ODO" : "TIME";
            if (HasDecelerationSwitch) type += " DS";
            if (HasPressureSwitch) type += " PS";
            if (OverspeedProtectionKMpH > 0) type += string.Format(" OS{0}", OverspeedProtectionKMpH);
            return type;
        }

        public override void Update()
        {
            // Setting current speed limit display only for compatibility reasons, EVM does not check and use this
            CurrentSpeedLimitMpS = CurrentSignalSpeedLimitMpS() >= 0 ? CurrentSignalSpeedLimitMpS() : EVMMaxSpeedMpS;
            if (CurrentSpeedLimitMpS > EVMMaxSpeedMpS)
                CurrentSpeedLimitMpS = EVMMaxSpeedMpS;
            SetCurrentSpeedLimitMpS(CurrentSpeedLimitMpS);
            
            // Update ActiveMode state:
            var activeMode = SpeedMpS() > ActiveModeLimitMpS;
            if (activeMode && !ActiveMode)
                VeryShortAlarm.Start();
            ActiveMode = activeMode;

            // Update ApproachingRestricted state:
            var approachingRestricted = NextSignalAspect(0) != Aspect.Clear_1 & NextSignalAspect(0) != Aspect.Clear_2;
            if (approachingRestricted)
            {
                if (!ApproachingRestricted)
                {
                    if (!VeryShortAlarm.Started && ActiveMode)
                        VeryShortAlarm.Start();
                }
                else
                {
                    if (!ShortAlarm.Started && ActiveMode)
                        ShortAlarm.Start();
                }
            }
            else
            {
                if (!LongAlarm.Started && ActiveMode)
                    LongAlarm.Start();
            }
            ApproachingRestricted = approachingRestricted;
            
            // Set cab next speed limit and aspect display
            NextSpeedLimitMpS = ApproachingRestricted ? NextSignalSpeedLimitMpS(0) : EVMMaxSpeedMpS;
            SetNextSpeedLimitMpS(NextSpeedLimitMpS);
            SetNextSignalAspect(NextSignalAspect(0));
            
            // Update Mode160 state:
            if (EVM160)
            {
                if (SpeedMpS() > Mode160OnMpS)
                    Mode160 = true;
                else if (SpeedMpS() < Mode160OffMpS)
                    Mode160 = false;
                ClearanceCounterMax = Mode160 ? ClearanceCounterMax160 : ClearanceCounterMax120;
                BrakePressureMaxBar = Mode160 ? BrakePressureMax160Bar : BrakePressureMax120Bar;
                DecelerationMinMpS2 = Mode160 ? DecelerationMin160MpS2 : DecelerationMin120MpS2;
            }
            
            // Update brake pressure switch:
            if (HasPressureSwitch)
            {
                PreviousPressureSwitch = PressureSwitchOn;
                if (BrakePipePressureBar() < BrakePressureMaxBar)
                    PressureSwitchOn = true;
                else if (BrakePipePressureBar() > BrakePressureMaxBar + 0.05f)
                    PressureSwitchOn = false;
                
                if (ETCS_STM_HU
                    && ApproachingRestricted
                    && PreviousPressureSwitch
                    && !PressureSwitchOn
                    && SpeedMpS() > NextSpeedLimitMpS
                    && SpeedMpS() > ApproachStopLimitMpS)
                {
                    SetEmergency(true);
                }
            }
            
            // Update Deceleration switch:
            var deltaT = ClockTime() - PreviousClockTime;
            if (HasDecelerationSwitch && (!Mode160 || deltaT > 1))
            {
                var decelerationMpS2 = (PreviousSpeedMpS - SpeedMpS()) / (deltaT);
                DecelerationSwitchOn = decelerationMpS2 > DecelerationMinMpS2;
                PreviousClockTime = ClockTime();
                PreviousSpeedMpS = SpeedMpS();
            }
            
            // Update "EVM" display lamp
            if (HasPressureSwitch || HasDecelerationSwitch)
                SetVigilanceAlarmDisplay(PressureSwitchOn || DecelerationSwitchOn && SpeedMpS() > 0);
            else
                SetVigilanceAlarmDisplay(AlerterSound());
                

            UpdateVigilance();
            
            // Update overspeed monitoring state, for compatibility reasons. EVM does not monitor actual speed limit
            if (SpeedMpS() < W(NextSpeedLimitMpS) && SpeedMpS() < W(CurrentSpeedLimitMpS))
            {
                Status = MonitoringStatus.Normal;
                SetInterventionSpeedLimitMpS(0);
            }
            else if (SpeedMpS() < W(CurrentSpeedLimitMpS))
            {
                Status = MonitoringStatus.Overspeed;
                SetInterventionSpeedLimitMpS(0);
            }
            else if (SpeedMpS() < FS(CurrentSpeedLimitMpS))
            {
                if (!OverspeedEmergency && !OverspeedFullService)
                {
                    Status = MonitoringStatus.Warning;
                    SetInterventionSpeedLimitMpS(FS(CurrentSpeedLimitMpS));
                }
            }
            else
            {
                SetOverspeedWarningDisplay(true);
                Status = MonitoringStatus.Intervention;
                
                if (!OverspeedEmergency && SpeedMpS() < EB(CurrentSpeedLimitMpS))
                {
                    SetInterventionSpeedLimitMpS(EB(CurrentSpeedLimitMpS));
                    OverspeedFullService = true;
                }
                else
                {
                    OverspeedEmergency = true;
                }
            }
            if (OverspeedEmergency)
                SetInterventionSpeedLimitMpS(SpeedMpS());
            SetMonitoringStatus(Status);
            
            // Update engine overspeed protection
            if (OverspeedProtectionMpS > 0)
            {
                if (SpeedMpS() > (ETCS_STM_HU ? W(OverspeedProtectionMpS) : OverspeedProtectionMpS))
                {
                    if (!VeryShortAlarm.Started) VeryShortAlarm.Start();
                    SetOverspeedWarningDisplay(true);
                }
                if (SpeedMpS() > (ETCS_STM_HU ? FS(OverspeedProtectionMpS) : OverspeedProtectionMpS))
                {
                    SetEmergency(true);
                    OverspeedFullService = true;
                }
            }
            
            // Emergency state auto-release
            if (!VigilanceEmergency
                && SpeedMpS() <= NextSpeedLimitMpS
                && (OverspeedProtectionMpS == 0 || SpeedMpS() <= OverspeedProtectionMpS * (ETCS_STM_HU ? 1 : 0.97f)))
            {
                SetEmergency(false);
                OverspeedFullService = false;
            }
            
            //Console.WriteLine("___A: {0}, R: {1}, E: {2}, D: {3}, BP: {4}", ActiveMode, ApproachingRestricted, Mode160, DecelerationSwitchOn, BrakePipePressureBar());

            if (!IsBrakeEmergency() && !IsBrakeFullService())
                SetPenaltyApplicationDisplay(false);
        }

        public override void HandleEvent(TCSEvent evt, string message)
        {
            switch(evt)
            {
                case TCSEvent.AlerterPressed:
                    if (!Activated || VigilanceEmergency)
                        return;

                    if (ApproachingRestricted && SpeedMpS() > NextSpeedLimitMpS)
                        ClearanceCounter++;
                    else
                        ClearanceCounter = 0;
                    
                    if (!AlerterSound() && ActiveMode)
                    {
                        VeryShortAlarm.Start();
                        LongAlarm.Stop();
                        ShortAlarm.Stop();
                    }
                    else if (!AlarmAck.Started && ActiveMode)
                        AlarmAck.Start();
                    break;
                
                case TCSEvent.AlerterReleased:
                    if (VigilanceEmergency && ActiveMode)
                        return;

                    ForcedAlarm = false;
                    
                    if (!ApproachingRestricted 
                        || ClearanceCounter <= ClearanceCounterMax 
                        || PressureSwitchOn
                        || DecelerationSwitchOn
                        || SpeedMpS() < NextSpeedLimitMpS
                        || SpeedMpS() < ApproachStopLimitMpS)
                    {
                        AlarmAck.Stop();
                        LongAlarm.Stop();
                        ShortAlarm.Stop();
                        VeryShortAlarm.Stop();
                    }
                    if (!ActiveMode || !OverspeedEmergency && SpeedMpS() < NextSpeedLimitMpS)
                    {
                        SetEmergency(false);
                        OverspeedFullService = false;
                    }
                    
                    break;
            }
        }

        public override void SetEmergency(bool state)
        {
            SetEmergencyBrake(state);
            if (state)
            {
                SetPenaltyApplicationDisplay(true);
                SetThrottleController(0.0f);
            }
            else
            {
                SetOverspeedWarningDisplay(false);
            }
        }

        void UpdateVigilance()
        {
            if (EVM120 && PreviousNextSignalAspect != NextSignalAspect(0))
            {
                PreviousNextSignalAspect = NextSignalAspect(0);
                ForcedAlarm = true;
            }

            if (!ActiveMode)
            {
                LongAlarm.Stop();
                ShortAlarm.Stop();
                VeryShortAlarm.Stop();
                if (!VigilanceEmergency)
                    AlarmAck.Stop();
                ClearanceCounter = 0;
                if (AlerterSound() && !ForcedAlarm)
                    SetVigilanceAlarm(false);
                OverspeedEmergency = false;
            }

            VigilanceAlarm = VeryShortAlarm.Triggered || ShortAlarm.Triggered || LongAlarm.Triggered || ForcedAlarm;
            VigilanceEmergency = AlarmAck.Triggered;
            SetVigilanceEmergencyDisplay(VigilanceEmergency);

            if (VigilanceEmergency)
            {
                SetEmergency(true);
                return;
            }

            if (VigilanceAlarm)
            {
                if (!AlerterSound())
                    SetVigilanceAlarm(true);
                if (!AlarmAck.Started)
                    AlarmAck.Start();

                if (!IsAlerterEnabled())
                {
                    HandleEvent(TCSEvent.AlerterPressed, String.Empty);
                    HandleEvent(TCSEvent.AlerterReleased, String.Empty);
                }
            }
            else
            {
                if (AlerterSound())
                    SetVigilanceAlarm(false);
            }
        }
        
        float W(float speedMpS)
        {
            var speed = MpS.ToKpH(speedMpS);
            if (speed < 16) return MpS.FromKpH(17.2f);
            else if (speed < 41) return MpS.FromKpH(43);
            else if (speed < 81) return MpS.FromKpH(84.3f);
            else if (speed < 101) return MpS.FromKpH(105);
            else if (speed < 121) return MpS.FromKpH(125);
            else return MpS.FromKpH(165);
        }
        
        float FS(float speedMpS)
        {
            var speed = MpS.ToKpH(speedMpS);
            if (speed < 16) return MpS.FromKpH(19.3f);
            else if (speed < 41) return MpS.FromKpH(46);
            else if (speed < 81) return MpS.FromKpH(88.7f);
            else if (speed < 101) return MpS.FromKpH(110);
            else if (speed < 121) return MpS.FromKpH(130);
            else return MpS.FromKpH(170);
        }
        
        float EB(float speedMpS)
        {
            var speed = MpS.ToKpH(speedMpS);
            if (speed < 16) return MpS.FromKpH(21.5f);
            else if (speed < 41) return MpS.FromKpH(49);
            else if (speed < 81) return MpS.FromKpH(93);
            else if (speed < 101) return MpS.FromKpH(115);
            else if (speed < 121) return MpS.FromKpH(135);
            else return MpS.FromKpH(175);
        }
    }
}