local reverseCamera

local function getRgbColors()
    local colors = {
        hair = {},
        makeUp = {}
    }

    for i = 0, GetNumHairColors() - 1 do
        colors.hair[i+1] = {GetPedHairRgbColor(i)}
    end

    for i = 0, GetNumMakeupColors() - 1 do
        colors.makeUp[i+1] = {GetPedMakeupRgbColor(i)}
    end

    return colors
end

local playerAppearance

local function getAppearance()
    if not playerAppearance then
        playerAppearance = client.getPedAppearance(cache.ped)
    end

    return playerAppearance
end
client.getAppearance = getAppearance

local function addToBlacklist(item, drawable, drawableId, blacklistSettings)
    if drawable == drawableId and item.textures then
        for i = 1, #item.textures do
            blacklistSettings.textures[#blacklistSettings.textures + 1] = item.textures[i]
        end
    end
    if not item.textures or #item.textures == 0 then
        blacklistSettings.drawables[#blacklistSettings.drawables + 1] = drawable
    end
end

local function listContains(items, item)
    for i = 1, #items do
        if items[i] == item then
            return true
        end
    end
    return false
end

local function listContainsAny(items, containedItems)
    for i = 1, #items do
        if listContains(containedItems, items[i]) then
            return true
        end
    end
    return false
end

local function allowedForPlayer(item, allowedAces)
    return (item.jobs and listContains(item.jobs, client.job.name)) or (item.gangs and listContains(item.gangs, client.gang.name)) or (item.aces and listContainsAny(item.aces, allowedAces) or (item.citizenids and listContains(item.citizenids, client.citizenid)))
end

local function filterPedModelsForPlayer(pedConfigs)
    local playerPeds = {}
    local allowedAces = lib.callback.await("illenium-appearance:server:GetPlayerAces", false)

    for i = 1, #pedConfigs do
        local config = pedConfigs[i]
        if (not config.jobs and not config.gangs and not config.aces and not config.citizenids) or allowedForPlayer(config, allowedAces) then
            for j = 1, #config.peds do
                playerPeds[#playerPeds + 1] = config.peds[j]
            end
        end
    end
    return playerPeds
end

local function filterTattoosByGender(tattoos)
    local filtered = {}
    local gender = client.getPedDecorationType()
    for k, v in pairs(tattoos) do
        filtered[k] = {}
        for i = 1, #v do
            local tattoo = v[i]
            if tattoo["hash" .. gender:gsub("^%l", string.upper)] ~= "" then
                filtered[k][#filtered[k] + 1] = tattoo
            end
        end
    end
    return filtered
end

local function filterBlacklistSettings(items, drawableId)
    local blacklistSettings = {
        drawables = {},
        textures = {}
    }

    local allowedAces = lib.callback.await("illenium-appearance:server:GetPlayerAces", false)

    for i = 1, #items do
        local item = items[i]
        if not allowedForPlayer(item, allowedAces) and item.drawables then
            for j = 0, #item.drawables do
                addToBlacklist(item, item.drawables[j], drawableId, blacklistSettings)
            end
        end
    end

    return blacklistSettings
end

local function componentBlacklistMap(gender, componentId)
    local genderSettings = Config.Blacklist[gender].components
    if componentId == 1 then
        return genderSettings.masks
    elseif componentId == 3 then
        return genderSettings.upperBody
    elseif componentId == 4 then
        return genderSettings.lowerBody
    elseif componentId == 5 then
        return genderSettings.bags
    elseif componentId == 6 then
        return genderSettings.shoes
    elseif componentId == 7 then
        return genderSettings.scarfAndChains
    elseif componentId == 8 then
        return genderSettings.shirts
    elseif componentId == 9 then
        return genderSettings.bodyArmor
    elseif componentId == 10 then
        return genderSettings.decals
    elseif componentId == 11 then
        return genderSettings.jackets
    end

    return {}
end

local function propBlacklistMap(gender, propId)
    local genderSettings = Config.Blacklist[gender].props

    if propId == 0 then
        return genderSettings.hats
    elseif propId == 1 then
        return genderSettings.glasses
    elseif propId == 2 then
        return genderSettings.ear
    elseif propId == 6 then
        return genderSettings.watches
    elseif propId == 7 then
        return genderSettings.bracelets
    end

    return {}
end

local function getComponentSettings(ped, componentId)
    local drawableId = GetPedDrawableVariation(ped, componentId)
    local gender = client.getPedDecorationType()

    local blacklistSettings = {
        drawables = {},
        textures = {}
    }

    if client.isPedFreemodeModel(ped) then
        blacklistSettings = filterBlacklistSettings(componentBlacklistMap(gender, componentId), drawableId)
    end

    return {
        component_id = componentId,
        drawable = {
            min = 0,
            max = GetNumberOfPedDrawableVariations(ped, componentId) - 1
        },
        texture = {
            min = 0,
            max = GetNumberOfPedTextureVariations(ped, componentId, drawableId) - 1
        },
        blacklist = blacklistSettings
    }
end
client.getComponentSettings = getComponentSettings

local function getPropSettings(ped, propId)
    local drawableId = GetPedPropIndex(ped, propId)
    local gender = client.getPedDecorationType()

    local blacklistSettings = {
        drawables = {},
        textures = {}
    }

    if client.isPedFreemodeModel(ped) then
        blacklistSettings = filterBlacklistSettings(propBlacklistMap(gender, propId), drawableId)
    end

    local settings = {
        prop_id = propId,
        drawable = {
            min = -1,
            max = GetNumberOfPedPropDrawableVariations(ped, propId) - 1
        },
        texture = {
            min = -1,
            max = GetNumberOfPedPropTextureVariations(ped, propId, drawableId) - 1
        },
        blacklist = blacklistSettings
    }
    return settings
end
client.getPropSettings = getPropSettings

local function getHairSettings(ped)
    local colors = getRgbColors()
    local gender = client.getPedDecorationType()
    local blacklistSettings = {
        drawables = {},
        textures = {}
    }

    if client.isPedFreemodeModel(ped) then
        blacklistSettings = filterBlacklistSettings(Config.Blacklist[gender].hair, GetPedDrawableVariation(ped, 2))
    end

    local settings = {
        style = {
            min = 0,
            max = GetNumberOfPedDrawableVariations(ped, 2) - 1
        },
        color = {
            items = colors.hair
        },
        highlight = {
            items = colors.hair
        },
        texture = {
            min = 0,
            max = GetNumberOfPedTextureVariations(ped, 2, GetPedDrawableVariation(ped, 2)) - 1
        },
        blacklist = blacklistSettings
    }

    return settings
end
client.getHairSettings = getHairSettings

local function getAppearanceSettings()
    local ped = {
        model = {
            items = filterPedModelsForPlayer(Config.Peds.pedConfig)
        }
    }

    local tattoos = {
        items = filterTattoosByGender(Config.Tattoos),
        opacity = {
            min = 0.1,
            max = 1,
            factor = 0.1,
        }
    }

    local components = {}
    for i = 1, #constants.PED_COMPONENTS_IDS do
        components[i] = getComponentSettings(cache.ped, constants.PED_COMPONENTS_IDS[i])
    end

    local props = {}
    for i = 1, #constants.PED_PROPS_IDS do
        props[i] = getPropSettings(cache.ped, constants.PED_PROPS_IDS[i])
    end

    local headBlend = {
        shapeFirst = {
            min = 0,
            max = 45
        },
        shapeSecond = {
            min = 0,
            max = 45
        },
        shapeThird = {
            min = 0,
            max = 45
        },
        skinFirst = {
            min = 0,
            max = 45
        },
        skinSecond = {
            min = 0,
            max = 45
        },
        skinThird = {
            min = 0,
            max = 45
        },
        shapeMix = {
            min = 0,
            max = 1,
            factor = 0.1,
        },
        skinMix = {
            min = 0,
            max = 1,
            factor = 0.1,
        },
        thirdMix = {
            min = 0,
            max = 1,
            factor = 0.1,
        }
    }

    local size = #constants.FACE_FEATURES
    local faceFeatures = table.create(0, size)
    for i = 1, size do
        local feature = constants.FACE_FEATURES[i]
        faceFeatures[feature] = { min = -1, max = 1, factor = 0.1}
    end

    local colors = getRgbColors()

    local colorMap = {
        beard = colors.hair,
        eyebrows = colors.hair,
        chestHair = colors.hair,
        makeUp = colors.makeUp,
        blush = colors.makeUp,
        lipstick = colors.makeUp,
    }

    size = #constants.HEAD_OVERLAYS
    local headOverlays = table.create(0, size)

    for i = 1, size do
        local overlay = constants.HEAD_OVERLAYS[i]
        local settings = {
            style = {
                min = 0,
                max = GetPedHeadOverlayNum(i - 1) - 1
            },
            opacity = {
                min = 0,
                max = 1,
                factor = 0.1,
            }
        }

        if colorMap[overlay] then
            settings.color = {
                items = colorMap[overlay]
            }
        end

        headOverlays[overlay] = settings
    end

    local eyeColor = {
        min = 0,
        max = 30
    }

    return {
        ped = ped,
        components = components,
        props = props,
        headBlend = headBlend,
        faceFeatures = faceFeatures,
        headOverlays = headOverlays,
        hair = getHairSettings(cache.ped),
        eyeColor = eyeColor,
        tattoos = tattoos
    }
end
client.getAppearanceSettings = getAppearanceSettings

local config
function client.getConfig() return config end

local isCameraInterpolating
local currentCamera
local cameraHandle
local function setCamera(key)
    if not isCameraInterpolating then
        if key ~= "current" then
            currentCamera = key
        end

        local coords, point = table.unpack(constants.CAMERAS[currentCamera])
        local reverseFactor = reverseCamera and -1 or 1

        if cameraHandle then
            local camCoords = GetOffsetFromEntityInWorldCoords(cache.ped, coords.x * reverseFactor, coords.y * reverseFactor, coords.z * reverseFactor)
            local camPoint = GetOffsetFromEntityInWorldCoords(cache.ped, point.x, point.y, point.z)
            local tmpCamera = CreateCameraWithParams("DEFAULT_SCRIPTED_CAMERA", camCoords.x, camCoords.y, camCoords.z, 0.0, 0.0, 0.0, 49.0, false, 0)

            PointCamAtCoord(tmpCamera, camPoint.x, camPoint.y, camPoint.z)
            SetCamActiveWithInterp(tmpCamera, cameraHandle, 1000, 1, 1)

            isCameraInterpolating = true

            CreateThread(function()
                repeat Wait(500)
                until not IsCamInterpolating(cameraHandle) and IsCamActive(tmpCamera)
                DestroyCam(cameraHandle, false)
                cameraHandle = tmpCamera
                isCameraInterpolating = false
            end)
        else
            local camCoords = GetOffsetFromEntityInWorldCoords(cache.ped, coords.x, coords.y, coords.z)
            local camPoint = GetOffsetFromEntityInWorldCoords(cache.ped, point.x, point.y, point.z)
            cameraHandle = CreateCameraWithParams("DEFAULT_SCRIPTED_CAMERA", camCoords.x, camCoords.y, camCoords.z, 0.0, 0.0, 0.0, 49.0, false, 0)

            PointCamAtCoord(cameraHandle, camPoint.x, camPoint.y, camPoint.z)
            SetCamActive(cameraHandle, true)
        end
    end
end
client.setCamera = setCamera

function client.rotateCamera(direction)
    if not isCameraInterpolating then
        local coords, point = table.unpack(constants.CAMERAS[currentCamera])
        local offset = constants.OFFSETS[currentCamera]
        local sideFactor = direction == "left" and 1 or -1
        local reverseFactor = reverseCamera and -1 or 1

        local camCoords = GetOffsetFromEntityInWorldCoords(
            cache.ped,
            (coords.x + offset.x) * sideFactor * reverseFactor,
            (coords.y + offset.y) * reverseFactor,
            coords.z
        )

        local camPoint = GetOffsetFromEntityInWorldCoords(cache.ped, point.x, point.y, point.z)
        local tmpCamera = CreateCameraWithParams("DEFAULT_SCRIPTED_CAMERA", camCoords.x, camCoords.y, camCoords.z, 0.0, 0.0, 0.0, 49.0, false, 0)

        PointCamAtCoord(tmpCamera, camPoint.x, camPoint.y, camPoint.z)
        SetCamActiveWithInterp(tmpCamera, cameraHandle, 1000, 1, 1)

        isCameraInterpolating = true

        CreateThread(function()
            repeat Wait(500)
            until not IsCamInterpolating(cameraHandle) and IsCamActive(tmpCamera)
            DestroyCam(cameraHandle, false)
            cameraHandle = tmpCamera
            isCameraInterpolating = false
        end)
    end
end

local playerCoords
local function pedTurn(ped, angle)
    reverseCamera = not reverseCamera
    local sequenceTaskId = OpenSequenceTask()
    if sequenceTaskId then
        TaskGoStraightToCoord(0, playerCoords.x, playerCoords.y, playerCoords.z, 8.0, -1, GetEntityHeading(ped) - angle, 0.1)
        TaskStandStill(0, -1)
        CloseSequenceTask(sequenceTaskId)
        ClearPedTasks(ped)
        TaskPerformSequence(ped, sequenceTaskId)
        ClearSequenceTask(sequenceTaskId)
    end
end
client.pedTurn = pedTurn

local function wearClothes(data, typeClothes)
    local dataClothes = constants.DATA_CLOTHES[typeClothes]
    local animationsOn = dataClothes.animations.on
    local components = dataClothes.components[client.getPedDecorationType()]
    local appliedComponents = data.components
    local props = dataClothes.props[client.getPedDecorationType()]
    local appliedProps = data.props

    RequestAnimDict(animationsOn.dict)
    while not HasAnimDictLoaded(animationsOn.dict) do
        Wait(0)
    end

    for i = 1, #components do
        local componentId = components[i][1]
        for j = 1, #appliedComponents do
            local applied = appliedComponents[j]
            if applied.component_id == componentId then
                SetPedComponentVariation(cache.ped, componentId, applied.drawable, applied.texture, 2)
            end
        end
    end

    for i = 1, #props do
        local propId = props[i][1]
        for j = 1, #appliedProps do
            local applied = appliedProps[j]
            if applied.prop_id == propId then
                SetPedPropIndex(cache.ped, propId, applied.drawable, applied.texture, true)
            end
        end
    end

    TaskPlayAnim(cache.ped, animationsOn.dict, animationsOn.anim, 3.0, 3.0, animationsOn.duration, animationsOn.move, 0, false, false, false)
end
client.wearClothes = wearClothes

local function removeClothes(typeClothes)
    local dataClothes = constants.DATA_CLOTHES[typeClothes]
    local animationsOff = dataClothes.animations.off
    local components = dataClothes.components[client.getPedDecorationType()]
    local props = dataClothes.props[client.getPedDecorationType()]

    RequestAnimDict(animationsOff.dict)
    while not HasAnimDictLoaded(animationsOff.dict) do
        Wait(0)
    end

    for i = 1, #components do
        local component = components[i]
        SetPedComponentVariation(cache.ped, component[1], component[2], 0, 2)
    end

    for i = 1, #props do
        ClearPedProp(cache.ped, props[i][1])
    end

    TaskPlayAnim(cache.ped, animationsOff.dict, animationsOff.anim, 3.0, 3.0, animationsOff.duration, animationsOff.move, 0, false, false, false)
end
client.removeClothes = removeClothes

local playerHeading
function client.getHeading() return playerHeading end

local callback
function client.startPlayerCustomization(cb, conf)
    playerAppearance = client.getPedAppearance(cache.ped)
    playerCoords = GetEntityCoords(cache.ped, true)
    playerHeading = GetEntityHeading(cache.ped)

    BackupPlayerStats()

    callback = cb
    config = conf
    reverseCamera = false
    isCameraInterpolating = false

    setCamera("default")
    SetNuiFocus(true, true)
    SetNuiFocusKeepInput(false)
    RenderScriptCams(true, false, 0, true, true)
    SetEntityInvincible(cache.ped, Config.InvincibleDuringCustomization)
    TaskStandStill(cache.ped, -1)

    if Config.HideRadar then DisplayRadar(false) end

    SendNuiMessage(json.encode({
        type = "appearance_display",
        payload = {
            asynchronous = Config.AsynchronousLoading
        }
    }))
end

function client.exitPlayerCustomization(appearance)
    RenderScriptCams(false, false, 0, true, true)
    DestroyCam(cameraHandle, false)
    SetNuiFocus(false, false)

    if Config.HideRadar then DisplayRadar(true) end

    ClearPedTasksImmediately(cache.ped)
    SetEntityInvincible(cache.ped, false)

    SendNuiMessage(json.encode({
        type = "appearance_hide",
        payload = {}
    }))

    if not appearance then
        client.setPlayerAppearance(getAppearance())
    else
        client.setPedTattoos(cache.ped, appearance.tattoos)
    end

    RestorePlayerStats()

    if callback then
        callback(appearance)
    end

    callback = nil
    config = nil
    playerAppearance = nil
    playerCoords = nil
    cameraHandle = nil
    currentCamera = nil
    reverseCamera = nil
    isCameraInterpolating = nil

end

AddEventHandler("onResourceStop", function(resource)
    if resource == GetCurrentResourceName() then
        SetNuiFocus(false, false)
        SetNuiFocusKeepInput(false)
    end
end)

exports("startPlayerCustomization", client.startPlayerCustomization)
