-- PlaceableSaleZone
-- Custom specialization for a vehicle sale zone that must be placed near
-- an existing Used Equipment Yard. Players drive vehicles into the zone,
-- walk up, and interact via an activatable to open a sell dialog.
--
-- Follows the VehicleSellingPoint trigger pattern:
--   - playerTrigger  (PLAYER collision mask)  → add/remove activatable
--   - vehicleTrigger (VEHICLE collision mask)  → track vehicles in range

PlaceableSaleZone = {}

PlaceableSaleZone.KEY = "saleZoneData"
PlaceableSaleZone.DEFAULT_NEAR_YARD_RADIUS = 50

function PlaceableSaleZone.prerequisitesPresent(specializations)
    return true
end

function PlaceableSaleZone.registerXMLPaths(schema, basePath)
    schema:register(XMLValueType.NODE_INDEX, basePath .. ".saleZone#playerTriggerNode", "Player trigger node")
    schema:register(XMLValueType.NODE_INDEX, basePath .. ".saleZone#vehicleTriggerNode", "Vehicle trigger node")
    schema:register(XMLValueType.FLOAT, basePath .. ".saleZone#nearYardRadius", "Max distance to a used equipment yard", PlaceableSaleZone.DEFAULT_NEAR_YARD_RADIUS)
end

function PlaceableSaleZone.registerFunctions(placeableType)
    SpecializationUtil.registerFunction(placeableType, "saleZonePlayerTriggerCallback",
        PlaceableSaleZone.saleZonePlayerTriggerCallback)
    SpecializationUtil.registerFunction(placeableType, "saleZoneVehicleTriggerCallback",
        PlaceableSaleZone.saleZoneVehicleTriggerCallback)
    SpecializationUtil.registerFunction(placeableType, "determineCurrentVehicles",
        PlaceableSaleZone.determineCurrentVehicles)
end

function PlaceableSaleZone.registerOverwrittenFunctions(placeableType)
    SpecializationUtil.registerOverwrittenFunction(placeableType, "getCanBePlacedAt",
        PlaceableSaleZone.getCanBePlacedAt)
    SpecializationUtil.registerOverwrittenFunction(placeableType, "getIsOnOwnedFarmland",
        PlaceableSaleZone.getIsOnOwnedFarmland)
    SpecializationUtil.registerOverwrittenFunction(placeableType, "getIsOnFarmland",
        PlaceableSaleZone.getIsOnFarmland)
end

function PlaceableSaleZone.registerEventListeners(placeableType)
    SpecializationUtil.registerEventListener(placeableType, "onLoad", PlaceableSaleZone)
    SpecializationUtil.registerEventListener(placeableType, "onPostFinalizePlacement", PlaceableSaleZone)
    SpecializationUtil.registerEventListener(placeableType, "onDelete", PlaceableSaleZone)
    SpecializationUtil.registerEventListener(placeableType, "onReadStream", PlaceableSaleZone)
    SpecializationUtil.registerEventListener(placeableType, "onWriteStream", PlaceableSaleZone)
end

-- ---------------------------------------------------------------------------
-- Placement constraint: must be near a yard
-- ---------------------------------------------------------------------------

function PlaceableSaleZone:getCanBePlacedAt(superFunc, x, y, z, farmId)
    local canBePlaced, errorMessage = superFunc(self, x, y, z, farmId)
    if not canBePlaced then
        return false, errorMessage
    end

    local nearestYard, nearestDist = PlaceableSaleZone.findNearestYard(x, z)
    local data = self[PlaceableSaleZone.KEY]
    local radius = data and data.nearYardRadius or PlaceableSaleZone.DEFAULT_NEAR_YARD_RADIUS

    if nearestYard == nil or nearestDist > radius then
        return false, g_i18n:getText("uey_saleZone_notNearYard")
    end

    return true
end

function PlaceableSaleZone:getIsOnOwnedFarmland(superFunc)
    return true
end

function PlaceableSaleZone:getIsOnFarmland(superFunc, farmlandId)
    return false
end

-- ---------------------------------------------------------------------------
-- Find nearest yard (works on both server and client)
-- ---------------------------------------------------------------------------

-- Returns 0 if (px, pz) is inside the yard polygon, otherwise the distance
-- to the nearest point on the polygon boundary.  Falls back to centre distance
-- when no polygon is available (legacy saves).
local function yardDistanceFromPoint(yard, px, pz)
    local poly = yard.bounds.polygon
    if poly == nil or #poly < 3 then
        local dx = yard.bounds.cx - px
        local dz = yard.bounds.cz - pz
        return math.sqrt(dx * dx + dz * dz)
    end

    -- Inside the polygon → distance is zero.
    if yard:containsPoint(px, pz) then
        return 0
    end

    -- Distance to the nearest edge of the polygon.
    local minDist = math.huge
    local n = #poly
    for i = 1, n do
        local ax, az = poly[i].x, poly[i].z
        local bx, bz = poly[(i % n) + 1].x, poly[(i % n) + 1].z
        local abx, abz = bx - ax, bz - az
        local apx, apz = px - ax, pz - az
        local lenSq = abx * abx + abz * abz
        local t = lenSq > 0 and math.max(0, math.min(1, (apx * abx + apz * abz) / lenSq)) or 0
        local ex, ez = ax + t * abx, az + t * abz
        local dx, dz = px - ex, pz - ez
        local d = math.sqrt(dx * dx + dz * dz)
        if d < minDist then minDist = d end
    end
    return minDist
end

function PlaceableSaleZone.findNearestYard(x, z)
    local nearestYard = nil
    local nearestDist = math.huge

    -- Server: use YardManager.
    if UsedEquipmentYards.yardManager ~= nil then
        for _, yard in pairs(UsedEquipmentYards.yardManager.yards) do
            local dist = yardDistanceFromPoint(yard, x, z)
            if dist < nearestDist then
                nearestDist = dist
                nearestYard = yard
            end
        end
    end

    -- Client: use clientYards.
    for _, yard in pairs(UsedEquipmentYards.clientYards) do
        local dist = yardDistanceFromPoint(yard, x, z)
        if dist < nearestDist then
            nearestDist = dist
            nearestYard = yard
        end
    end

    return nearestYard, nearestDist
end

-- ---------------------------------------------------------------------------
-- Lifecycle
-- ---------------------------------------------------------------------------

function PlaceableSaleZone:onLoad(savegame)
    local xmlFile = self.xmlFile
    local data = {}
    data.yardId = nil
    data.nearYardRadius = xmlFile:getValue("placeable.saleZone#nearYardRadius", PlaceableSaleZone.DEFAULT_NEAR_YARD_RADIUS)

    -- Resolve trigger nodes from i3d mappings.
    data.playerTriggerNode = xmlFile:getValue("placeable.saleZone#playerTriggerNode", nil, self.components, self.i3dMappings)
    data.vehicleTriggerNode = xmlFile:getValue("placeable.saleZone#vehicleTriggerNode", nil, self.components, self.i3dMappings)

    data.vehicleShapesInRange = {}
    data.activatable = nil

    self[PlaceableSaleZone.KEY] = data

    -- Set up triggers.
    if data.playerTriggerNode ~= nil then
        addTrigger(data.playerTriggerNode, "saleZonePlayerTriggerCallback", self)
    end
    if data.vehicleTriggerNode ~= nil then
        addTrigger(data.vehicleTriggerNode, "saleZoneVehicleTriggerCallback", self)
    end

    -- Create activatable (added/removed dynamically via player trigger).
    data.activatable = SaleZoneActivatable.new(self)

    if savegame ~= nil then
        local key = savegame.key .. ".saleZone"
        data.yardId = savegame.xmlFile:getInt(key .. "#yardId")
    end
end

function PlaceableSaleZone:onPostFinalizePlacement()
    if not self.isServer then return end
    local data = self[PlaceableSaleZone.KEY]
    if data.yardId ~= nil then return end

    -- Link to nearest yard.
    local x, _, z = getWorldTranslation(self.rootNode)
    local nearestYard = PlaceableSaleZone.findNearestYard(x, z)
    if nearestYard ~= nil then
        data.yardId = nearestYard.id
    end
end

function PlaceableSaleZone:onWriteStream(streamId, connection)
    local data = self[PlaceableSaleZone.KEY]
    local yardId = data ~= nil and data.yardId or 0
    streamWriteInt32(streamId, yardId)
end

function PlaceableSaleZone:onReadStream(streamId, connection)
    local data = self[PlaceableSaleZone.KEY]
    if data == nil then return end
    local yardId = streamReadInt32(streamId)
    if yardId ~= 0 then
        data.yardId = yardId
    end
end

function PlaceableSaleZone:onDelete()
    local data = self[PlaceableSaleZone.KEY]
    if data == nil then return end

    if data.playerTriggerNode ~= nil then
        removeTrigger(data.playerTriggerNode)
        data.playerTriggerNode = nil
    end
    if data.vehicleTriggerNode ~= nil then
        removeTrigger(data.vehicleTriggerNode)
        data.vehicleTriggerNode = nil
    end
    if data.activatable ~= nil then
        g_currentMission.activatableObjectsSystem:removeActivatable(data.activatable)
        data.activatable = nil
    end
end

-- ---------------------------------------------------------------------------
-- Trigger callbacks (VehicleSellingPoint pattern)
-- ---------------------------------------------------------------------------

function PlaceableSaleZone:saleZonePlayerTriggerCallback(triggerId, otherId, onEnter, onLeave, onStay)
    if not (onEnter or onLeave) then return end
    if g_localPlayer == nil or otherId ~= g_localPlayer.rootNode then return end

    local data = self[PlaceableSaleZone.KEY]
    if data == nil or data.activatable == nil then return end

    if onEnter then
        g_currentMission.activatableObjectsSystem:addActivatable(data.activatable)
    elseif onLeave then
        g_currentMission.activatableObjectsSystem:removeActivatable(data.activatable)
    end
end

function PlaceableSaleZone:saleZoneVehicleTriggerCallback(triggerId, otherId, onEnter, onLeave, onStay, otherShapeId)
    if otherShapeId == nil or not (onEnter or onLeave) then return end
    local data = self[PlaceableSaleZone.KEY]
    if data == nil then return end

    if onEnter then
        data.vehicleShapesInRange[otherShapeId] = true
    elseif onLeave then
        data.vehicleShapesInRange[otherShapeId] = nil
    end
end

-- ---------------------------------------------------------------------------
-- Determine current vehicles in the zone
-- ---------------------------------------------------------------------------

function PlaceableSaleZone:determineCurrentVehicles()
    local data = self[PlaceableSaleZone.KEY]
    if data == nil then return {} end

    local vehicles = {}
    local playerFarmId = g_currentMission:getFarmId()
    if playerFarmId == FarmManager.SPECTATOR_FARM_ID then return vehicles end

    for shapeId, inRange in pairs(data.vehicleShapesInRange) do
        if not inRange or not entityExists(shapeId) then
            data.vehicleShapesInRange[shapeId] = nil
        else
            local vehicle = g_currentMission.nodeToObject[shapeId]
            if vehicle ~= nil and vehicle:isa(Vehicle) then
                local rootVehicle = vehicle.rootVehicle
                local subVehicles = rootVehicle:getChildVehicles()
                for _, subVehicle in ipairs(subVehicles) do
                    if subVehicle:getOwnerFarmId() == playerFarmId then
                        table.addElement(vehicles, subVehicle)
                    end
                end
            end
        end
    end

    return vehicles
end

-- ---------------------------------------------------------------------------
-- Public accessors (used by SaleZoneActivatable)
-- ---------------------------------------------------------------------------

function PlaceableSaleZone.getLinkedYardId(placeable)
    local data = placeable[PlaceableSaleZone.KEY]
    return data and data.yardId
end

-- ---------------------------------------------------------------------------
-- Savegame persistence
-- ---------------------------------------------------------------------------

function PlaceableSaleZone.registerSavegameXMLPaths(schema, basePath)
    schema:register(XMLValueType.INT, basePath .. ".saleZone#yardId", "Linked yard id")
end

function PlaceableSaleZone:saveToXMLFile(xmlFile, key, usedModNames)
    local data = self[PlaceableSaleZone.KEY]
    if data ~= nil and data.yardId ~= nil then
        xmlFile:setInt(key .. ".saleZone#yardId", data.yardId)
    end
end

function PlaceableSaleZone:loadFromXMLFile(xmlFile, key)
    local data = self[PlaceableSaleZone.KEY]
    if data ~= nil then
        data.yardId = xmlFile:getInt(key .. ".saleZone#yardId")
    end
end
