--------------------------------------------------------------------------------
-- Simulation acceleration DLL support
--------------------------------------------------------------------------------
if not TURBOSTROI and (not Turbostroi or not Turbostroi.SetMTAffinityMask) then return end
local turbostroiTrains = {}
if Turbostroi and not TURBOSTROI then
    local FPS = 1/engine.TickInterval()
    local messageTimeout = 0
    local messageCounter = 0
    local dataCache = {{},{}}

    local SendMessage = Turbostroi.SendMessage
    local RecvMessages = Turbostroi.RecvMessages
    local RecvMessage = Turbostroi.RecvMessage
    local ReadAvailable = Turbostroi.ReadAvailable

    local unpack = unpack

    hook.Add("EntityRemoved","Turbostroi",function(ent)
        if dataCache[ent] then
            dataCache[ent] = nil
        end
        if turbostroiTrains[ent] then
            turbostroiTrains[ent] = nil
        end
    end)
    for k,ent in pairs(ents.GetAll()) do
        if ent.Base == "gmod_subway_base" and not ent.NoTrain and not ent.DontAccelerateSimulation then
            turbostroiTrains[ent] = true
        end
    end
    hook.Add("OnEntityCreated","Turbostroi",function(ent)
        timer.Simple(0,function()
            if IsValid(ent) and ent.Base == "gmod_subway_base" and not ent.NoTrain and not ent.DontAccelerateSimulation then
                turbostroiTrains[ent] = true
            end
        end)
    end)
    local id,system,name,index,value
    local msg_count = 0
    local idActionTable = {
        [1] = function (train)
            if train.Systems[system] then
                train.Systems[system][name] = value
                if train.TriggerTurbostroiInput then train:TriggerTurbostroiInput(system,name,value) end
            end
        end,
        [2] = function (train)
            if index == 0 and name ~= "bass" then index = nil end
            if value == 0 and name ~= "bass" then value = nil end
            if name == "" then name = nil end
                --net.WriteString(name)
            train:PlayOnce(system,name,index,value)
        end,
        [3] = function (train)
            if name == "on" then
                --print("[!]Wire "..index.." starts update! Value "..value)
                dataCache[train]["wiresW"][index] = value
                --train:WriteTrainWire(index,value)
                if not train.TrainWireWritersID[index] then train.TrainWireWritersID[index] = true end
                train.TrainWireTurbostroi[index] = value
                if train.TriggerTurbostroiInput then train:TriggerTurbostroiInput("TrainWire",index,value) end
            else
                --print("[!]Wire "..index.." stop update!")
                dataCache[train]["wiresW"][index] = nil
            end
        end,
        [4] = function (train)
            if train.Systems[system] then
                train.Systems[system]:TriggerInput(name,value)
            end
        end,
        [5] = function (train)
            for twid,value in pairs(dataCache[train]["wiresW"]) do
                --train:WriteTrainWire(twid,value)
            end
        end,
        [6] = function (train)
            if IsValid(Player(index)) then
                if value==0 then
                    Player(index):PrintMessage( HUD_PRINTCONSOLE, "--START" )
                    print("--START")
                end
                Player(index):PrintMessage( HUD_PRINTCONSOLE, system )
                print(system)
            end
        end,
    }
    local function updateTrains(trains)
        --local recvMessage = Turbostroi.RecvMessage
        -- Get data packets from simulation
        for train in pairs(trains) do
            if not dataCache[train] then
                if not SendMessage(train,5,"","",0,0) then return end
                dataCache[train] = {wiresW = {}}

                for sys_name,system in pairs(train.Systems) do
                    if system.OutputsList and system.DontAccelerateSimulation then
                        for _,name in pairs(system.OutputsList) do
                            local value = system[name] or 0
                            if type(value) == "boolean" then value = value and 1 or 0 end
                            if type(value) == "number" then
                                if not dataCache[train][sys_name] then dataCache[train][sys_name] = {} end
                                dataCache[train][sys_name][name] = math.Round(value)
                            end
                        end
                    end
                end
            end
            msg_count = ReadAvailable(train)
            for _ = 1, msg_count do
                id,system,name,index,value = RecvMessage(train)

                idActionTable[id](train)
                messageCounter = messageCounter + 1

            end
        end
        -- Send train wire values
        -- Output all system values
        for train in pairs(trains) do
            if train.ReadTrainWire then
                for i in pairs(train.TrainWires) do
                    if not dataCache[train]["wires"] then dataCache[train]["wires"] = {} end
                    if dataCache[train]["wires"][i] ~= train:ReadTrainWire(i) then
                        if SendMessage(train,3,"","",i,train:ReadTrainWire(i)) then
                            dataCache[train]["wires"][i] = train:ReadTrainWire(i)
                        end
                    end
                end
                for sys_name,system in pairs(train.Systems) do
                    if system.OutputsList and system.DontAccelerateSimulation then
                        for _,name in pairs(system.OutputsList) do
                            local value = system[name] or 0
                            if type(value) == "boolean" then
                                value = value and 1 or 0
                            end
                            if type(value) == "number" then
                                value = math.Round(value,1)
                                if not dataCache[train][sys_name] then dataCache[train][sys_name] = {} end
                                if dataCache[train][sys_name][name] ~= value then
                                    if SendMessage(train,1,sys_name,name,0,value) then
                                        dataCache[train][sys_name][name] = value
                                    end
                                end
                            end
                        end
                    end
                end
            end
        end
    end
    if Turbostroi then
        concommand.Add("metrostroi_turbostroi_run",function(ply,_,_,cmd)
            if not IsValid(ply) or not ply:IsSuperAdmin() then return end
            local train = ply:GetTrain()
            if IsValid(train) then
                print(cmd:sub(1,2),cmd:sub(3,4))
                SendMessage(train,6,cmd:sub(1,255),cmd:sub(256,511),ply:UserID(),0)
            end
        end)
        function Turbostroi.TriggerInput(train,system,name,value)
            local v = value or 0
            if type(value) == "boolean" then v = value and 1 or 0 end
                SendMessage(train,4,system,name,0,v)
            --end
        end
        CreateConVar("turbostroi_main_cores",1, FCVAR_ARCHIVE,"Set cores for main thread")
        CreateConVar("turbostroi_train_cores",254, FCVAR_ARCHIVE,"Set cores for train threads")
        cvars.AddChangeCallback("turbostroi_main_cores", function(cvar, old, value)
            Turbostroi.SetMTAffinityMask(tonumber(value) or 1) -- CPU5 CPU4 on 6 core --NEWTURBOSTROI
        end, "turbostroi")
        cvars.AddChangeCallback("turbostroi_train_cores", function(cvar, old, value)
            Turbostroi.SetSTAffinityMask(tonumber(value) or 254) -- 0 - disabled --NEWTURBOSTROI
        end, "turbostroi")
        Turbostroi.SetMTAffinityMask(GetConVar("turbostroi_main_cores"):GetInt() or 1) -- CPU5 CPU4 on 6 core --NEWTURBOSTROI
        Turbostroi.SetSTAffinityMask(GetConVar("turbostroi_train_cores"):GetInt() or 254) -- 0 - disabled --NEWTURBOSTROI
        Turbostroi.SetSimulationFPS(FPS)
        hook.Add("Think", "Turbostroi_Think", function()
            if not Turbostroi then return end
            updateTrains(turbostroiTrains)
            --[[
            -- HACK
            GLOBAL_SKIP_TRAIN_SYSTEMS = nil

            -- Print stats
                if ((CurTime() - messageTimeout) > 1.0) then
                messageTimeout = CurTime()
                RunConsoleCommand("say",Format("Metrostroi: %d messages per second (%d per tick)",messageCounter,messageCounter / FPS))
                messageCounter = 0
            end]]
        end)
    end
    return
end




--------------------------------------------------------------------------------
-- Turbostroi scripts
--------------------------------------------------------------------------------
-- NEW API
local ffi = require("ffi")
ffi.cdef[[
bool ThreadSendMessage(void *p, int message, const char* system_name, const char* name, double index, double value);
int ThreadReadAvailable(void* p);
typedef struct {
    int message;
    char system_name[64];
    char name[64];
    double index;
    double value;
} thread_msg;
thread_msg ThreadRecvMessage(void* p);
]]
local TS = ffi.load("gmsv_turbostroi_win32")

Metrostroi = {}
local dataCache = {wires = {},wiresW = {},wiresL = {}}
Metrostroi.BaseSystems = {} -- Systems that can be loaded
Metrostroi.Systems = {} -- Constructors for systems

LoadSystems = {} -- Systems that must be loaded/initialized
GlobalTrain = {} -- Train emulator
GlobalTrain.Systems = {} -- Train systems
GlobalTrain.TrainWires = {}
GlobalTrain.WriteTrainWires = {}

TimeMinus = 0
_Time = 0
function CurTime()
    --return CurrentTime-TimeMinus
    return _Time
end
--function CurTime() return os.clock() end

function Metrostroi.DefineSystem(name)
    TRAIN_SYSTEM = {}
    Metrostroi.BaseSystems[name] = TRAIN_SYSTEM

    -- Create constructor
    Metrostroi.Systems[name] = function(train,...)
        local tbl = { _base = name }
        local TRAIN_SYSTEM = Metrostroi.BaseSystems[tbl._base]
        if not TRAIN_SYSTEM then print("No system: "..tbl._base) return end
        for k,v in pairs(TRAIN_SYSTEM) do
            if type(v) == "function" then
                tbl[k] = function(...)
                    if not Metrostroi.BaseSystems[tbl._base][k] then
                        print("ERROR",k,tbl._base)
                    end
                    return Metrostroi.BaseSystems[tbl._base][k](...)
                end
            else
                tbl[k] = v
            end
        end

        tbl.Initialize = tbl.Initialize or function() end
        tbl.Think = tbl.Think or function() end
        tbl.Inputs = tbl.Inputs or function() return {} end
        tbl.Outputs = tbl.Outputs or function() return {} end
        tbl.TriggerInput = tbl.TriggerInput or function() end
        tbl.TriggerOutput = tbl.TriggerOutput or function() end

        tbl.Train = train
        tbl:Initialize(...)
        tbl.OutputsList = tbl:Outputs()
        tbl.InputsList = tbl:Inputs()
        tbl.IsInput = {}
        for k,v in pairs(tbl.InputsList) do tbl.IsInput[v] = true end
        return tbl
    end
end

function GlobalTrain.LoadSystem(self,a,b,...)
    local name
    local sys_name
    if b then
        name = b
        sys_name = a
    else
        name = a
        sys_name = a
    end

    if not Metrostroi.Systems[name] then print("Error: No system defined: "..name) return end
    if self.Systems[sys_name] then print("Error: System already defined: "..sys_name)  return end

    self[sys_name] = Metrostroi.Systems[name](self,...)
    self[sys_name].Name = sys_name
    self[sys_name].BaseName = name
    self.Systems[sys_name] = self[sys_name]

    -- Don't simulate on here
    local no_acceleration = Metrostroi.BaseSystems[name].DontAccelerateSimulation
    if no_acceleration then
        self.Systems[sys_name].Think = function() end
        self.Systems[sys_name].TriggerInput = function(train,name,value)
            local v = value or 0
            if type(value) == "boolean" then v = value and 1 or 0 end
            TS.ThreadSendMessage(_userdata, 4,sys_name,name,0,v)
        end -- replace with new api

    --Precache values
    elseif self[sys_name].OutputsList then
        dataCache[sys_name] = {}
        for _,name in pairs(self[sys_name].OutputsList) do
            dataCache[sys_name][name] = 0--self[sys_name][name] or 0
        end
    end
end

function GlobalTrain.PlayOnce(self,soundid,location,range,pitch)
    TS.ThreadSendMessage(_userdata, 2,soundid or "",location or "",range or 0,pitch or 0) -- replace with new api
end

function GlobalTrain.ReadTrainWire(self,n)
    return self.TrainWires[n] or 0
end

function GlobalTrain.WriteTrainWire(self,n,v)
    self.WriteTrainWires[n] = v
end


GlobalTrain.DeltaTime = 0.33

--------------------------------------------------------------------------------
-- Main train code (turbostroi side)
--------------------------------------------------------------------------------
print("[!] Train initialized!")
function Think(skipped)
    -- This is just blatant copy paste from init.lua of base train entity
    local self = GlobalTrain

    --[[ if skipped then
        self.BeSkip = self.BeSkip or CurTime()
        return
    else
        self.PrevTime = self.PrevTime or CurTime()
        if self.BeSkip then
            --print(1,(CurTime()-self.BeSkip)-0.03)
            TimeMinus = TimeMinus + math.max(0,(CurTime()-self.BeSkip)-0.03)
            --print(2,TimeMinus)
            self.BeSkip = false
        end
    end--]]

    -- Is initialized?
    if not self.Initialized then
        Initialize()
        return
    end

    self.DeltaTime = (CurrentTime - self.PrevTime)--self.DeltaTime+math.min(0.02,((CurrentTime - self.PrevTime)-self.DeltaTime)*0.1)
    self.PrevTime = CurrentTime
    if skipped or self.DeltaTime<=0 then return end
    _Time = _Time+self.DeltaTime

    -- Perform data exchange
    DataExchange()

    -- Simulate according to schedule
    for i,s in ipairs(self.Schedule) do
        for k,v in ipairs(s) do
            v:Think(self.DeltaTime / (v.SubIterations or 1),i)
        end
    end
end

function Initialize()
    if not CurrentTime then return end
    print("[!] Loading systems")
    local time = os.clock()
    for k,v in pairs(LoadSystems) do
        GlobalTrain:LoadSystem(k,v)
    end
    print(string.format("[!] -Took %.2fs",os.clock()-time))
    GlobalTrain.PrevTime = CurrentTime
    local iterationsCount = 1
    if (not GlobalTrain.Schedule) or (iterationsCount ~= GlobalTrain.Schedule.IterationsCount) then
        GlobalTrain.Schedule = { IterationsCount = iterationsCount }
        local SystemIterations = {}

        -- Find max number of iterations
        local maxIterations = 0
        for k,v in pairs(GlobalTrain.Systems) do
            SystemIterations[k] = (v.SubIterations or 1)
            maxIterations = math.max(maxIterations,(v.SubIterations or 1))
        end

        -- Create a schedule of simulation
        for iteration=1,maxIterations do
            GlobalTrain.Schedule[iteration] = {}
            -- Populate schedule
            for k,v in pairs(GlobalTrain.Systems) do
                if ((iteration)%(maxIterations/(v.SubIterations or 1))) == 0 then
                    table.insert(GlobalTrain.Schedule[iteration],v)
                end

            end
        end
    end
    GlobalTrain.Initialized = true
end
local msg_data
local msg_count = 0
local id,system,name,index,value
function DataExchange()
    -- Get data packets
    msg_count = TS.ThreadReadAvailable(_userdata)
    for i = 1, msg_count do
        msg_data = TS.ThreadRecvMessage(_userdata)
        if msg_data.message == 1 then
            local system_name = ffi.string(msg_data.system_name)
            if GlobalTrain.Systems[system_name] then
                GlobalTrain.Systems[system_name][ffi.string(msg_data.name)] = msg_data.value
            end
        end
        if msg_data.message == 3 then
            dataCache["wiresW"][msg_data.index] = msg_data.value
        end
        if msg_data.message == 4 then
            local system_name = ffi.string(msg_data.system_name)
            if GlobalTrain.Systems[system_name] then
                GlobalTrain.Systems[system_name]:TriggerInput(ffi.string(msg_data.name),msg_data.value)
            end
        end
        if msg_data.message == 5 then
            dataCache["wiresL"] = {}
        end
        if msg_data.message == 6 then
            local scr = [[
            local _retdata=""
            local print = function(...)
                for k,v in ipairs({...}) do _retdata = _retdata..tostring(v).."\t" end
                _retdata = _retdata.."\n"
            end
            ]]
            scr = scr..ffi.string(msg_data.system_name)..ffi.string(msg_data.name).."\n"
            scr = scr.."return _retdata"
            local data,err = loadstring(scr)
            if data then
                local ret = tostring(data()) or "N\\A"
                for i=0,math.ceil(#ret/63) do
                    TS.ThreadSendMessage(_userdata, 6, ret:sub(i*63,(i+1)*63-1), "",msg_data.index,i)
                end
            else
                print(err)
                TS.ThreadSendMessage(_userdata, 6, tostring(err), "",msg_data.index,0)
            end
            --Turbostroi.SendMessage(train,6,cmd:sub(1,255),cmd:sub(256,511),ply:UserID(),0)
        end
    end
    for twid,value in pairs(dataCache["wiresW"]) do
        GlobalTrain.TrainWires[twid] = value
    end

    -- Output all variable values
    for sys_name,system in pairs(GlobalTrain.Systems) do
        if system.OutputsList and (not system.DontAccelerateSimulation) then
            for _,name in pairs(system.OutputsList) do
                local value = (system[name] or 0)
                --if type(value) == "boolean" then value = value and 1 or 0 end
                if not dataCache[sys_name] then print(sys_name) end
                if dataCache[sys_name][name] ~= value then
                    --print(sys_name,name,value)
                    --if SendMessage(1,sys_name,name,0,tonumber(value) or 0) then -- OLD API
                    if TS.ThreadSendMessage(_userdata, 1, sys_name , name, 0, tonumber(value) or 0) then -- NEW API
                        dataCache[sys_name][name] = value
                    end
                end
            end
        end
    end

    -- Output train wire writes
    for twID,value in pairs(GlobalTrain.WriteTrainWires) do
        --local value = tonumber(value) or 0
        if dataCache["wires"][twID] ~= value then
            dataCache["wires"][twID] = value
            dataCache["wiresL"][twID] = false
        end
        if not dataCache["wiresL"][twID] or dataCache["wiresL"][twID]~=GlobalTrain.PrevTime then
            --SendMessage(3,"","on",tonumber(twID) or 0,dataCache["wires"][twID]) -- OLD API
            TS.ThreadSendMessage(_userdata, 3, "", "on", tonumber(twID) or 0, dataCache["wires"][twID]) -- NEW API
            --print("[!]Wire "..twID.." starts update! Value "..dataCache["wires"][twID])
        end
        GlobalTrain.WriteTrainWires[twID] = nil
        dataCache["wiresL"][twID] = CurTime()
    end
    for twID,time in pairs(dataCache["wiresL"]) do
        if time~=CurTime() then
            TS.ThreadSendMessage(_userdata,3, "", "off", tonumber(twID) or 0, 0)
            --print("[!]Wire "..twID.." stops update!")
            dataCache["wiresL"][twID] = nil
        end
    end
    --SendMessage(5,"","",0,0) -- OLD API
    --C.ThreadSendMessage(_userdata, 5,"","",0,0) -- NEW API
    --print(string.format("%s %s",count,#msgCache))
    --count = 0

end
