--[[

Run_Trains_V2.lua

Refactored version using LuaFileSystem (lfs)

Improvements:
- No index.txt (files discovered directly from directory)
- Automatic Data directory creation
- Atomic save (.tmp -> .tbl with .bak backup)
- Cleaner file handling
- Engine list fully local
- Safer modified tracking
- Simplified load/save UI
- Optional comment lines in .tbl files (#)

Author: Carl Ross, 2026

Included in the Remote Train Control distribution with permission.

]]

require("defines")
local lfs = require("lfs")

title = "Run trains V2"

---------------------------------------------------------------------
-- State
---------------------------------------------------------------------

local EngineList = {}
local modified = false

---------------------------------------------------------------------
-- Path handling
---------------------------------------------------------------------

local function getScriptDir()
    local info = debug.getinfo(1, "S")
    local path = info.source:sub(2)
    return path:match("^(.*[\\/])") or ""
end

local function getDataDir()
    return getScriptDir() .. "Data"
end

local function ensureDataDir()
    local dir = getDataDir()
    local attr = lfs.attributes(dir)

    if attr then
        return attr.mode == "directory"
    end

    local ok, err = lfs.mkdir(dir)
    if not ok then
        print("Failed to create Data directory:", err)
        return false
    end

    print("Created Data directory:", dir)
    return true
end

local function makeFilePath(name)
    return getDataDir() .. "\\" .. name .. ".tbl"
end

---------------------------------------------------------------------
-- Utility
---------------------------------------------------------------------

local function trim(s)
    return (s:gsub("^%s+", ""):gsub("%s+$", ""))
end

local function fileExists(path)
    local a = lfs.attributes(path)
    return a and a.mode == "file"
end

---------------------------------------------------------------------
-- File discovery (NO index file)
---------------------------------------------------------------------

local function listTableFiles()

    local files = {}

    for file in lfs.dir(getDataDir()) do
        if file ~= "." and file ~= ".." then
            local name = file:match("^(.*)%.tbl$")
            if name then
                table.insert(files, name)
            end
        end
    end

    table.sort(files)

    print("\nAvailable Train Lists:\n")

    if #files == 0 then
        print("  (none found)")
    else
        for i, name in ipairs(files) do
            local path = makeFilePath(name)
            local attr = lfs.attributes(path)

            local stamp = ""
            if attr and attr.modification then
                stamp = os.date("%Y-%m-%d %H:%M", attr.modification)
            end

            print(string.format("%2d  %-20s %s", i, name, stamp))
        end
    end

    print("\n0  Create New List\n")

    return files
end

---------------------------------------------------------------------
-- Parsing / validation
---------------------------------------------------------------------

local function parseInput(s)
    local engine, speed =
        s:match("(%-?%d+)%s*,%s*(%-?%d+)")

    if not engine then
        engine, speed =
            s:match("(%-?%d+)%s+(%-?%d+)")
    end

    if not engine then return nil end

    return tonumber(engine), tonumber(speed)
end

local function validate(engine, speed)
    if engine < 1 or engine > 120 or engine == 100 then
        return false, "Engine must be 1–120 (excluding 100)"
    end
    if speed < 1 or speed > 60 then
        return false, "Speed must be 1–60"
    end
    return true
end

---------------------------------------------------------------------
-- Engine list display/edit
---------------------------------------------------------------------

local function printList()
    for i, e in ipairs(EngineList) do
        print(string.format("Row %d  Engine %d  Speed %d",
            i, e.engine, e.speed))
    end
end

local function addEntries()
    while true do
        local s = InputQuery(
            "Engine data",
            "Enter engine, speed (blank to finish)",
            ""
        )

        if not s or s == "" then break end

        local engine, speed = parseInput(s)

        if not engine then
            print("Invalid input")
        else
            local ok, err = validate(engine, speed)
            if not ok then
                print(err)
            else
                table.insert(EngineList, {
                    engine = engine,
                    speed = speed
                })
                modified = true
            end
        end
    end
end

local function deleteEntry()
    printList()

    local s = InputQuery(
        "Delete",
        "Enter row number to delete",
        ""
    )

    if not s then return end

    local row = tonumber(s)
    if not row or not EngineList[row] then
        print("Invalid row")
        return
    end

    table.remove(EngineList, row)
    modified = true
end

---------------------------------------------------------------------
-- Load
---------------------------------------------------------------------

local function loadTable()

    local files = listTableFiles()
    if #files == 0 then return end

    local choice = InputQuery(
        "Load",
        "Select file number (0 = cancel):",
        "0"
    )

    if not choice then return end
    choice = tonumber(choice)

    if not choice or choice == 0 then return end
    if not files[choice] then return end

    local name = files[choice]
    local path = makeFilePath(name)

    local f = io.open(path, "r")
    if not f then
        print("Cannot open file")
        return
    end

    EngineList = {}

    for line in f:lines() do
        line = trim(line)

        -- allow comments
        if line ~= "" and not line:match("^#") then
            local engine, speed = parseInput(line)
            if engine and speed then
                table.insert(EngineList, {
                    engine = engine,
                    speed = speed
                })
            end
        end
    end

    f:close()

    print("Loaded:", name)
    modified = false
end

---------------------------------------------------------------------
-- Atomic save (safe + backup)
---------------------------------------------------------------------

local function saveTable()

    if not modified then return end

    local ans = InputQuery(
        "Save",
        "Save changes? (Y/N)",
        "Y"
    )

    if not ans or trim(ans):lower() ~= "y" then
        return
    end

    local fname = InputQuery(
        "File name",
        "Enter file name:",
        ""
    )

    if not fname or fname == "" then return end

    local base = trim(fname)
    local finalPath = makeFilePath(base)
    local tmpPath = finalPath .. ".tmp"
    local bakPath = finalPath .. ".bak"

    -- write temp file
    local f = io.open(tmpPath, "w")
    if not f then
        print("Cannot write temp file")
        return
    end

    f:write("# Run_Trains V2 file: ", base, "\n")
    f:write("# Saved: ", os.date(), "\n\n")

    for _, e in ipairs(EngineList) do
        f:write(e.engine, ",", e.speed, "\n")
    end

    f:close()

    -- rotate backup
    if fileExists(finalPath) then
        os.remove(bakPath)
        os.rename(finalPath, bakPath)
    end

    -- promote temp to final
    os.rename(tmpPath, finalPath)

    print("Saved:", finalPath)
    modified = false
end

---------------------------------------------------------------------
-- Main setup
---------------------------------------------------------------------

function setup()

    if not ensureDataDir() then
        return false
    end

    -- choose file or new
    local files = listTableFiles()

    local choice = InputQuery(
        "Startup",
        "Select file number or 0 for new list:",
        "0"
    )

    if choice and tonumber(choice) and tonumber(choice) > 0 then
        local n = tonumber(choice)
        if files[n] then
            local path = makeFilePath(files[n])

            local f = io.open(path, "r")
            if f then
                EngineList = {}

                for line in f:lines() do
                    line = trim(line)
                    if line ~= "" and not line:match("^#") then
                        local engine, speed = parseInput(line)
                        if engine then
                            table.insert(EngineList, {
                                engine = engine,
                                speed = speed
                            })
                        end
                    end
                end

                f:close()
                print("Loaded:", files[n])
            end
        end
    else
        addEntries()
    end

    -- review loop
    while true do

        print("\nCurrent Engine List:")
        printList()

        local ans = InputQuery(
            "Confirm",
            "Are these entries OK? (Y/N)",
            "Y"
        )

        if not ans then return false end

        ans = trim(ans):lower()

        if ans == "y" then
            break
        elseif ans == "n" then

            local act = InputQuery(
                "Edit",
                "Delete or Add? (D/A)",
                "D"
            )

            if act then
                act = trim(act):lower()

                if act == "d" then
                    deleteEntry()
                elseif act == "a" then
                    addEntries()
                end
            end
        end
    end

    -- final run
    print("\nFinal Engine List:")
    printList()

    saveTable()

    for i, entry in ipairs(EngineList) do

        local engine = entry.engine
        local speed = entry.speed

        if IsActive(engine) then
            local ti = GetTIUNo(engine)
            SetSpeed(speed, 0, engine, ti)
        else
            print("Engine not active:", engine)
        end
    end

    return true
end

---------------------------------------------------------------------
-- Cleanup
---------------------------------------------------------------------

function cleanup()

    print("cleanup() - shutting down engines")
    ShutDownActiveEngines(5)

    return true
end