-- 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 motors = 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.maxGearIndex = 1
M.minGearIndex = -1
M.throttle = 0
M.brake = 0
M.clutchRatio = 1
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.flywheelTorque = 0
M.gearboxTorque = 0

M.ignition = true
M.isEngineRunning = 0

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

M.energyStorages = {}

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

local regenHandling = {
  coastRegenCoef = 0.2, --mod coast instead of default
  brakeRegenCoef = 0.3,
  creep = 0, --mod creep
  noCreepBrakeThreshold = 0.01, --mod creep
  throttleOffset = 0, --mod throttleOffset
  throttleOffsetCreepSpeed = 0, --mod throttleOffset
  throttleOffsetCoastdownSpeed = 0, --mod throttleOffset
  regenMaxSideAccel = 10, --mod reduce regen gradually with lateral g
  brakelightsRegenThreshold = 0.5 --mod regen brakelights
}

local function getGearName()
  return automaticHandling.mode
end

local function getGearPosition()
  return (automaticHandling.modeIndex - 1) / (#automaticHandling.modes - 1)
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

  input.event("parkingbrake", 0, FILTER_DIRECT)

  local motorDirection = 1 --D
  if automaticHandling.mode == "P" then
    motorDirection = 0
    M.brake = max(M.brake, M.gearboxHandling.arcadeAutoBrakeAmount)
  elseif automaticHandling.mode == "N" then
    motorDirection = 0
  elseif automaticHandling.mode == "R" then
    motorDirection = -1
  end

  for _, v in ipairs(motors) do
    v.motorDirection = motorDirection
  end

  M.isSportModeActive = automaticHandling.mode == "S"
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()
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()
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
      guihooks.message({ txt = "vehicle.drivetrain.cannotShiftAuto", context = { mode = desiredMode } }, 2, "vehicle.shiftLogic.cannotShift")
    end
    desiredMode = "N"
  end
  automaticHandling.mode = desiredMode

  applyGearboxMode()
end

local function updateExposedData()
  local motorCount = 0
  M.rpm = 0
  local load = 0
  local motorTorque = 0
  for _, v in ipairs(motors) do
    M.rpm = max(M.rpm, abs(v.outputAV1) * constants.avToRPM)
    load = load + (v.engineLoad or 0)
    motorTorque = motorTorque + (v.outputTorque1 or 0)
    motorCount = motorCount + 1
  end
  load = load / motorCount

  M.smoothedAvgAVInput = sharedFunctions.updateAvgAVDeviceCategory("engine")
  M.waterTemp = 0
  M.oilTemp = 0
  M.checkEngine = 0
  M.ignition = electrics.values.ignitionLevel > 1
  M.engineThrottle = M.throttle
  M.engineLoad = load
  M.ignition = electrics.values.ignitionLevel > 1
  M.engineTorque = motorTorque
  M.flywheelTorque = motorTorque
  M.gearboxTorque = motorTorque
  M.isEngineRunning = 1
end

local function updateInGearArcade(dt)
  M.throttle = M.inputValues.throttle
  M.brake = M.inputValues.brake
  M.isArcadeSwitched = false
  M.clutchRatio = 1
  local motorCount = 0

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

  -- neutral gear handling
  if 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.5 and gearIndex > -1 then
        gearIndex = -1
        M.timer.neutralSelectionDelayTimer = M.timerConstants.neutralSelectionDelay
        automaticHandling.mode = "R"
        applyGearboxMode()
      end
    end

    if electrics.values.ignitionLevel <= 1 and automaticHandling.mode ~= "P" then
      gearIndex = 0
      M.timer.neutralSelectionDelayTimer = M.timerConstants.neutralSelectionDelay
      automaticHandling.mode = "P"
      applyGearboxMode()
    end
  end
  
  local creepThrottle = regenHandling.creep * clamp(1 - electrics.values.wheelspeed * constants.avToRPM / regenHandling.throttleOffsetCreepSpeed - max(M.brake,input.parkingbrake) / regenHandling.noCreepBrakeThreshold,0,1) --mod creep
  local throttleOffset = regenHandling.throttleOffset * clamp(electrics.values.wheelspeed * constants.avToRPM / (regenHandling.throttleOffsetCoastdownSpeed - regenHandling.throttleOffsetCreepSpeed) + regenHandling.throttleOffsetCreepSpeed / (regenHandling.throttleOffsetCreepSpeed - regenHandling.throttleOffsetCoastdownSpeed),0,1) --mod throttleOffset
  local regenThrottle = M.throttle <= throttleOffset and min(regenHandling.coastRegenCoef * clamp(throttleOffset - M.throttle,0,1) + M.brake * regenHandling.brakeRegenCoef, 1) or 0 --mod coast regen
  local emergencyBrakeCoef = clamp(1.5 - M.brake,0,1) --mod regen soft cut
  local escCoef = electrics.values.escActive and 0 or 1
  local steeringCoef = clamp((regenHandling.regenMaxSideAccel - abs(sensors.gx2)) / regenHandling.regenMaxSideAccel,0,1) --mod reduce regen gradually with lateral g
  electrics.values.regenThrottle = regenThrottle * escCoef * steeringCoef * emergencyBrakeCoef
  electrics.values.creepThrottle = creepThrottle --mod creep
  if electrics.values.regenThrottle >= regenHandling.brakelightsRegenThreshold and automaticHandling.mode == "D" then --mod regen brakelights
    M.brake = max(M.brake,0.001)
  end

  for _, motor in ipairs(powertrain.getDevicesByType("electricMotorLogicmod")) do --mod logicmod regen blend
    regenTorque = (clamp( - motor.actualTorque * fsign(motor.outputAV1) / motor.maxWantedRegenTorque,0,1) or 0) --mod logicmod display
    motorCount = motorCount + 1
  end
  if M.brake >= 0.01 and automaticHandling.mode == "D" and regenHandling.regenBrakeBlend >= 0.01 and electrics.values.wheelspeed * constants.avToRPM >= regenHandling.throttleOffsetCreepSpeed then
    --M.brake = clamp(M.brake * (1 + regenHandling.regenBrakeBlend * regenTorque) - regenHandling.regenBrakeBlend * regenTorque,0.01,1) --mod which function is better?
    M.brake = clamp((M.brake - 1) / (1 - regenHandling.regenBrakeBlend * regenTorque) + 1,0.01,1) --mod which function is better?
  end --mod regen blend

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

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

  if electrics.values.ignitionLevel <= 1 and automaticHandling.mode ~= "P" then
    M.timer.neutralSelectionDelayTimer = M.timerConstants.neutralSelectionDelay
    automaticHandling.mode = "P"
    applyGearboxMode()
  end

  local motorCount = 0
  local regenTorque = 0
  local creepThrottle = regenHandling.creep * clamp(1 - electrics.values.wheelspeed * constants.avToRPM / regenHandling.throttleOffsetCreepSpeed - max(M.brake,input.parkingbrake) / regenHandling.noCreepBrakeThreshold,0,1) --mod creep
  local throttleOffset = regenHandling.throttleOffset * clamp(electrics.values.wheelspeed * constants.avToRPM / (regenHandling.throttleOffsetCoastdownSpeed - regenHandling.throttleOffsetCreepSpeed) + regenHandling.throttleOffsetCreepSpeed / (regenHandling.throttleOffsetCreepSpeed - regenHandling.throttleOffsetCoastdownSpeed),0,1) --mod throttleOffset
  local regenThrottle = M.throttle <= throttleOffset and min(regenHandling.coastRegenCoef * clamp(throttleOffset - M.throttle,0,1) + M.brake * regenHandling.brakeRegenCoef, 1) or 0 --mod coast regen
  local emergencyBrakeCoef = clamp(1.5 - M.brake,0,1) --mod regen soft cut
  local escCoef = electrics.values.escActive and 0 or 1
  local steeringCoef = clamp((regenHandling.regenMaxSideAccel - abs(sensors.gx2)) / regenHandling.regenMaxSideAccel,0,1) --mod reduce regen gradually with lateral g
  electrics.values.regenThrottle = regenThrottle * escCoef * steeringCoef * emergencyBrakeCoef
  electrics.values.creepThrottle = creepThrottle --mod creep
  if electrics.values.regenThrottle >= regenHandling.brakelightsRegenThreshold and automaticHandling.mode == "D" then --mod regen brakelights
    M.brake = max(M.brake,0.001)
  end

  for _, motor in ipairs(powertrain.getDevicesByType("electricMotorLogicmod")) do --mod logicmod regen blend
    regenTorque = (clamp( - motor.actualTorque * fsign(motor.outputAV1) / motor.maxWantedRegenTorque,0,1) or 0) --mod logicmod display
    motorCount = motorCount + 1
  end
  if M.brake >= 0.01 and automaticHandling.mode == "D" and regenHandling.regenBrakeBlend >= 0.01 and electrics.values.wheelspeed * constants.avToRPM >= regenHandling.throttleOffsetCreepSpeed then
    --M.brake = clamp(M.brake * (1 + regenHandling.regenBrakeBlend * regenTorque) - regenHandling.regenBrakeBlend * regenTorque,0.01,1) --mod which function is better?
    M.brake = clamp((M.brake - 1) / (1 - regenHandling.regenBrakeBlend * regenTorque) + 1,0.01,1) --mod which function is better?
  end --mod regen blend

  local gearIndex = automaticHandling.gearIndexLookup[automaticHandling.mode]
  M.currentGearIndex = (automaticHandling.mode == "N" or automaticHandling.mode == "P") and 0 or gearIndex
  if automaticHandling.mode == "P" then
    M.brake = max(M.brake, M.gearboxHandling.arcadeAutoBrakeAmount)
  end
  updateExposedData()
end

local function sendTorqueData()
  for _, v in ipairs(motors) do
    v:sendTorqueData()
  end
end

local function setIgnition(enabled)
  for _, motor in ipairs(motors) do
    motor:setIgnition(enabled and 1 or 0)
  end
end

local function init(jbeamData, sharedFunctionTable)
  sharedFunctions = sharedFunctionTable

  M.currentGearIndex = 0
  M.throttle = 0
  M.brake = 0
  M.clutchRatio = 1

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

  motors = {}
  local motorNames = jbeamData.motorNames or { "mainMotor" }
  for _, v in ipairs(motorNames) do
    local motor = powertrain.getDevice(v)
    if motor then
      M.maxRPM = max(M.maxRPM, motor.maxAV * constants.avToRPM)
      table.insert(motors, motor)
    end
  end

  if #motors <= 0 then
    log("E", "shiftLogic-electricMotor", "No motors have been specified, functionality will be limited!")
  end

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

  automaticHandling.modes = {}
  automaticHandling.modeIndexLookup = {}
  local modes = jbeamData.automaticModes or "PRND"
  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 = 1
  automaticHandling.minGearIndex = -1

  M.idleRPM = 0
  M.maxGearIndex = automaticHandling.maxGearIndex
  M.minGearIndex = abs(automaticHandling.minGearIndex)
  M.energyStorages = sharedFunctions.getEnergyStorages(motors)

  regenHandling.coastRegenCoef = jbeamData.coastRegenCoef or 0.2 --mod coast instead of default
  regenHandling.brakeRegenCoef = jbeamData.brakeRegenCoef or 0.3
  regenHandling.throttleOffset = jbeamData.throttleOffset or 0   --mod throttleOffset
  regenHandling.throttleOffsetCreepSpeed = jbeamData.throttleOffsetCreepSpeed or 0   --mod throttleOffset
  regenHandling.throttleOffsetCoastdownSpeed = jbeamData.throttleOffsetCoastdownSpeed or 0   --mod throttleOffset
  regenHandling.creep = jbeamData.creep or 0 --mod creep
  regenHandling.noCreepBrakeThreshold = jbeamData.noCreepBrakeThreshold or 0.01 --mod creep
  regenHandling.regenMaxSideAccel = jbeamData.regenMaxSideAccel or 10 --mod reduce regen gradually with lateral g
  regenHandling.brakelightsRegenThreshold = jbeamData.brakelightsRegenThreshold or 0.5 --mod regen brakelights
  regenHandling.regenBrakeBlend = jbeamData.regenBrakeBlend or 0 --mod regen brake blend
  
  applyGearboxMode()
end

local function getState()
  local data = {grb_mde = automaticHandling.mode}

  return tableIsEmpty(data) and nil or data
end

local function setState(data)
  if data.grb_mde then
    automaticHandling.mode = data.grb_mde
    automaticHandling.modeIndex = automaticHandling.modeIndexLookup[automaticHandling.mode]
    applyGearboxMode()
  end
end

M.init = init

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

M.getState = getState
M.setState = setState

return M
