-- This Source Code Form is subject to the terms of the bCDDL, v. 1.1.
-- If a copy of the bCDDL was not distributed with this
-- file, You can obtain one at http://beamng.com/bCDDL-1.1.txt

local M = {}

local max = math.max
local min = math.min
local abs = math.abs

local constants = {rpmToAV = 0.104719755, avToRPM = 9.549296596425384}

local newDesiredGearIndex = 0
local previousGearIndex = 0
local gearbox = nil
local engine = nil
local clutch = nil
local secondaryGearbox = nil

local sharedFunctions = nil
local gearboxAvailableLogic = nil
local gearboxLogic = nil

M.gearboxHandling = nil
M.timer = nil
M.timerConstants = nil
M.inputValues = nil
M.shiftPreventionData = nil
M.shiftBehavior = nil
M.smoothedValues = nil

M.currentGearIndex = 0
M.throttle = 0
M.brake = 0
M.throttleInput = 0
M.isArcadeSwitched = false
M.isSportModeActive = false

M.smoothedAvgAVInput = 0
M.rpm = 0
M.idleRPM = 0
M.maxRPM = 0

M.engineThrottle = 0
M.engineLoad = 0
M.engineTorque = 0
M.gearboxTorque = 0

M.ignition = true
M.isEngineRunning = 0

M.oilTemp = 0
M.waterTemp = 0
M.checkEngine = false

M.energyStorages = {}

local secondaryGearboxHandling = {
	timeSinceShift = 0
}

local automaticHandling = {
  availableModes = {"P","R", "N", "D", "L"},
  hShifterModeLookup = {[-1] = "R", [0] = "N", "P", "D", "L"},
  ecvtGearIndexLookup = {P = -2, R = -1, N = 0, D = 1, L = 2},
  availableModeLookup = {},
  existingModeLookup = {},
  modeIndexLookup = {},
  modes = {},
  mode = nil,
  modeIndex = 0,
  maxAllowedGearIndex = 0,
  minAllowedGearIndex = 0,
}

local cvtHandling = {
  aggression = 0.5,
  highAV = 0,
  lowAV = 0,
}

local smoother = {
  --gearRatio smoother reduces oscillations during sharp changes in throttle
  cvtGearRatioSmoother = nil,
  --target smoother represents "driver intent"
  cvtTargetAVSmoother = nil,
  --input engagement
  inputEngageSmoother = nil,
}

local torqueConverterHandling = {
  lockupAV = 0,
  lockupRange = 0,
  lockupMinGear = 0,
  hasLockup = false,
}

local function getGearName()
  local modePrefix = ""
  if automaticHandling.mode == "S" then
    modePrefix = "S"
  elseif type(automaticHandling.mode) == "number" then
    modePrefix = "M"
  end
  return automaticHandling.mode
end

local function getGearPosition()
  return (automaticHandling.modeIndex - 1) / (#automaticHandling.modes - 1)
end

local function applyGearboxModeRestrictions()
  local manualModeIndex
  if string.sub(automaticHandling.mode, 1,1) == "M" then
    manualModeIndex = string.sub(automaticHandling.mode, 2)
  end
  local maxGearIndex = gearbox.maxGearIndex
  local minGearIndex = gearbox.minGearIndex
  if automaticHandling.mode == "1" then
    maxGearIndex = 1
    minGearIndex = 1
  elseif automaticHandling.mode == "2" then
    maxGearIndex = 2
    minGearIndex = 1
  elseif manualModeIndex then
    maxGearIndex = manualModeIndex
    minGearIndex = manualModeIndex
  end

  automaticHandling.maxGearIndex = maxGearIndex
  automaticHandling.minGearIndex = minGearIndex
end

local function gearboxBehaviorChanged(behavior)
  gearboxLogic = gearboxAvailableLogic[behavior]
  M.updateGearboxGFX = gearboxLogic.inGear
  M.shiftUp = gearboxLogic.shiftUp
  M.shiftDown = gearboxLogic.shiftDown
  M.shiftToGearIndex = gearboxLogic.shiftToGearIndex
end

local function applyGearboxMode()
  local autoIndex = automaticHandling.modeIndexLookup[automaticHandling.mode]
  if autoIndex then
    automaticHandling.modeIndex = min(max(autoIndex, 1), #automaticHandling.modes)
    automaticHandling.mode = automaticHandling.modes[automaticHandling.modeIndex]
  end

  if automaticHandling.mode == "P" then
    gearbox:setMode("park")
  elseif automaticHandling.mode == "N" then
    gearbox:setMode("neutral")
  elseif automaticHandling.mode == "R" then
    gearbox:setMode("reverse")
  else
    gearbox:setMode("drive")
  end

  M.isSportModeActive = automaticHandling.mode == "S"
  
  if secondaryGearbox then
	if secondaryGearbox.gearIndex < 1 then
		secondaryGearbox:setGearIndex(1)
	end
	
	if secondaryGearbox.mode ~= "drive" then
		secondaryGearbox:setMode("drive")
	end
  end
end

local function shiftUp()
  if automaticHandling.mode == "N" then
    M.timer.gearChangeDelayTimer = M.timerConstants.gearChangeDelay
  end

  automaticHandling.modeIndex = min(automaticHandling.modeIndex + 1, #automaticHandling.modes)
  automaticHandling.mode = automaticHandling.modes[automaticHandling.modeIndex]

  applyGearboxMode()
  applyGearboxModeRestrictions()
end

local function shiftDown()
  if automaticHandling.mode == "N" then
    M.timer.gearChangeDelayTimer = M.timerConstants.gearChangeDelay
  end

  automaticHandling.modeIndex = max(automaticHandling.modeIndex - 1, 1)
  automaticHandling.mode = automaticHandling.modes[automaticHandling.modeIndex]

  applyGearboxMode()
  applyGearboxModeRestrictions()
end

local function shiftToGearIndex(index)
  local desiredMode = automaticHandling.hShifterModeLookup[index]
  if not desiredMode or not automaticHandling.existingModeLookup[desiredMode] then
    if desiredMode and not automaticHandling.existingModeLookup[desiredMode] then
      gui.message({txt = "vehicle.drivetrain.cannotShiftAuto", context = {mode = desiredMode}}, 2, "vehicle.shiftLogic.cannotShift")
    end
    desiredMode = "N"
  end
  automaticHandling.mode = desiredMode

  applyGearboxMode()
  applyGearboxModeRestrictions()
end

local function updateExposedData()
  M.rpm = engine and (engine.outputAV1 * constants.avToRPM) or 0
  M.smoothedAvgAVInput =type(sharedFunctions.updateAvgAVSingleDevice) == "function" and sharedFunctions.updateAvgAVSingleDevice("gearbox") or 0
  M.waterTemp = (engine and engine.thermals) and (engine.thermals.coolantTemperature and engine.thermals.coolantTemperature or engine.thermals.oilTemperature) or 0
  M.oilTemp = (engine and engine.thermals) and engine.thermals.oilTemperature or 0
  M.checkEngine = engine and (engine.isDisabled and not engine.isInIdleStop) or false
  M.ignition = engine and not engine.isDisabled or false
  M.engineThrottle = (engine and engine.isDisabled) and 0 or M.throttle
  M.engineLoad = engine and (engine.isDisabled and 0 or engine.instantEngineLoad) or 0
  M.running = engine and not engine.isDisabled or false
  M.engineTorque = engine and engine.combustionTorque or 0
  M.gearboxTorque = gearbox and gearbox.outputTorque or 0
  M.isEngineRunning = engine and ((engine.isStalled or engine.ignitionCoef <= 0) and 0 or 1) or 1
end

local function updateInputClutch(dt)
  if gearbox and clutch then
    -- Handle gearbox input engaging
    
    local targetInputClutchRatio = gearbox.inputEngaged and 1 or 0
    local inputClutchRatio = smoother.inputEngageSmoother:get(targetInputClutchRatio, dt)
    
    electrics.values[clutch.electricsClutchRatioName] = inputClutchRatio
  end
end

local function updateInGearArcade(dt)
  M.throttle = M.inputValues.throttle
  M.brake = M.inputValues.brake
  M.isArcadeSwitched = false

  local gearIndex = automaticHandling.ecvtGearIndexLookup[automaticHandling.mode]
  -- driving backwards? - only with automatic shift - for obvious reasons ;)
  if (gearIndex < 0 and M.smoothedValues.avgAV <= 0.15) or (gearIndex <= 0 and M.smoothedValues.avgAV < -1) then
    M.throttle, M.brake = M.brake, M.throttle
    M.isArcadeSwitched = true
  end

  electrics.values.lockupClutchRatio = 0

  -- neutral gear handling
  if abs(gearbox.gearIndex) <= 1 and M.timer.neutralSelectionDelayTimer <= 0 then
    if automaticHandling.mode ~= "P" and abs(M.smoothedValues.avgAV) < M.gearboxHandling.arcadeAutoBrakeAVThreshold and M.throttle <= 0 then
      M.brake = max(M.brake, M.gearboxHandling.arcadeAutoBrakeAmount)
    end

    if automaticHandling.mode ~= "N" and abs(M.smoothedValues.avgAV) < M.gearboxHandling.arcadeAutoBrakeAVThreshold and M.smoothedValues.throttle <= 0 then
      gearIndex = 0
      automaticHandling.mode = "N"
      applyGearboxMode()
    else
      if M.smoothedValues.throttleInput > 0 and M.inputValues.throttle > 0 and M.smoothedValues.brakeInput <= 0 and M.smoothedValues.avgAV > -1 and gearIndex < 1 then
        gearIndex = 1
        M.timer.neutralSelectionDelayTimer = M.timerConstants.neutralSelectionDelay
        automaticHandling.mode = "D"
        applyGearboxMode()
      end

      if M.smoothedValues.brakeInput > 0.1 and M.inputValues.brake > 0 and M.smoothedValues.throttleInput <= 0 and M.smoothedValues.avgAV <= 0.15 and gearIndex > -1  then
        gearIndex = -1
        M.timer.neutralSelectionDelayTimer = M.timerConstants.neutralSelectionDelay
        automaticHandling.mode = "R"
        applyGearboxMode()
      end
    end
  end

  --Arcade mode gets a "rev limiter" in case the engine does not have one
  if engine.outputAV1 > engine.maxAV and not engine.hasRevLimiter then
    M.throttle = 0
  end

  M.currentGearIndex = (automaticHandling.mode == "N" or automaticHandling.mode == "P") and 0 or gearIndex
  updateExposedData()
  updateInputClutch(dt)
end

local function updateInGear(dt)
  M.throttle = M.inputValues.throttle
  M.brake = M.inputValues.brake
  M.isArcadeSwitched = false

  local gearIndex = automaticHandling.ecvtGearIndexLookup[automaticHandling.mode]

  electrics.values.lockupClutchRatio = 0
  
  if secondaryGearbox and M.secondaryGearboxMinAV and M.secondaryGearboxMaxAV then
	secondaryGearboxHandling.timeSinceShift = secondaryGearboxHandling.timeSinceShift + dt
	
	if M.throttle > 0.01 and math.abs(secondaryGearbox.inputAV) > M.secondaryGearboxMaxAV and secondaryGearboxHandling.timeSinceShift > 1.0 then
		if secondaryGearbox.gearIndex < secondaryGearbox.maxGearIndex then
			secondaryGearbox:setGearIndex(secondaryGearbox.gearIndex + 1)
			timeSinceShift = 0
		end
	elseif M.throttle < 0.01 and math.abs(secondaryGearbox.inputAV) < M.secondaryGearboxMinAV and secondaryGearboxHandling.timeSinceShift > 1.0 then
		if secondaryGearbox.gearIndex > 1 then
			secondaryGearbox:setGearIndex(secondaryGearbox.gearIndex - 1)
			timeSinceShift = 0
		end
	end
  end

  M.currentGearIndex = (automaticHandling.mode == "N" or automaticHandling.mode == "P") and 0 or gearIndex
  updateExposedData()
  updateInputClutch(dt)
end

local function sendTorqueData()
  if engine then
    engine:sendTorqueData()
  end
end

local function init(jbeamData, sharedFunctionTable)
  sharedFunctions = sharedFunctionTable
  engine = powertrain.getDevice("mainEngine")
  gearbox = powertrain.getDevice("gearbox")
  clutch = powertrain.getDevice("clutch")
  secondaryGearbox = powertrain.getDevice("secondaryGearbox")
  newDesiredGearIndex = 0
  previousGearIndex = 0

  M.currentGearIndex = 0
  M.throttle = 0
  M.brake = 0
  M.clutchRatio = 0
  M.secondaryGearboxMinAV = jbeamData.secondaryGearboxMinAV
  M.secondaryGearboxMaxAV = jbeamData.secondaryGearboxMaxAV

  gearboxAvailableLogic = {
    arcade =
    {
      inGear = updateInGearArcade,
      shiftUp = sharedFunctions.warnCannotShiftSequential,
      shiftDown = sharedFunctions.warnCannotShiftSequential,
      shiftToGearIndex = sharedFunctions.switchToRealisticBehavior,
    },
    realistic =
    {
      inGear = updateInGear,
      shiftUp = shiftUp,
      shiftDown = shiftDown,
      shiftToGearIndex = shiftToGearIndex,
    }
  }

  local cvtGearRatioSmoothingIn = jbeamData.cvtGearRatioSmoothingIn or 20
  local cvtGearRatioSmoothingOut = jbeamData.cvtGearRatioSmoothingOut or 5
  local cvtTargetAVSmoothingIn = jbeamData.cvtTargetAVSmoothingIn or 0.5
  local cvtTargetAVSmoothingOut = jbeamData.cvtTargetAVSmoothingOut or 2
  local inputEngageSmoothingIn = jbeamData.inputEngageSmoothingIn or 5.0
  local inputEngageSmoothingOut = jbeamData.inputEngageSmoothingOut or 3.8

  smoother.cvtGearRatioSmoother = newTemporalSmoothingNonLinear(cvtGearRatioSmoothingIn, cvtGearRatioSmoothingOut)
  smoother.cvtTargetAVSmoother = newTemporalSmoothingNonLinear(cvtTargetAVSmoothingIn, cvtTargetAVSmoothingOut)
  smoother.inputEngageSmoother = newTemporalSmoothingNonLinear(inputEngageSmoothingIn, inputEngageSmoothingOut)

  automaticHandling.availableModeLookup = {}
  for _,v in pairs(automaticHandling.availableModes) do
    automaticHandling.availableModeLookup[v] = true
  end

  automaticHandling.modes = {}
  automaticHandling.modeIndexLookup = {}
  local modes = jbeamData.automaticModes or "PRNDL"
  local modeCount = #modes
  local modeOffset = 0
  for i = 1, modeCount do
    local mode = modes:sub(i,i)
    if automaticHandling.availableModeLookup[mode] then
      automaticHandling.modes[i + modeOffset] = mode
      automaticHandling.modeIndexLookup[mode] = i + modeOffset
      automaticHandling.existingModeLookup[mode] = true
    else
      print("unknown auto mode: "..mode)
    end
  end

  local defaultMode = jbeamData.defaultAutomaticMode or "N"
  automaticHandling.modeIndex = string.find(modes, defaultMode)
  automaticHandling.mode = automaticHandling.modes[automaticHandling.modeIndex]
  automaticHandling.maxGearIndex = gearbox.maxGearIndex
  automaticHandling.minGearIndex = gearbox.minGearIndex

  cvtHandling.aggression = jbeamData.cvtAggression or 0.5

  smoother.cvtGearRatioSmoother:set(gearbox.maxGearRatio)
  smoother.cvtTargetAVSmoother:set(cvtHandling.lowAV)
  smoother.inputEngageSmoother:set(gearbox.inputEngaged and 1 or 0)
  
  M.maxRPM = engine and engine.maxRPM or 0
  M.idleRPM = engine and engine.idleRPM or 0
  M.energyStorages = sharedFunctions.getEnergyStorages({engine})
  
  applyGearboxMode()
end

M.init = init

M.gearboxBehaviorChanged = gearboxBehaviorChanged
M.shiftUp = shiftUp
M.shiftDown = shiftDown
M.shiftToGearIndex = shiftToGearIndex
M.updateGearboxGFX = nop
M.getGearName = getGearName
M.getGearPosition = getGearPosition
M.sendTorqueData = sendTorqueData

return M