local CustomQuaternion = {}
CustomQuaternion.__index = CustomQuaternion

CustomQuaternion.__mul = function(q, r, out)
    out = out or q
    local qx, qy, qz, qw = q:unpack()
    local rx, ry, rz, rw = r:unpack()
    out.x = qx * rw + qw * rx + qy * rz - qz * ry
    out.y = qy * rw + qw * ry + qz * rx - qx * rz
    out.z = qz * rw + qw * rz + qx * ry - qy * rx
    out.w = qw * rw - qx * rx - qy * ry - qz * rz
    return out
end

function CustomQuaternion:new()
    local o = {
        x = 0,
        y = 0,
        z = 0,
        w = 0
    }
    setmetatable(o, CustomQuaternion)
    self.__index = self

    return o
end

CustomQuaternion.unpack = function(q)
    return q.x, q.y, q.z, q.w
end

function CustomQuaternion.fromAngleAxis(angle, x, y, z)
    return CustomQuaternion:new():setAngleAxis(angle, x, y, z)
end

CustomQuaternion.setAngleAxis = function(q, angle, x, y, z)
    local s = math.sin(angle * .5)
    local c = math.cos(angle * .5)
    q.x = x * s
    q.y = y * s
    q.z = z * s
    q.w = c
    return q
end

CustomQuaternion.length = function(q)
    return math.sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w)
end

CustomQuaternion.normalize = function(q)
    local l = q:length();

    if l == 0 then
        q.x = 0;
        q.y = 0;
        q.z = 0;
        q.w = 1;
    else
        l = 1 / l;
        q.x = q.x * l;
        q.y = q.y * l;
        q.z = q.z * l;
        q.w = q.w * l;
    end

    return q
end

---
CustomQuaternion.toRotationMatrix = function(q)
    local q0 = q.w
    local q1 = q.x
    local q2 = q.y
    local q3 = q.z

    local r00 = 2 * (q0 * q0 + q1 * q1) - 1
    local r01 = 2 * (q1 * q2 - q0 * q3)
    local r02 = 2 * (q1 * q3 + q0 * q2)

    local r10 = 2 * (q1 * q2 + q0 * q3)
    local r11 = 2 * (q0 * q0 + q2 * q2) - 1
    local r12 = 2 * (q2 * q3 - q0 * q1)

    local r20 = 2 * (q1 * q3 - q0 * q2)
    local r21 = 2 * (q2 * q3 + q0 * q1)
    local r22 = 2 * (q0 * q0 + q3 * q3) - 1

    return r00, r01, r02, r10, r11, r12, r20, r21, r22
end

local function clamp(v, min, max)
    if v < min then
        return min
    end
    if v > max then
        return max
    end
    return v
end

CustomQuaternion.toEulerAngles = function(m11, m12, m13, m21, m22, m23, m31, m32, m33)
    local y = math.asin(clamp(m13, -1, 1));
    local x = 0
    local z = 0

    if (math.abs(m13) < 0.9999999) then
        x = math.atan2(-m23, m33);
        z = math.atan2(-m12, m11);
    else
        x = math.atan2(m32, m22);
        z = 0
    end
    return math.deg(x), math.deg(y), math.deg(z)
end

CustomQuaternion.fromEulerAngles = function(x, y, z)
    local this = {}

    local c1 = math.cos(x / 2);
    local c2 = math.cos(y / 2);
    local c3 = math.cos(z / 2);

    local s1 = math.sin(x / 2);
    local s2 = math.sin(y / 2);
    local s3 = math.sin(z / 2);

    this._x = s1 * c2 * c3 + c1 * s2 * s3;
    this._y = c1 * s2 * c3 - s1 * c2 * s3;
    this._z = c1 * c2 * s3 + s1 * s2 * c3;
    this._w = c1 * c2 * c3 - s1 * s2 * s3;

    local resQ = CustomQuaternion:new()
    resQ.x = this._x
    resQ.y = this._y
    resQ.z = this._z
    resQ.w = this._w

    return resQ
end
-------------------------------------------------------------------------------------

local tempVector2 = Vector3f.new()
local wTransformFieldNum = nil
local targetFPS = 90
Heli_GlobalSpeed = 0
Heli_GlobalHeading = 0
-------------------------
local function getJavaFieldNum(object, fieldName)
    for i = 0, getNumClassFields(object) - 1 do
        local javaField = getClassField(object, i)
        if luautils.stringEnds(tostring(javaField), '.' .. fieldName) then
            return i
        end
    end
end

local function moveVehicle(vehicle, x_delta, y_delta, z_delta)
    if wTransformFieldNum == nil then
        wTransformFieldNum = getJavaFieldNum(vehicle, "jniTransform")
    end
    if wTransformFieldNum then
        local tmpTransform = getClassFieldVal(vehicle, getClassField(vehicle, wTransformFieldNum))
        local wTranform = vehicle:getWorldTransform(tmpTransform)
        local origin = getClassFieldVal(wTranform, getClassField(wTranform, 1))
        origin:set(origin:x() + x_delta, origin:y() + y_delta, origin:z() + z_delta)
        vehicle:setWorldTransform(wTranform)
    end
end
local function ReduceCondtion(vehicle)
    for i = 1, vehicle:getPartCount() do
        local part = vehicle:getPartByIndex(i - 1)
        local ConditionNow = part:getCondition()
        if ZombRand(1, 100) <= 60 then
            Panzer:sendVehicleCommandWrapper(getPlayer(), part, "setPartCondition", {
                condition = ConditionNow - ZombRand(4, 10)
            })
        end
    end
end

local function sqobject(sqa)
    local sq = sqa
    if sq == nil then
        return false;
    end
    local sqObjs = sq:getObjects();
    local sqSize = sqObjs:size();
    local tbl = {}
    for i = sqSize - 1, 0, -1 do
        local obj = sqObjs:get(i);
        table.insert(tbl, obj)
    end
    return sq, sqObjs, tbl
end

local function HasFlag(NewSuqare, FlagTable)
    local sq, sqObjs, objTbl = sqobject(NewSuqare)
    if objTbl then
        for i = 1, #objTbl do
            local obj = objTbl[i]
            local sprite = obj:getSprite()
            if sprite and sprite:getProperties() then
                for j = 1, #FlagTable do
                    if sprite:getProperties():Is(FlagTable[j]) then
                        return true
                    end
                end
            end
        end
    end
    return false
end
local function GetNowMaxZ(playerObj, curr_z)
    local MaxZ = 0
    local ZNow = math.floor(curr_z)
    while true do
        local sq = getCell():getGridSquare(playerObj:getX(), playerObj:getY(), ZNow)
        if HasFlag(sq, { IsoFlagType.solidfloor }) then
            MaxZ = ZNow
            break
        else
            ZNow = ZNow - 1
            if ZNow <= 0 then
                MaxZ = ZNow
                break
            end
        end
    end
    return MaxZ + 0.4
end

local function CheckViald(playerObj, Vehicle)
    if not playerObj then
        return false
    end
    if not Vehicle then
        return false
    end
    if not GetHeliType(Vehicle) then
        return false
    end
    local seat = Vehicle:getSeat(playerObj)
    if seat ~= 0 then
        return false
    end
    return true
end

local updatetick = 60

local function damagecar(vehicle)
    vehicle:playSound("JetAlarm")

    if SandboxVars.WT.FloorHit then
        ReduceCondtion(vehicle)
    end
end

local BanFlagList = { IsoFlagType.WallW, IsoFlagType.WallN, IsoFlagType.WallSE, IsoFlagType.WallNW }

local function Blocked(playerObj, direction, vehicle)
    local squaretable = {}
    local playerangle = playerObj:getForwardDirection():getDirection()

    local vx = vehicle:getX()
    local vy = vehicle:getY()
    local vz = math.floor(vehicle:getWorldPos(0, 0, 0, tempVector2):z())
    local RangeX1, RangeX2, RangeY1, RangeY2 = 0, 10, 0, 5
    if direction == "UP" then

    end
    if direction == "DOWN" then
        RangeX1, RangeX2, RangeY1, RangeY2 = 0, 10, 0, 5
        playerangle = playerangle + 60
    end
    if direction == "LEFT" then
        RangeX1, RangeX2, RangeY1, RangeY2 = 0, 10, 0, 5
        playerangle = playerangle - 90
    end
    if direction == "RIGHT" then
        playerangle = playerangle + 90
    end
    for z = RangeX1, RangeX2 do
        for i = RangeY1, RangeY2 do
            local vx1 = vx + z / 2 * math.cos(playerangle) + i / 3 * math.cos(playerangle + math.pi / 2)
            local vy1 = vy + z / 2 * math.sin(playerangle) + i / 3 * math.sin(playerangle + math.pi / 2)
            squaretable[getCell():getGridSquare(vx1, vy1, vz)] = 1
            local vx2 = vx + z / 2 * math.cos(playerangle) + i / 3 * math.cos(playerangle - math.pi / 2)
            local vy2 = vy + z / 2 * math.sin(playerangle) + i / 3 * math.sin(playerangle - math.pi / 2)
            squaretable[getCell():getGridSquare(vx2, vy2, vz)] = 1
        end
    end
    for k, v in pairs(squaretable) do
        local square = k
        if square then
            if HasFlag(square, BanFlagList) then
                if updatetick > 0 then
                    updatetick = updatetick - 1
                else
                    updatetick = 60 * getAverageFPS() / 60
                    damagecar(vehicle)
                end
                return true
            end
        end
        -- if isDebugEnabled() then
        --     local sqareX = k:getX()
        --     local sqareY = k:getY()
        --     local sqareZ = k:getZ()
        --     addAreaHighlight(sqareX, sqareY, sqareX + 1, sqareY + 1, sqareZ, 0.1, 0.8, 0.1, 0.1)
        -- end
    end
end

local LastLightSquare = nil
local Light = nil
local LastAngleY = 0
local isRotating = false

local function helicopterMovementUpdate()
    local playerObj = getPlayer()
    local Vehicle = playerObj:getVehicle()
    if not CheckViald(playerObj, Vehicle) then
        return
    end
    local fpsMultiplier = targetFPS / getAverageFPS()
    local curr_z = Vehicle:getWorldPos(0, 0, 0, tempVector2):z()
    if curr_z > 1 then
        if not playerObj:isGhostMode() then
            playerObj:setGhostMode(true)
        end

        playerObj:setZ(curr_z)
        -- local tmpTransform = getJavaFieldNum(playerObj, "BodyDamage")
        -- print(Vehicle:getSquare())
        -- print(SystemDisabler.setDoCharacterStats(false))
    else
        if playerObj:isGhostMode() then
            playerObj:setGhostMode(false)
        end
        playerObj:setZ(0)
    end
    local NowMaxZ = GetNowMaxZ(playerObj, curr_z)
    if not Vehicle:isEngineRunning() then
        if curr_z > NowMaxZ then
            local tempValue = -0.07
            tempValue = tempValue * fpsMultiplier
            moveVehicle(Vehicle, 0, tempValue, 0)
        end
        return
    end
    local modData = Vehicle:getModData()
    if not modData.NightLight then
        modData.NightLight = false
    end
    if modData.NightLight then
        local CurrentSquare = getCell():getGridSquare(playerObj:getX(), playerObj:getY(), NowMaxZ)
        if LastLightSquare ~= CurrentSquare then
            LastLightSquare = getCell():getGridSquare(playerObj:getX(), playerObj:getY(), NowMaxZ)
            if Light then
                getCell():removeLamppost(Light)
            end
            Light = IsoLightSource.new(playerObj:getX(), playerObj:getY(), NowMaxZ, 0.8, 0.6, 0.8, 20)
            getCell():addLamppost(Light)
        end
    else
        if Light then
            getCell():removeLamppost(Light)
            Light = nil
        end
    end

    local curr_z = Vehicle:getWorldPos(0, 0, 0, tempVector2):z()
    local GasTankType = "GasTank"
    local GasMultiples = HeliList[GetHeliType(Vehicle)].GasMultiples * SandboxVars.WT.HeliGasMultiples * 0.1
    if Vehicle:getPartById(GasTankType) and Vehicle:getPartById(GasTankType):getInventoryItem() then
        if curr_z > 1 then
            Vehicle:getPartById(GasTankType):setContainerContentAmount(
                Vehicle:getPartById(GasTankType):getContainerContentAmount() - 0.03 * fpsMultiplier * GasMultiples)
            -- Vehicle:setZombiesDontAttack(true)
        else
            Vehicle:getPartById(GasTankType):setContainerContentAmount(
                Vehicle:getPartById(GasTankType):getContainerContentAmount() - 0.01 * fpsMultiplier * GasMultiples)
            -- Vehicle:setZombiesDontAttack(false)
        end
    end

    if curr_z > 1 then
        local HeliType = GetHeliType(Vehicle)
        local vec0 = Vector3f.new(0, 0, curr_z)
        local vec1 = Vehicle:getWorldPos(0, 0, 1, Vector3f.new()):add(-Vehicle:getX(), -Vehicle:getY(), -curr_z)
        local angleZ = vec1:angle(vec0)
        vec1 = Vehicle:getWorldPos(1, 0, 0, Vector3f.new()):add(-Vehicle:getX(), -Vehicle:getY(), -curr_z)
        local angleX = vec1:angle(vec0)
        local BasicAcceleration = HeliList[HeliType].BasicAccelerationModifier or 0.4
        local MaxSpeed = HeliList[HeliType].MaxSpeed or 0.15
        if not HeliList[HeliType].BasicAccelerationModifier then
            BasicAcceleration = 0.4
            MaxSpeed = 0.15
        end
        local dq = CustomQuaternion.fromEulerAngles(math.rad(Vehicle:getAngleX()), math.rad(Vehicle:getAngleY()),
            math.rad(Vehicle:getAngleZ()))
        local ax = 0
        local ay = 0
        local az = 0
        local angle_90 = math.rad(90)
        isRotating = false
        if isKeyDown(Keyboard.KEY_A) then
            ay = 0.7 * fpsMultiplier
            isRotating = true
        end

        if isKeyDown(Keyboard.KEY_D) then
            ay = -0.7 * fpsMultiplier
            isRotating = true
        end
        if isKeyDown(Keyboard.KEY_UP) then
            if not isKeyDown(Keyboard.KEY_LEFT) and not isKeyDown(Keyboard.KEY_RIGHT) then
                if angleZ < angle_90 + MaxSpeed and not Blocked(playerObj, "UP", Vehicle) then
                    ax = BasicAcceleration * fpsMultiplier
                else
                    ax = -BasicAcceleration * fpsMultiplier
                end
            end
        elseif isKeyDown(Keyboard.KEY_DOWN) then
            if not isKeyDown(Keyboard.KEY_LEFT) and not isKeyDown(Keyboard.KEY_RIGHT) then
                if angleZ > angle_90 - MaxSpeed and not Blocked(playerObj, "DOWN", Vehicle) then
                    ax = -BasicAcceleration * fpsMultiplier
                else
                    ax = BasicAcceleration * fpsMultiplier
                end
            end
        else
            if not modData.AutoBalance then
                if angleZ > angle_90 then
                    ax = -BasicAcceleration * 2 * (angleZ - angle_90) * fpsMultiplier
                elseif angleZ < angle_90 then
                    ax = BasicAcceleration * 2 * (angle_90 - angleZ) * fpsMultiplier
                end
            end
        end

        if isKeyDown(Keyboard.KEY_LEFT) then
            if not isKeyDown(Keyboard.KEY_UP) and not isKeyDown(Keyboard.KEY_DOWN) then
                if angleX < angle_90 + MaxSpeed and not Blocked(playerObj, "LEFT", Vehicle) then
                    az = -BasicAcceleration * fpsMultiplier
                else
                    az = BasicAcceleration * fpsMultiplier
                end
            end
        elseif isKeyDown(Keyboard.KEY_RIGHT) then
            if not isKeyDown(Keyboard.KEY_UP) and not isKeyDown(Keyboard.KEY_DOWN) then
                if angleX > angle_90 - MaxSpeed and not Blocked(playerObj, "RIGHT", Vehicle) then
                    az = BasicAcceleration * fpsMultiplier
                else
                    az = -BasicAcceleration * fpsMultiplier
                end
            end
        else
            if angleX > angle_90 then
                az = BasicAcceleration * 3 * (angleX - angle_90) * fpsMultiplier
            elseif angleX < angle_90 then
                az = -BasicAcceleration * 3 * (angle_90 - angleX) * fpsMultiplier
            end
        end

        local nqx = CustomQuaternion.fromAngleAxis(math.rad(ax), 1, 0, 0)
        local nqy = CustomQuaternion.fromAngleAxis(math.rad(ay), 0, 1, 0)
        local nqz = CustomQuaternion.fromAngleAxis(math.rad(az), 0, 0, 1)
        local rs = dq * nqx * nqy * nqz
        local resx, resy, resz = CustomQuaternion.toEulerAngles(CustomQuaternion.toRotationMatrix(rs))
        Vehicle:setAngles(resx, resy, resz)

        local forceVec = Vehicle:getWorldPos(0, 0, 1, Vector3f.new()):add(-Vehicle:getX(), -Vehicle:getY(), -curr_z)
        forceVec = forceVec:normalize()
        if angleZ > math.pi / 2 then
            forceVec:mul(math.abs(math.cos(angleZ)) / 2)
            forceVec:mul(2)
            forceVec:mul(fpsMultiplier)
            forceVec:set(forceVec:x(), 0, forceVec:y())
            moveVehicle(Vehicle, forceVec:x(), forceVec:y(), forceVec:z())
        else
            forceVec:mul(-math.cos(angleZ) / 2)
            forceVec:mul(2)
            forceVec:mul(fpsMultiplier)
            forceVec:set(forceVec:x(), 0, forceVec:y())
            moveVehicle(Vehicle, forceVec:x(), forceVec:y(), forceVec:z())
        end

        forceVec = Vehicle:getWorldPos(1, 0, 0, Vector3f.new()):add(-Vehicle:getX(), -Vehicle:getY(), -curr_z)
        forceVec = forceVec:normalize()
        if angleX > math.pi / 2 then
            forceVec:mul(math.abs(math.cos(angleX)) / 2)
            forceVec:mul(2)
            forceVec:mul(fpsMultiplier)
            forceVec:set(forceVec:x(), 0, forceVec:y())
            moveVehicle(Vehicle, forceVec:x(), forceVec:y(), forceVec:z())
        else
            forceVec:mul(-math.cos(angleX) / 2)
            forceVec:mul(2)
            forceVec:mul(fpsMultiplier)
            forceVec:set(forceVec:x(), 0, forceVec:y())
            moveVehicle(Vehicle, forceVec:x(), forceVec:y(), forceVec:z())
        end

        if curr_z < 8 then
            if isKeyDown(Keyboard.KEY_W) and Vehicle:getRemainingFuelPercentage() > 0 then
                local tempValue = 0.04
                tempValue = tempValue * fpsMultiplier
                moveVehicle(Vehicle, 0, tempValue, 0)
            end
        end
        local angleY = Vehicle:getAngleY()
        if angleY ~= LastAngleY then
            LastAngleY = angleY
        else
            if isRotating then
                local TempZ = 0
                if ay > 0 then
                    TempZ = 0.05
                else
                    TempZ = -0.05
                end
                local nqx = CustomQuaternion.fromAngleAxis(math.rad(0), 1, 0, 0)
                local nqy = CustomQuaternion.fromAngleAxis(math.rad(0), 0, 1, 0)
                local nqz = CustomQuaternion.fromAngleAxis(math.rad(TempZ), 0, 0, 1)
                local rs = dq * nqx * nqy * nqz
                local resx, resy, resz = CustomQuaternion.toEulerAngles(CustomQuaternion.toRotationMatrix(rs))
                Vehicle:setAngles(resx, resy, resz)
            end
        end

        Heli_GlobalSpeed = (ax + ay + az) * fpsMultiplier * 600
        Heli_GlobalHeading = Vehicle:getAngleZ()
    else
        if isKeyDown(Keyboard.KEY_W) and Vehicle:getRemainingFuelPercentage() > 0 then
            local tempValue = 0.04
            tempValue = tempValue * fpsMultiplier
            moveVehicle(Vehicle, 0, tempValue, 0)
        end
    end
    local engine = Vehicle:getPartById("Engine")
    if engine then
        local engineCondtion = engine:getCondition()
        if engineCondtion < 10 then
            local tempValue = -0.1
            tempValue = tempValue * fpsMultiplier
            if Vehicle:isEngineRunning() then
                ISVehicleMenu.onShutOff(playerObj)
            end
            moveVehicle(Vehicle, 0, tempValue, 0)
        end
    end
    if isKeyDown(Keyboard.KEY_S) and curr_z > 0.4 then
        local tempValue = -0.03
        tempValue = tempValue * fpsMultiplier
        moveVehicle(Vehicle, 0, tempValue, 0)
    elseif Vehicle:getRemainingFuelPercentage() <= 0 and curr_z > 0.4 then -- We are falling!
        local tempValue = -0.07
        tempValue = tempValue * fpsMultiplier
        moveVehicle(Vehicle, 0, tempValue, 0)
    end
end

Events.OnTick.Add(helicopterMovementUpdate)

--- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- --- ---

local function helicopterExit(player)
    local Vehicle = player:getVehicle()
    local seat = nil
    if Vehicle ~= nil then
        seat = Vehicle:getSeat(player)
    end
    if Vehicle == nil then
        return
    elseif seat ~= 0 then
        return
    end
    if not GetHeliType(Vehicle) then
        return
    end

    if player:isGhostMode() and not player:isGodMod() then
        player:setGhostMode(false)
    end
end

Events.OnExitVehicle.Add(helicopterExit)

local function checkBlackZone()
    local player = getPlayer()
    local vehicle = player:getVehicle()
    if not vehicle or not GetHeliType(vehicle) then
        return
    end

    local playerX, playerY = player:getX(), player:getY()
    local directions = { {
        x = playerX + 50,
        y = playerY
    }, {
        x = playerX - 50,
        y = playerY
    }, {
        x = playerX,
        y = playerY + 50
    }, {
        x = playerX,
        y = playerY - 50
    } }

    for _, dir in ipairs(directions) do
        local square = getCell():getGridSquare(dir.x, dir.y, 0)
        if not square then
            player:Say(getText("IGUI_BlackZoneWarn"))
            vehicle:playSound("JetAlarm")
            return
        end
    end
end

Events.EveryOneMinute.Add(checkBlackZone)
