-- OE ProTune ECU (BeamNG) - universal auto-attach
-- Loads on every vehicle. Any compatible combustion vehicle (NA or turbo) gets
-- the trainer automatically, so players only need to install the mod.

local M = {}

-- Friendly names for Orlando / D&O community cars; everything else gets a
-- generic prettified model name.
local CUSTOM_NAMES = {
  docivic4 = "Honda Civic Wagon RT4WD",
  civek = "Honda Civic EK",
  dragcivek = "Honda Civic EK (Drag)",
  eg6fs = "Honda Civic EG6",
  civic_ej1 = "Honda Civic EJ1 Coupe",
  civicef = "Honda Civic EF",
  do_crx = "Honda CRX",
  DOmirage = "Mitsubishi Mirage",
  ARs2k = "Honda S2000",
  hon_prelude_si_94 = "Honda Prelude Si"
}

local tuner = nil
local decided = false
local attempts = 0
local MAX_ATTEMPTS = 300

local function vehicleModelId()
  local data = v and v.data or {}
  local model = data.model or ""
  if model ~= "" then return model end
  local dir = tostring(data.vehicleDirectory or ""):gsub("\\", "/"):gsub("^vehicles/", "")
  return dir:match("^([^/]+)") or ""
end

local function prettyName(id)
  id = tostring(id or "")
  if id == "" then return "BeamNG Vehicle" end
  local words = {}
  for word in id:gsub("[_%-%./]+", " "):gmatch("%S+") do
    words[#words + 1] = word:sub(1, 1):upper() .. word:sub(2)
  end
  return #words > 0 and table.concat(words, " ") or "BeamNG Vehicle"
end

-- BeamNG 0.39 turned information.name into a LOCALISATION TABLE:
--   name = { txt = "ui.vehicleconfig.information.name.BRAND BRAND",
--            ctx = { brand1 = "ui.vehicleconfig.brand.Bruckell",
--                    brand2 = "ui.vehicleconfig.brand.Moonhawk" } }
--
-- The old STRING form ("Bruckell Moonhawk") is what every saved tune is filed under: app.js
-- durPath() keys settings/oeProTune/<carName>.json on this name, and so do the autosave
-- (oep_autosave_<carName>), dyno-baseline, power-history and MAP-bank-cache localStorage keys, plus
-- the controller's strict matchesIdentity() string compare. Once the `type(name) == "string"` check
-- started failing on the new table, carName silently fell through to prettyName(model) ("Moonhawk")
-- and all five of those keys pointed somewhere new - users reported every tune gone after updating.
-- Nothing was ever lost; the files were simply orphaned under the old name.
--
-- Rebuild the historical string from the ctx KEYS (last dot-segment), never the translations, so the
-- result is byte-identical in every game language. Verified against a real user's folder: this
-- reproduces Bruckell_Moonhawk, Bruckell_Bastion, Gavril_D_Series, Gavril_Barstow, Gavril_Roamer,
-- ETK_800_Series and Ibishu_BX_Series exactly.
local function localisedNameToString(name)
  if type(name) == "string" then return name ~= "" and name or nil end
  if type(name) ~= "table" then return nil end
  local ctx = name.ctx
  if type(ctx) ~= "table" then return nil end
  local parts, i = {}, 1
  while true do
    local seg = ctx["brand" .. i]
    if type(seg) ~= "string" or seg == "" then break end
    parts[#parts + 1] = seg:match("([^%.]+)$") or seg
    i = i + 1
  end
  if #parts == 0 then return nil end
  return table.concat(parts, " ")
end

local function vehicleDisplayName(model)
  if CUSTOM_NAMES[model] then return CUSTOM_NAMES[model] end
  local data = v and v.data or {}
  local info = data.information
  if type(info) == "table" then
    -- each candidate is tried properly: the old code short-circuited on a truthy TABLE at info.name
    -- and then failed its own string test, so Name/model/title were never reached.
    local name = localisedNameToString(info.name) or localisedNameToString(info.Name)
      or localisedNameToString(info.model) or localisedNameToString(info.title)
    if name then return name end
  end
  return prettyName(model)
end

local function isTunableEngine(device)
  return device
    -- COMBUSTION ENGINE ONLY: intakeAirDensityCoef is the field the tuner multiplies, and only
    -- combustion engines have it. Electric / hydraulics motors (trailer legs + tailgates, EVs, cranes)
    -- also expose setTempRevLimiter / maxAV / idleAV (and idleAV=0 is TRUTHY in Lua, so the old idle
    -- check passed), so without this guard they got mis-detected as engines and wrapping them crashed
    -- the whole vehicle ("corrupted" trailer). Require the combustion field and the problem is gone.
    and type(device.intakeAirDensityCoef) == "number"
    and device.setTempRevLimiter
    and (device.revLimiterAV or device.maxAV)
    and (device.idleRPM or device.idleAV)
end

local function findEngine()
  if not (powertrain and powertrain.getDevice) then return nil end
  if isTunableEngine(powertrain.getDevice("mainEngine")) then return "mainEngine" end
  if powertrain.getDeviceNames then
    local names = powertrain.getDeviceNames()
    if type(names) == "table" then
      for _, name in pairs(names) do
        if type(name) == "string" and isTunableEngine(powertrain.getDevice(name)) then return name end
      end
    end
  end
  if powertrain.getDevices then
    local devices = powertrain.getDevices()
    if type(devices) == "table" then
      for name, device in pairs(devices) do
        if type(name) == "string" and isTunableEngine(device) then return name end
      end
    end
  end
  return nil
end

local function tryAttach()
  attempts = attempts + 1
  -- powertrain may not be ready on the very first frames; retry for a while
  local engineName = findEngine()
  if not engineName then
    if attempts >= MAX_ATTEMPTS then decided = true end
    return
  end
  decided = true
  rawset(_G, "oeProTuneExt", nil)

  -- jbeam-hooked cars already run the controller directly; don't double up
  if controller and controller.getController and controller.getController("oeProTune") then return end

  -- friendly heads-up: both this trainer and the OrlandoEdits tuner are global
  -- engine controllers. Running both at once means two tunes fighting over the
  -- same engine. Warn, then take over.
  if rawget(_G, "oeTunerExt") and log then
    log("W", "oeProTune", "OrlandoEdits Tuner is also attached to this vehicle. Disable one of the two tuners to avoid conflicting engine control.")
  end

  local ok, mod = pcall(require, "controller/oeProTune")
  if not ok or not mod then
    if log then log("E", "oeProTune", "failed to load controller/oeProTune: " .. tostring(mod)) end
    return
  end

  local model = vehicleModelId()
  tuner = mod
  -- carNameFallback is the name a build with the 0.39 bug would have produced. The UI reads the
  -- durable tune file under carName first and falls back to this, so a player who only ever ran the
  -- broken build (or who re-tuned in the window before this fix) still finds their work instead of
  -- having it orphaned in the opposite direction. Nil when the two would be identical.
  local primary = vehicleDisplayName(model)
  local fallback = prettyName(model)
  tuner.init({
    carName = primary,
    carNameFallback = (fallback ~= primary) and fallback or nil,
    engineName = engineName,
  })
  rawset(_G, "oeProTuneExt", tuner)
end

local function updateGFX(dt)
  if not decided then tryAttach() end
  if tuner and tuner.updateGFX then tuner.updateGFX(dt) end
end

local function onReset()
  if tuner and tuner.reset then tuner.reset() end
end

M.updateGFX = updateGFX
M.onReset = onReset

return M
