--------------------------------------------------------------------------------
-- Generic relay with configureable parameters
--------------------------------------------------------------------------------
-- Copyright (C) 2013-2018 Metrostroi Team & FoxWorks Aerospace s.r.o.
-- Contains proprietary code. See license.txt for additional information.
--------------------------------------------------------------------------------
Metrostroi.DefineSystem("Relay")

local relay_types = {
    ["PK-162"] = {
        pneumatic       = true,
        contactor       = true,
    },
    ["Switch"] = {
        contactor       = true,
    },
    ["GV_10ZH"] = {
        contactor       = true,
        normally_closed = true,
    },
    ["VA21-29"] = {
        contactor       = true,
        normally_closed = true,
        bass = true,
    },
    ["AVU-045"] = {
        bass    = true,
    },
    ["Switch"] = {
        close_time = 0,
        open_time = 0,
    },
    ["ARS"] = {
        close_time = 0,
        open_time = 0,
    }
}

function TRAIN_SYSTEM:Initialize(parameters,extra_parameters)
    ----------------------------------------------------------------------------
    -- Initialize parameters
    if not parameters then parameters = {} end
    if type(parameters) ~= "table" then
        relay_type = parameters
        if relay_types[relay_type] then
            parameters = relay_types[relay_type]
        else
            --print("[sys_relay.lua] Unknown relay type: "..parameters)
            parameters = {}
        end
        parameters.relay_type = relay_type
    end

    -- Create new table
    local old_param = parameters
    parameters = {} for k,v in pairs(old_param) do parameters[k] = v end

    -- Add extra parameters
    if type(extra_parameters) == "table" then
        for k,v in pairs(extra_parameters) do
            parameters[k] = v
        end
    end

    -- Contactors have different failure modes
    parameters.contactor        = parameters.contactor or false
    -- Should the relay be initialized in 'closed' state
    parameters.normally_closed  = parameters.normally_closed or false
    -- Time in which relay will close (seconds)
    parameters.close_time       = parameters.close_time or 0.050
    -- Time in which relay will open (seconds)
    parameters.open_time        = parameters.open_time or 0.050
    -- Is relay latched (stays in its position even without voltage)
    parameters.latched          = parameters.latched or false
    -- Should relay be spring-returned to initial position
    parameters.returns          = parameters.returns or (not parameters.latched)
    -- Trigger level for the relay
    parameters.trigger_level    = parameters.trigger_level or 0.5
    for k,v in pairs(parameters) do
        self[k] = v
    end



    ----------------------------------------------------------------------------
    -- Relay parameters
    if self.close_time == 0 then
        FailSim.AddParameter(self,"CloseTime",      { value = parameters.close_time})
    else
        FailSim.AddParameter(self,"CloseTime",      { value = parameters.close_time, precision = self.contactor and 0.35 or 0.10, min = 0.010, varies = true })
    end
    if self.open_time == 0 then
        FailSim.AddParameter(self,"OpenTime",       { value = parameters.open_time})
    else
        FailSim.AddParameter(self,"OpenTime",       { value = parameters.open_time, precision = self.contactor and 0.35 or 0.10, min = 0.010, varies = true })
    end
    -- Did relay short-circuit?
    FailSim.AddParameter(self,"ShortCircuit",   { value = 0.000, precision = 0.00 })
    -- Was there a spurious trip?
    FailSim.AddParameter(self,"SpuriousTrip",   { value = 0.000, precision = 0.00 })

    -- Calculate failure parameters
    local MTBF = parameters.MTBF or 1000000 -- cycles, mean time between failures
    local MFR = 1/MTBF   -- cycles^-1, total failure rate
    local openWeight,closeWeight
    -- FIXME
    openWeight = self.open_weight or 0.25
    closeWeight = self.close_weight or 0.25
    --[[if self.Contactor then
        openWeight = 0.25
        closeWeight = 0.25
    elseif self.NormallyOpen then
        openWeight = 0.4
        closeWeight = 0.1
    else
        openWeight = 0.1
        closeWeight = 0.4
    end]]--

    -- Add failure points
    FailSim.AddFailurePoint(self,   "CloseTime", "Mechanical problem (close time not nominal)",
        { type = "precision",   value = 0.5,    mfr = MFR*0.65*openWeight, recurring = true } )
    FailSim.AddFailurePoint(self,   "OpenTime", "Mechanical problem (open time not nominal)",
        { type = "precision",   value = 0.5,    mfr = MFR*0.65*closeWeight , recurring = true } )
    FailSim.AddFailurePoint(self,   "CloseTime", "Stuck closed",
        { type = "value",       value = 1e9,    mfr = MFR*0.65*openWeight, dmtbf = 0.2 } )
    FailSim.AddFailurePoint(self,   "OpenTime", "Stuck open",
        { type = "value",       value = 1e9,    mfr = MFR*0.65*closeWeight , dmtbf = 0.4 } )
    FailSim.AddFailurePoint(self,   "SpuriousTrip", "Spurious trip",
        { type = "on",                          mfr = MFR*0.20, dmtbf = 0.4 } )
    --FailSim.AddFailurePoint(self, "ShortCircuit", "Short-circuit",
        --{ type = "on",                            mfr = MFR*0.15, dmtbf = 0.2 } )



    ----------------------------------------------------------------------------
    -- Initial relay state
    if self.normally_closed then
        self.TargetValue = 1.0
        self.Value = 1.0
    else
        self.TargetValue = self.defaultvalue or 0.0
        self.Value = self.defaultvalue or 0.0
    end
    -- Time when relay should change its value
    self.Time = 0
    self.ChangeTime = nil
    self.Blocked = 0
    -- This increases precision at cost of perfomance
    self.SubIterations = parameters.iterations or 1--relay
end

function TRAIN_SYSTEM:Inputs()
    return { "Open","Close","+","-","Set","Toggle","Block","OpenBypass","Check","OpenTime","CloseTime"}
end

function TRAIN_SYSTEM:Outputs()
    return { "Value" , "Blocked","TargetValue"}
end


function TRAIN_SYSTEM:TriggerInput(name,value)
    -- Boolean values accepted
    if type(value) == "boolean" then value = value and 1 or 0 end
    if name == "OpenTime" then
        self.open_time = value
        FailSim.AddParameter(self,"OpenTime",       { value = self.open_time})
    end
    if name == "CloseTime" then
        self.close_time = value
        FailSim.AddParameter(self,"CloseTime",      { value = self.close_time})
    end
    if name == "Reset" then
        if self.normally_closed then
            self:TriggerInput("Set",1)
        else
            self:TriggerInput("Set",self.defaultvalue or 0.0)
        end
    end
        --print(name)
    if name == "Check" then
        if value < 0 and self.Value == 1 then
            self:TriggerInput("Set",0)
            --self:TriggerInput("Set",0)
            self.Train:PlayOnce("av_off","cabin",0.7,70)
        end
        return
    end
    if value == -1 and self.relay_type == "VA21-29" then
        self:TriggerInput("Set",0)
        return
    end
    if name == "OpenBypass" then
        if (not self.ChangeTime) and (self.TargetValue ~= 0.0) then
            self.ChangeTime = self.Time + FailSim.Value(self,"OpenTime")
        end
        self.TargetValue = 0.0
        if self.ChangeTime==self.Time and self.Train.DeltaTime then self:Think(self.Train.DeltaTime) end

        return
    end

    if self.Blocked > 0 and name ~= "Block" and (name == "Close" and self.relay_type == "PK-162" or self.relay_type ~= "PK-162") then return end

    -- Open/close coils of the relay
    if (name == "Block") then
        self.Blocked = value
    elseif (name == "Close") and (value > self.trigger_level) and (self.Value ~= 1.0 or self.TargetValue ~= 1.0) then --(self.TargetValue ~= 1.0 and self.rpb))
        if self.pneumatic  and self.Train.Pneumatic.TrainLinePressure < 3 then return end
        if (not self.ChangeTime) or (self.TargetValue ~= 1.0) then
            self.ChangeTime = self.Time + FailSim.Value(self,"CloseTime")
        end
        if self.Value == 1.0 then self.ChangeTime = nil end
        self.TargetValue = 1.0
        if self.ChangeTime==self.Time and self.Train.DeltaTime then self:Think(self.Train.DeltaTime) end

    elseif (name == "Open") and (value > self.trigger_level) and (self.Value ~= 0.0 or self.TargetValue ~= 0.0) then
        if (not self.ChangeTime) or (self.TargetValue ~= 0.0) then
            self.ChangeTime = self.Time + FailSim.Value(self,"OpenTime")
        end
        self.TargetValue = 0.0
        if self.ChangeTime==self.Time and self.Train.DeltaTime then self:Think(self.Train.DeltaTime) end

    elseif name == "NoOpenTime" and value > 0 then
        self.ChangeTime = self.Time
    elseif (name == "+") and (self.Value < (self.maxvalue or self.three_position and 2 or 1)) and value > 0 then
        self.ChangeTime = self.Time + FailSim.Value(self,"CloseTime")
        self.TargetValue = math.min(self.maxvalue or self.three_position and 2 or 1,self.Value+1)
        if self.ChangeTime==self.Time and self.Train.DeltaTime then self:Think(self.Train.DeltaTime) end

    elseif (name == "-") and (self.Value > 0) and value > 0 then
        self.ChangeTime = self.Time + FailSim.Value(self,"OpenTime")
        self.TargetValue = math.max(0.0,self.Value-1)
        if self.ChangeTime==self.Time and self.Train.DeltaTime then self:Think(self.Train.DeltaTime) end

    elseif name == "Set" then
        if self.pneumatic and value > 0 and self.Train.Pneumatic.TrainLinePressure < 3 then return end
        if self.maxvalue then
            if not self.ChangeTime then
                self.ChangeTime = self.Time + FailSim.Value(self,"OpenTime")
            end
            self.TargetValue = math.max(0.0,math.min(self.maxvalue,math.floor(value)))
            if self.ChangeTime==self.Time and self.Train.DeltaTime then self:Think(self.Train.DeltaTime) end

        elseif self.three_position then
            if not self.ChangeTime then
                self.ChangeTime = self.Time + FailSim.Value(self,"OpenTime")
            end
            self.TargetValue = math.max(0.0,math.min(2.0,math.floor(value)))
            if self.ChangeTime==self.Time and self.Train.DeltaTime then self:Think(self.Train.DeltaTime) end

        else
            if value > self.trigger_level
            then self:TriggerInput("Close",self.trigger_level+1)
            else self:TriggerInput("Open",self.trigger_level+1)
            end
        end
    elseif (name == "Toggle") and (value > 0.5) then
        if self.maxvalue then
            self:TriggerInput("Set",self.Value > self.maxvalue-1 and 0 or self.Value+1)
        elseif self.three_position then
            self:TriggerInput("Set",self.Value > 1 and 0 or self.Value+1)
        else
            self:TriggerInput("Set",(1.0 - self.Value)*(self.trigger_level+1))
        end
    end
end

function TRAIN_SYSTEM:Think(dT)
    --print(self.relay_type)
    self.Time = self.Time + dT
    -- Short-circuited relay
    if FailSim.Value(self,"ShortCircuit") > 0.5 then
        self.Value = 1.0
        return
    end
    -- Spurious trip
    if FailSim.Value(self,"SpuriousTrip") > 0.5 then
        self.SpuriousTripTimer = self.Time + (0.5 + 2.5*math.random())
        FailSim.ResetParameter(self,"SpuriousTrip",0.0)
        FailSim.Age(self,1)

        -- Simulate switch right away
        self.Value = 1.0 - self.Value
        self.TargetValue = self.Value
        self.ChangeTime = nil
    end
    if self.SpuriousTripTimer and (self.Time > self.SpuriousTripTimer) then
        self.Value = self.TargetValue
        self.SpuriousTripTimer = nil
    end
    -- Switch relay
    if self.ChangeTime and (self.Time > self.ChangeTime) and not self.SpuriousTripTimer then
        -- Electropneumatic relays make this sound
        if self.bass and self.Value ~= self.TargetValue then
            --[[
            if self.Value ~= 0.0 and self.maxvalue ~= 2 or self.Value ~= 1.0 and self.maxvalue == 2 then
                if self.av3 then self.Train:PlayOnce("vu22b_on","cabin") end
                if self.igla then self.Train:PlayOnce("igla_on","cabin") end
                if self.button then self.Train:PlayOnce("button_press","cabin",0.51) end
                if self.vud then self.Train:PlayOnce("vu22_on","cabin") end
                if self.uava then self.Train:PlayOnce("uava_on","cabin") end
                if self.pb then self.Train:PlayOnce("switch6","cabin") end
                if self.programm then self.Train:PlayOnce("inf_on","cabin") end
                if self.programm1 then self.Train:PlayOnce("triple_up-0","cabin") end
                if self.programm2 then self.Train:PlayOnce("triple_down-0","cabin") end
                if self.av then self.Train:PlayOnce("auto_on","cabin") end
                if self.mainav then self.Train:PlayOnce("mainauto_on","cabin") end
                if self.krishka then self.Train:PlayOnce("kr_close","cabin") end
                if self.paketnik then self.Train:PlayOnce("pak_on","cabin") end
                if self.switch then self.Train:PlayOnce("switch_on","cabin") end
                if self.rcr then self.Train:PlayOnce("rcr_on","cabin") end
            end
            if self.Value == 0.0 and self.maxvalue ~= 2 or self.Value == 1.0 and self.maxvalue == 2 then
                if self.av3 then self.Train:PlayOnce("vu22b_off","cabin") end
                if self.igla then self.Train:PlayOnce("igla_off","cabin") end
                if self.button then self.Train:PlayOnce("button_release","cabin",0.56) end
                if self.vud then self.Train:PlayOnce("vu22_off","cabin") end
                if self.uava then self.Train:PlayOnce("uava_off","cabin") end
                if self.pb then self.Train:PlayOnce("switch6_off","cabin") end
                if self.programm then self.Train:PlayOnce("inf_off","cabin") end
                if self.programm1 then self.Train:PlayOnce("triple_0-up","cabin") end
                if self.programm2 then self.Train:PlayOnce("triple_0-down","cabin") end
                if self.av then self.Train:PlayOnce("auto_off","cabin") end
                if self.mainav then self.Train:PlayOnce("mainauto_off","cabin") end
                if self.krishka then self.Train:PlayOnce("kr_open","cabin") end
                if self.paketnik then self.Train:PlayOnce("pak_off","cabin") end
                if self.switch then self.Train:PlayOnce("switch_off","cabin") end
                if self.rcr then self.Train:PlayOnce("rcr_off","cabin") end
            end
            ]]
            if self.bass_separate then
                if self.TargetValue > 0 then
                    self.Train:PlayOnce(self.Name.."_on","bass",1)
                else
                    self.Train:PlayOnce(self.Name.."_off","bass",1)
                end
            else
                self.Train:PlayOnce(self.Name,"bass",self.TargetValue)
            end
        end
        self.Value = self.TargetValue
        self.ChangeTime = nil

        -- Age relay a little
        FailSim.Age(self,1)
    end
end
