----------------------------------------------------------------------------------------------------
-- FreeFloatComponentJoints
----------------------------------------------------------------------------------------------------
-- Purpose: allow components to behave in free float mode.
--
-- Based on: The unlock component joint script from rafftnix.
--
-- Copyright (c) Wopster, 2020
--
-- FS 22 (c) Ifko[nator], 2023
----------------------------------------------------------------------------------------------------

---@class FreeFloatComponentJoints
FreeFloatComponentJoints = {};
FreeFloatComponentJoints.MOD_NAME = g_currentModName;

function FreeFloatComponentJoints.initSpecialization()
    local schema = Vehicle.xmlSchema;

    schema:setXMLSpecializationType("FreeFloatComponentJoints");

    local freeFloatComponentJointKey = "vehicle.freeFloatComponentJoints.freeFloatComponentJoint(?)";

    schema:register(XMLValueType.NODE_INDEX, freeFloatComponentJointKey .. "#movingToolNode", "Moving tool node link node");
    schema:register(XMLValueType.INT, freeFloatComponentJointKey .. "#componentJointIndex", "Component joint index");
    schema:register(XMLValueType.FLOAT, freeFloatComponentJointKey .. "#duration", "Duration value");
    schema:register(XMLValueType.BOOL, freeFloatComponentJointKey .. "#isAdaptive", "Is adaptive (true or false)");
end;

function FreeFloatComponentJoints.prerequisitesPresent(specializations)
    return SpecializationUtil.hasSpecialization(Cylindered, specializations);
end;

function FreeFloatComponentJoints.registerFunctions(vehicleType)
    SpecializationUtil.registerFunction(vehicleType, "loadFreeFloatComponentJointFromXML", FreeFloatComponentJoints.loadFreeFloatComponentJointFromXML);
    SpecializationUtil.registerFunction(vehicleType, "canFreeFloat", FreeFloatComponentJoints.canFreeFloat);
    SpecializationUtil.registerFunction(vehicleType, "setFreeFloatState", FreeFloatComponentJoints.setFreeFloatState);
end;

function FreeFloatComponentJoints.registerEventListeners(vehicleType)
    SpecializationUtil.registerEventListener(vehicleType, "onLoad", FreeFloatComponentJoints);
    SpecializationUtil.registerEventListener(vehicleType, "onReadStream", FreeFloatComponentJoints);
    SpecializationUtil.registerEventListener(vehicleType, "onWriteStream", FreeFloatComponentJoints);
    SpecializationUtil.registerEventListener(vehicleType, "onUpdateTick", FreeFloatComponentJoints);
    SpecializationUtil.registerEventListener(vehicleType, "onRegisterActionEvents", FreeFloatComponentJoints);
end;

function FreeFloatComponentJoints:onLoad(savegame)
    self.spec_freeFloatComponentJoints = self[("spec_%s.freeFloatComponentJoints"):format(FreeFloatComponentJoints.MOD_NAME)];
    
    local specFreeFloatComponentJoints = self.spec_freeFloatComponentJoints;
    
    specFreeFloatComponentJoints.isFreeFloating = false;
    specFreeFloatComponentJoints.isDirty = false;
    specFreeFloatComponentJoints.freeFloatComponentJoints = {};

    local freeFloatComponentJointNumber = 0;

    while true do
        local key = ("vehicle.freeFloatComponentJoints.freeFloatComponentJoint(%d)"):format(freeFloatComponentJointNumber);

        if not self.xmlFile:hasProperty(key) then
            break;
        end;

        local entry = {};

        if self:loadFreeFloatComponentJointFromXML(self.xmlFile, key, entry) then
            table.insert(specFreeFloatComponentJoints.freeFloatComponentJoints, entry);
        end;

        freeFloatComponentJointNumber = freeFloatComponentJointNumber + 1;
    end;
end;

function FreeFloatComponentJoints:loadFreeFloatComponentJointFromXML(xmlFile, key, control)
    control.componentJointIndex = Utils.getNoNil(xmlFile:getValue(key .. "#componentJointIndex"), 0);
    control.movingToolNode = xmlFile:getValue(key .. "#movingToolNode", nil, self.components, self.i3dMappings);

    if control.movingToolNode == nil then
        g_logManager:xmlError(self.configFileName, ("Moving tool node not set for the free float component joint: %d"):format(control.componentJointIndex));

        return false;
    end;

    local movingTool = self:getMovingToolByNode(control.movingToolNode);
    -- Backup values from the moving tool.
    control.rotSpeed = movingTool.rotSpeed;
    control.freeFloatDuration = Utils.getNoNil(xmlFile:getValue(key .. "#duration"), 5000);

    control.lastMinRotation = 0;
    control.lastMaxRotation = 0;
    control.alpha = 0;
    control.isAdaptive = xmlFile:getValue(key .. "#isAdaptive", true);

    return true;
end;

function FreeFloatComponentJoints:onReadStream(streamId, connection)
    local isFreeFloating = streamReadBool(streamId);

    self:setFreeFloatState(isFreeFloating, true);
end;

function FreeFloatComponentJoints:onWriteStream(streamId, connection)
    streamWriteBool(streamId, self.spec_freeFloatComponentJoints.isFreeFloating);
end;

function FreeFloatComponentJoints:onUpdateTick(dt)
    local specFreeFloatComponentJoints = self.spec_freeFloatComponentJoints;

    if not self.isServer then
        return;
    end;

    if specFreeFloatComponentJoints.isDirty then
        local didUpdate = false;

        for _, control in ipairs(specFreeFloatComponentJoints.freeFloatComponentJoints) do
            if control.targetMinRotation ~= control.lastMinRotation
                or control.targetMaxRotation ~= control.lastMaxRotation 
            then
                local componentJointDesc = self.componentJoints[control.componentJointIndex];
                local movingTool = self:getMovingToolByNode(control.movingToolNode);

                control.alpha = math.min(control.alpha + dt / control.freeFloatDuration, 1);
                control.lastMinRotation = MathUtil.lerp(control.lastMinRotation, control.targetMinRotation, control.alpha);
                control.lastMaxRotation = MathUtil.lerp(control.lastMaxRotation, control.targetMaxRotation, control.alpha);

                self:setComponentJointRotLimit(componentJointDesc, movingTool.rotationAxis, -control.lastMinRotation, control.lastMaxRotation);
                self:setMovingToolDirty(movingTool);

                didUpdate = true;
            end;
        end;

        if not didUpdate then
            specFreeFloatComponentJoints.isDirty = false;
        end;
    end;
end;

function FreeFloatComponentJoints:canFreeFloat()
    return true;
end;

function FreeFloatComponentJoints:setFreeFloatState(isFreeFloating, noEventSend)
    local specFreeFloatComponentJoints = self.spec_freeFloatComponentJoints;

    if specFreeFloatComponentJoints.isFreeFloating ~= isFreeFloating then
        FreeFloatComponentJointsEvent.sendEvent(self, isFreeFloating, noEventSend);

        specFreeFloatComponentJoints.isFreeFloating = isFreeFloating;

        local actionEvent = specFreeFloatComponentJoints.actionEvents[InputAction.RE_TOGGLE_FLOAT];

        if actionEvent ~= nil then
            local text = g_i18n:getText("action_enableFreeFloat"):format(self.typeDesc);

            if not specFreeFloatComponentJoints.isFreeFloating then
                text = g_i18n:getText("action_disableFreeFloat"):format(self.typeDesc);
            end;

            g_inputBinding:setActionEventText(actionEvent.actionEventId, text);
        end;

        if self.isServer then
            for _, control in ipairs(specFreeFloatComponentJoints.freeFloatComponentJoints) do
                local movingTool = self:getMovingToolByNode(control.movingToolNode);
                local targetMinRotation = 0;
                local targetMaxRotation = 0;

                control.alpha = 0;

                if isFreeFloating then
                    movingTool.rotSpeed = 0;

                    local rotation = {unpack(movingTool.curRot)};
                    local curRot = rotation[movingTool.rotationAxis];
                    local rotLimit = (movingTool.rotMax - movingTool.rotMin) * 0.5;
                    local minRot = rotLimit + curRot;
                    local maxRot = rotLimit - curRot;

                    if not control.isAdaptive then
                        local curRotAbs = math.abs(curRot);

                        if curRot < 0 then
                            maxRot = curRotAbs + movingTool.rotMax;
                        else
                            maxRot = movingTool.rotMax - curRotAbs;
                        end;

                        rotLimit = movingTool.rotMax - movingTool.rotMin;
                        minRot = rotLimit - maxRot;
                    end;

                    targetMinRotation = minRot;
                    targetMaxRotation = maxRot;
                else
                    movingTool.rotSpeed = control.rotSpeed;

                    local x, y, z = getRotation(movingTool.node);

                    movingTool.curRot = {x, y, z};
                end;

                control.targetMinRotation = targetMinRotation;
                control.targetMaxRotation = targetMaxRotation;

                specFreeFloatComponentJoints.isDirty = true;
            end;
        end;
    end;
end;

--- Action events
function FreeFloatComponentJoints.actionEventToggleFloat(self)
    if self:canFreeFloat() then
        self:setFreeFloatState(not self.spec_freeFloatComponentJoints.isFreeFloating);
    end;
end;

function FreeFloatComponentJoints:onRegisterActionEvents(isActiveForInput, isActiveForInputIgnoreSelection)
    if self.isClient then
        local specFreeFloatComponentJoints = self.spec_freeFloatComponentJoints;

        self:clearActionEventsTable(specFreeFloatComponentJoints.actionEvents);

        if isActiveForInput then
            local _, actionEventId = self:addActionEvent(specFreeFloatComponentJoints.actionEvents, InputAction.RE_TOGGLE_FLOAT, self, FreeFloatComponentJoints.actionEventToggleFloat, false, true, false, true, nil, nil, true);
            local text = g_i18n:getText("action_enableFreeFloat"):format(self.typeDesc);

            if not specFreeFloatComponentJoints.isFreeFloating then
                text = g_i18n:getText("action_disableFreeFloat"):format(self.typeDesc);
            end;

            g_inputBinding:setActionEventText(actionEventId, text);
            g_inputBinding:setActionEventTextVisibility(actionEventId, true);
            g_inputBinding:setActionEventTextPriority(actionEventId, GS_PRIO_HIGH);
        end;
    end;
end;