OutdoorEntryLights = {}

source(g_currentModDirectory .. "scripts/OutdoorEntryLightsEvent.lua")

function OutdoorEntryLights.prerequisitesPresent(specializations)
    return true
end

function OutdoorEntryLights.initSpecialization()
    local schema = Vehicle.xmlSchema

    schema:setXMLSpecializationType("OutdoorEntryLights")

    schema:register(XMLValueType.INT, "vehicle.outdoorEntryLights#delayDuration", "Delay before lights turn off", 5000)
    schema:register(XMLValueType.FLOAT, "vehicle.outdoorEntryLights#activationDistance", "Activation distance", 6)
    schema:register(XMLValueType.FLOAT, "vehicle.outdoorEntryLights#deactivationDistance", "Deactivation distance", 8)

    schema:register(XMLValueType.NODE_INDEX, "vehicle.outdoorEntryLights.lights.light(?)#node", "Entry light node")

    schema:register(XMLValueType.INT, "vehicle.outdoorEntryLights.blinkers#blinkInterval", "Blink interval", 350)
    schema:register(XMLValueType.INT, "vehicle.outdoorEntryLights.blinkers#blinkCount", "Blink count", 2)
    schema:register(XMLValueType.NODE_INDEX, "vehicle.outdoorEntryLights.blinkers.blinker(?)#node", "Blinker node")

    schema:register(XMLValueType.STRING, "vehicle.outdoorEntryLights.sound#file", "Horn sound file")
    schema:register(XMLValueType.FLOAT, "vehicle.outdoorEntryLights.sound#volumeScale", "Horn volume scale", 0.8)
    schema:register(XMLValueType.FLOAT, "vehicle.outdoorEntryLights.sound#maxDistance", "Horn max audible distance", 25)
    schema:register(XMLValueType.INT, "vehicle.outdoorEntryLights.sound#doublePause", "Double horn pause", 180)

    schema:setXMLSpecializationType()
end

function OutdoorEntryLights.registerEventListeners(vehicleType)
    SpecializationUtil.registerEventListener(vehicleType, "onLoad", OutdoorEntryLights)
    SpecializationUtil.registerEventListener(vehicleType, "onDelete", OutdoorEntryLights)
    SpecializationUtil.registerEventListener(vehicleType, "onUpdateTick", OutdoorEntryLights)
    SpecializationUtil.registerEventListener(vehicleType, "onReadStream", OutdoorEntryLights)
    SpecializationUtil.registerEventListener(vehicleType, "onWriteStream", OutdoorEntryLights)
    SpecializationUtil.registerEventListener(vehicleType, "onReadUpdateStream", OutdoorEntryLights)
    SpecializationUtil.registerEventListener(vehicleType, "onWriteUpdateStream", OutdoorEntryLights)
    SpecializationUtil.registerEventListener(vehicleType, "onEnterVehicle", OutdoorEntryLights)
    SpecializationUtil.registerEventListener(vehicleType, "onLeaveVehicle", OutdoorEntryLights)
end

function OutdoorEntryLights.registerFunctions(vehicleType)
    SpecializationUtil.registerFunction(vehicleType, "setOutdoorEntryLightsState", OutdoorEntryLights.setOutdoorEntryLightsState)
    SpecializationUtil.registerFunction(vehicleType, "startOutdoorEntryLightsBlink", OutdoorEntryLights.startOutdoorEntryLightsBlink)
    SpecializationUtil.registerFunction(vehicleType, "setOutdoorEntryBlinkersVisible", OutdoorEntryLights.setOutdoorEntryBlinkersVisible)
    SpecializationUtil.registerFunction(vehicleType, "playOutdoorEntryHorn", OutdoorEntryLights.playOutdoorEntryHorn)
    SpecializationUtil.registerFunction(vehicleType, "getIsOutdoorEntryHornAudible", OutdoorEntryLights.getIsOutdoorEntryHornAudible)
    SpecializationUtil.registerFunction(vehicleType, "onOutdoorEntryLightsProximityChanged", OutdoorEntryLights.onOutdoorEntryLightsProximityChanged)
    SpecializationUtil.registerFunction(vehicleType, "onOutdoorEntryLightsOccupiedChanged", OutdoorEntryLights.onOutdoorEntryLightsOccupiedChanged)
    SpecializationUtil.registerFunction(vehicleType, "handleOutdoorEntryLocalLeave", OutdoorEntryLights.handleOutdoorEntryLocalLeave)
	SpecializationUtil.registerFunction(vehicleType, "applyOutdoorEntryLightsVisibility", OutdoorEntryLights.applyOutdoorEntryLightsVisibility)
end

function OutdoorEntryLights:onLoad(savegame)
    local spec = {}
    self.spec_outdoorEntryLights = spec

    spec.isActive = false

    spec.delayTimer = 0
    spec.delayDuration = self.xmlFile:getValue("vehicle.outdoorEntryLights#delayDuration", 5000)
    spec.pendingTurnOff = false

    spec.wasEntered = false
    spec.clientWasInRange = false
    spec.wasAnyPlayerInRange = false
    spec.proximityConnections = {}

    spec.clientWasOccupied = false
    spec.lastLocalOccupied = false
    spec.occupiedConnections = {}

    spec.exitRangeMonitor = false
    spec.forceProximityRecheckTimer = 0
    spec.forceProximityRecheckDuration = 3000

    spec.ignoreProximityFalseTimer = 0
    spec.ignoreProximityFalseDuration = 300
	
	
    spec.lastControlledVehicle = nil

    spec.blinkNodes = {}
    spec.blinkActive = false
    spec.blinkTimer = 0
    spec.blinkInterval = self.xmlFile:getValue("vehicle.outdoorEntryLights.blinkers#blinkInterval", 350)
    spec.blinkCount = self.xmlFile:getValue("vehicle.outdoorEntryLights.blinkers#blinkCount", 2)
    spec.blinkSwitchesLeft = spec.blinkCount * 2
    spec.blinkersVisible = false

    spec.activationDistance = self.xmlFile:getValue("vehicle.outdoorEntryLights#activationDistance", 6)
    spec.deactivationDistance = self.xmlFile:getValue("vehicle.outdoorEntryLights#deactivationDistance", spec.activationDistance + 2)

    spec.hornSample = nil
    spec.hornVolume = self.xmlFile:getValue("vehicle.outdoorEntryLights.sound#volumeScale", 0.8)
    spec.hornMaxDistance = self.xmlFile:getValue("vehicle.outdoorEntryLights.sound#maxDistance", 25)
    spec.hornDoublePause = self.xmlFile:getValue("vehicle.outdoorEntryLights.sound#doublePause", 180)

    local soundFile = self.xmlFile:getValue("vehicle.outdoorEntryLights.sound#file")

    if soundFile ~= nil then
        local soundPath = Utils.getFilename(soundFile, self.baseDirectory)

        spec.hornSample = createSample("OutdoorEntryLightsHorn")

        if not loadSample(spec.hornSample, soundPath, false) then
            Logging.warning("[%s] OutdoorEntryLights: Sound konnte nicht geladen werden: %s", self.configFileName, soundPath)
            spec.hornSample = nil
        end
    end

    spec.secondHornTimer = 0
    spec.secondHornPending = false

    spec.lightNodes = {}

    self.xmlFile:iterate("vehicle.outdoorEntryLights.lights.light", function(_, key)
        local node = self.xmlFile:getValue(key .. "#node", nil, self.components, self.i3dMappings)

        if node ~= nil then
            table.insert(spec.lightNodes, node)
            setVisibility(node, false)
        end
    end)

    self.xmlFile:iterate("vehicle.outdoorEntryLights.blinkers.blinker", function(_, key)
        local node = self.xmlFile:getValue(key .. "#node", nil, self.components, self.i3dMappings)

        if node ~= nil then
            table.insert(spec.blinkNodes, node)
            setVisibility(node, false)
        end
    end)

    self:raiseActive()
end

function OutdoorEntryLights:onWriteStream(streamId, connection)
    local spec = self.spec_outdoorEntryLights

    streamWriteBool(streamId, spec ~= nil and spec.isActive or false)
    streamWriteBool(streamId, spec ~= nil and spec.blinkActive or false)
    streamWriteBool(streamId, spec ~= nil and spec.blinkersVisible or false)
end

function OutdoorEntryLights:onReadStream(streamId, connection)
    local spec = self.spec_outdoorEntryLights

    if spec == nil then
        streamReadBool(streamId)
        streamReadBool(streamId)
        streamReadBool(streamId)
        return
    end

    spec.isActive = streamReadBool(streamId)
    spec.blinkActive = streamReadBool(streamId)
    spec.blinkersVisible = streamReadBool(streamId)

    self:applyOutdoorEntryLightsVisibility()
    self:setOutdoorEntryBlinkersVisible(spec.blinkersVisible)
end

function OutdoorEntryLights:onWriteUpdateStream(streamId, connection, dirtyMask)
    local spec = self.spec_outdoorEntryLights

    streamWriteBool(streamId, spec ~= nil and spec.isActive or false)
    streamWriteBool(streamId, spec ~= nil and spec.blinkActive or false)
    streamWriteBool(streamId, spec ~= nil and spec.blinkersVisible or false)
end

function OutdoorEntryLights:onReadUpdateStream(streamId, timestamp, connection)
    local spec = self.spec_outdoorEntryLights

    if spec == nil then
        streamReadBool(streamId)
        streamReadBool(streamId)
        streamReadBool(streamId)
        return
    end

    spec.isActive = streamReadBool(streamId)
    spec.blinkActive = streamReadBool(streamId)
    spec.blinkersVisible = streamReadBool(streamId)

    self:applyOutdoorEntryLightsVisibility()
    self:setOutdoorEntryBlinkersVisible(spec.blinkersVisible)
end

function OutdoorEntryLights:onEnterVehicle(...)
    local spec = self.spec_outdoorEntryLights

    if spec == nil then
        return
    end

    spec.clientWasOccupied = true
    spec.lastLocalOccupied = true
    spec.wasEntered = true
    spec.clientWasInRange = false
    spec.exitRangeMonitor = false

    OutdoorEntryLightsOccupiedEvent.sendEvent(self, true)
    OutdoorEntryLightsProximityEvent.sendEvent(self, false)

    if spec.isActive then
        self:setOutdoorEntryLightsState(false, false)
    end
end

function OutdoorEntryLights:onLeaveVehicle(...)
    self:handleOutdoorEntryLocalLeave()
end

function OutdoorEntryLights:handleOutdoorEntryLocalLeave()
    local spec = self.spec_outdoorEntryLights

    if spec == nil then
        return
    end

    spec.clientWasOccupied = false
    spec.lastLocalOccupied = false
    spec.wasEntered = true

    -- Nach Ausstieg gilt Spieler lokal erstmal als in Reichweite.
    spec.clientWasInRange = true
    spec.exitRangeMonitor = true

    spec.forceProximityRecheckTimer = spec.forceProximityRecheckDuration or 3000

    OutdoorEntryLightsOccupiedEvent.sendEvent(self, false)

	-- Lokal sofort als Ausstiegslicht einschalten
	if not spec.isActive then
		self:setOutdoorEntryLightsState(true, false)
	end

    self:raiseActive()
end

function OutdoorEntryLights:onDelete()
    local spec = self.spec_outdoorEntryLights

    if spec ~= nil and spec.hornSample ~= nil then
        delete(spec.hornSample)
        spec.hornSample = nil
    end
end

function OutdoorEntryLights:onOutdoorEntryLightsOccupiedChanged(connection, occupied)
    local spec = self.spec_outdoorEntryLights

    if spec == nil or g_server == nil then
        return
    end

    local key = connection or "singleplayer"

    spec.occupiedConnections = spec.occupiedConnections or {}
    spec.proximityConnections = spec.proximityConnections or {}

    if occupied then
        spec.occupiedConnections[key] = true

        spec.proximityConnections = {}
        spec.wasAnyPlayerInRange = false
        spec.pendingTurnOff = false
        spec.delayTimer = 0

        if spec.isActive then
            self:setOutdoorEntryLightsState(false, false)
        end
    else
        -- Fahrzeug ist sicher frei.
        spec.occupiedConnections = {}

        -- Silent-Aussteigen: Licht an, aber keine Hupe/Blinker.
        spec.wasEntered = true

        -- Server merkt: Spieler steht nach Ausstieg noch in Reichweite.
        spec.proximityConnections = {}
        spec.proximityConnections[key] = true
        spec.wasAnyPlayerInRange = true

        spec.ignoreProximityFalseTimer = spec.ignoreProximityFalseDuration or 300

        spec.pendingTurnOff = false
        spec.delayTimer = 0

		if not spec.isActive then
			self:setOutdoorEntryLightsState(true, false)
		end
    end
end

function OutdoorEntryLights:onOutdoorEntryLightsProximityChanged(connection, inRange)
    local spec = self.spec_outdoorEntryLights

    if spec == nil or g_server == nil then
        return
    end

    local key = connection or "singleplayer"

    if not inRange and spec.ignoreProximityFalseTimer ~= nil and spec.ignoreProximityFalseTimer > 0 then
        return
    end

    local motorRunning = self.getIsMotorStarted ~= nil and self:getIsMotorStarted()
    local vehicleOccupied = spec.occupiedConnections ~= nil and next(spec.occupiedConnections) ~= nil

    if vehicleOccupied or motorRunning then
        spec.proximityConnections = {}
        spec.wasAnyPlayerInRange = false
        spec.pendingTurnOff = false
        spec.delayTimer = 0

        if spec.isActive then
            self:setOutdoorEntryLightsState(false, false)
        end

        return
    end

    spec.proximityConnections = spec.proximityConnections or {}

    if inRange then
        spec.proximityConnections[key] = true
    else
        -- Wichtig: bei false komplett leeren, damit kein zweiter Spieler hängen bleibt
        spec.proximityConnections = {}
        spec.wasAnyPlayerInRange = false
    end

    local anyPlayerInRange = next(spec.proximityConnections) ~= nil

    if anyPlayerInRange and not spec.wasAnyPlayerInRange then
        spec.wasAnyPlayerInRange = true
        spec.pendingTurnOff = false
        spec.delayTimer = 0

        local silentUnlock = spec.wasEntered

        if not spec.isActive then
            self:setOutdoorEntryLightsState(true, false)

            if not silentUnlock then
                self:startOutdoorEntryLightsBlink(false)
                self:playOutdoorEntryHorn(1, false)
            end
        end

        spec.wasEntered = false
    end

    if not anyPlayerInRange then
        spec.wasAnyPlayerInRange = false
        spec.wasEntered = false

        if spec.isActive and not spec.pendingTurnOff then
            spec.pendingTurnOff = true
            spec.delayTimer = spec.delayDuration
        end
    end
end

function OutdoorEntryLights:onUpdateTick(dt, isActiveForInput, isActiveForInputIgnoreSelection, isSelected)
    local spec = self.spec_outdoorEntryLights

    if spec == nil then
        return
    end

    self:raiseActive()

    if g_server ~= nil and spec.ignoreProximityFalseTimer ~= nil and spec.ignoreProximityFalseTimer > 0 then
        spec.ignoreProximityFalseTimer = spec.ignoreProximityFalseTimer - dt
    end

    local controlledVehicle = nil

    if g_currentMission ~= nil then
        controlledVehicle = g_currentMission.controlledVehicle
    end

    local vehicleEntered = self.getIsEntered ~= nil and self:getIsEntered()
    local vehicleControlled = self.getIsControlled ~= nil and self:getIsControlled()
    local motorRunning = self.getIsMotorStarted ~= nil and self:getIsMotorStarted()

    local localOccupied = vehicleEntered or vehicleControlled or controlledVehicle == self

    if localOccupied ~= spec.lastLocalOccupied then
        if localOccupied then
            spec.clientWasOccupied = true
            spec.lastLocalOccupied = true
            spec.wasEntered = true
            spec.clientWasInRange = false
            spec.exitRangeMonitor = false

            OutdoorEntryLightsOccupiedEvent.sendEvent(self, true)
            OutdoorEntryLightsProximityEvent.sendEvent(self, false)

            if spec.isActive then
                self:setOutdoorEntryLightsState(false, false)
            end
        else
            self:handleOutdoorEntryLocalLeave()
        end
    end

    -- Server blockiert bei besetztem Fahrzeug oder laufendem Motor.
    if g_server ~= nil and (localOccupied or motorRunning) then
        spec.proximityConnections = {}
        spec.wasAnyPlayerInRange = false
        spec.pendingTurnOff = false
        spec.delayTimer = 0

        if spec.isActive then
            self:setOutdoorEntryLightsState(false, false)
        end
    end

    local forceRecheck = spec.forceProximityRecheckTimer ~= nil and spec.forceProximityRecheckTimer > 0

    if forceRecheck then
        spec.forceProximityRecheckTimer = spec.forceProximityRecheckTimer - dt
    end

    -- Exit-Monitor: nach Ausstieg Weglaufen sicher erkennen.
    if spec.exitRangeMonitor == true then
        local cameraNode = getCamera()

        if cameraNode ~= nil and cameraNode ~= 0 and self.components ~= nil and self.components[1] ~= nil then
            local px, py, pz = getWorldTranslation(cameraNode)
            local vx, vy, vz = getWorldTranslation(self.components[1].node)

            local dx = px - vx
            local dz = pz - vz
            local distance = math.sqrt(dx * dx + dz * dz)

            local deactivationDistance = spec.deactivationDistance or ((spec.activationDistance or 6) + 2)

            if distance >= deactivationDistance then
                spec.exitRangeMonitor = false
                spec.clientWasInRange = false
                spec.ignoreProximityFalseTimer = 0

                OutdoorEntryLightsProximityEvent.sendEvent(self, false)
            end
        end
    end

    -- Nur zu Fuss auslösen. Anderes Fahrzeug blockiert, ausser beim Exit-Recheck.
    if controlledVehicle ~= nil and controlledVehicle ~= self and not forceRecheck and not spec.exitRangeMonitor then
        if spec.clientWasInRange then
            spec.clientWasInRange = false
            OutdoorEntryLightsProximityEvent.sendEvent(self, false)
        end
    elseif not motorRunning or forceRecheck or spec.exitRangeMonitor then
        local playerNode = nil
        local player = nil

        if g_localPlayer ~= nil then
            player = g_localPlayer
        elseif g_currentMission ~= nil and g_currentMission.player ~= nil then
            player = g_currentMission.player
        elseif g_currentMission ~= nil and g_currentMission.controlledPlayer ~= nil then
            player = g_currentMission.controlledPlayer
        end

        if player ~= nil then
            playerNode = player.rootNode or player.graphicsRootNode or player.cameraNode
        end

        if playerNode == nil then
            local cameraNode = getCamera()

            if cameraNode ~= nil and cameraNode ~= 0 then
                playerNode = cameraNode
            end
        end

        if playerNode ~= nil and self.components ~= nil and self.components[1] ~= nil then
            local px, py, pz = getWorldTranslation(playerNode)
            local vx, vy, vz = getWorldTranslation(self.components[1].node)

            local dx = px - vx
            local dz = pz - vz
            local distance = math.sqrt(dx * dx + dz * dz)

            local activationDistance = spec.activationDistance or 6
            local deactivationDistance = spec.deactivationDistance or activationDistance + 2

            if distance <= activationDistance and not spec.clientWasInRange then
				spec.clientWasInRange = true
				OutdoorEntryLightsProximityEvent.sendEvent(self, true)

			elseif distance >= deactivationDistance and spec.clientWasInRange then
				spec.clientWasInRange = false
				spec.ignoreProximityFalseTimer = 0
				OutdoorEntryLightsProximityEvent.sendEvent(self, false)
			end
        end
    end

    if g_server ~= nil and spec.pendingTurnOff then
        spec.delayTimer = spec.delayTimer - dt

        if spec.delayTimer <= 0 then
            spec.pendingTurnOff = false
            spec.delayTimer = 0

            self:setOutdoorEntryLightsState(false, false)
            self:startOutdoorEntryLightsBlink(false)
            self:playOutdoorEntryHorn(2, false)
        end
    end

    if spec.blinkActive then
        spec.blinkTimer = spec.blinkTimer - dt

        if spec.blinkTimer <= 0 then
            spec.blinkTimer = spec.blinkInterval
            spec.blinkSwitchesLeft = spec.blinkSwitchesLeft - 1

            spec.blinkersVisible = not spec.blinkersVisible
            self:setOutdoorEntryBlinkersVisible(spec.blinkersVisible)

            if spec.blinkSwitchesLeft <= 0 then
                spec.blinkActive = false
                spec.blinkersVisible = false
                self:setOutdoorEntryBlinkersVisible(false)
            end
        end
    end

    if spec.secondHornPending then
        spec.secondHornTimer = spec.secondHornTimer - dt

        if spec.secondHornTimer <= 0 then
            spec.secondHornPending = false

            if spec.hornSample ~= nil and self:getIsOutdoorEntryHornAudible() then
                playSample(spec.hornSample, 1, spec.hornVolume, 0, 0, 0)
            end
        end
    end
end

function OutdoorEntryLights:applyOutdoorEntryLightsVisibility()
    local spec = self.spec_outdoorEntryLights

    if spec == nil or spec.lightNodes == nil then
        return
    end

    for _, node in ipairs(spec.lightNodes) do
        if node ~= nil then
            setVisibility(node, spec.isActive)
        end
    end
end

function OutdoorEntryLights:setOutdoorEntryLightsState(state, noEventSend)
    local spec = self.spec_outdoorEntryLights

    if spec == nil then
        return
    end

    if spec.isActive == state then
        return
    end

    spec.isActive = state

    OutdoorEntryLightsEvent.sendEvent(self, state, noEventSend)
    self:applyOutdoorEntryLightsVisibility()
end

function OutdoorEntryLights:startOutdoorEntryLightsBlink(noEventSend)
    local spec = self.spec_outdoorEntryLights

    if spec == nil or spec.blinkNodes == nil or #spec.blinkNodes == 0 then
        return
    end

    OutdoorEntryLightsBlinkEvent.sendEvent(self, noEventSend)

    spec.blinkActive = false
    spec.blinkersVisible = false
    self:setOutdoorEntryBlinkersVisible(false)

    spec.blinkActive = true
    spec.blinkTimer = 1
    spec.blinkSwitchesLeft = spec.blinkCount * 2
end

function OutdoorEntryLights:setOutdoorEntryBlinkersVisible(state)
    local spec = self.spec_outdoorEntryLights

    if spec == nil or spec.blinkNodes == nil then
        return
    end

    for _, node in ipairs(spec.blinkNodes) do
        if node ~= nil then
            setVisibility(node, state)
        end
    end
end

function OutdoorEntryLights:getIsOutdoorEntryHornAudible()
    local spec = self.spec_outdoorEntryLights

    if spec == nil then
        return false
    end

    local cameraNode = getCamera()

    if cameraNode == nil or cameraNode == 0 then
        return false
    end

    if self.components == nil or self.components[1] == nil then
        return false
    end

    local cx, cy, cz = getWorldTranslation(cameraNode)
    local vx, vy, vz = getWorldTranslation(self.components[1].node)

    local dx = cx - vx
    local dz = cz - vz
    local distance = math.sqrt(dx * dx + dz * dz)

    return distance <= (spec.hornMaxDistance or 25)
end

function OutdoorEntryLights:playOutdoorEntryHorn(times, noEventSend)
    local spec = self.spec_outdoorEntryLights

    if spec == nil or spec.hornSample == nil then
        return
    end

    OutdoorEntryLightsHornEvent.sendEvent(self, times, noEventSend)

    if times == 1 then
        if self:getIsOutdoorEntryHornAudible() then
            playSample(spec.hornSample, 1, spec.hornVolume, 0, 0, 0)
        end

    elseif times == 2 then
        if self:getIsOutdoorEntryHornAudible() then
            playSample(spec.hornSample, 1, spec.hornVolume, 0, 0, 0)
        end

        spec.secondHornTimer = spec.hornDoublePause
        spec.secondHornPending = true
    end
end