Property = {
    property_id = nil,
    propertyData = nil,

    shell = nil,
    shellData = nil,
    inProperty = false,
    shellObj = nil,

    has_access = false,
    owner = false,

    storageTarget = nil,
    clothingTarget = nil,
    furnitureObjs = {},

    garageZone = nil,
    doorbellPool = {},

    entranceTarget = nil, -- needed for ox target
    exitTarget = nil,     -- needed for ox target

    blip = nil,
}
Property.__index = Property

function Property:new(propertyData)
    local self = setmetatable({}, Property)
    self.property_id = tostring(propertyData.property_id)

    -- Remove furnitures from property data for memory purposes
    propertyData.furnitures = {}
    self.propertyData = propertyData

    local citizenid = PlayerData.citizenid

    self.owner = propertyData.owner == citizenid
    self.has_access = lib.table.contains(self.propertyData.has_access, citizenid)

    if propertyData.apartment then
        local aptName = propertyData.apartment
        local apartment = ApartmentsTable[aptName]

        if not apartment and Config.Apartments[aptName] then
            ApartmentsTable[aptName] = Apartment:new(Config.Apartments[aptName])
            apartment = ApartmentsTable[aptName]
        elseif not apartment then
            Debug(aptName .. " not found in Config")
            return
        end

        apartment:AddProperty(self.property_id)
    else
        self:RegisterPropertyEntrance()
        self:RegisterGarageZone()
    end

    return self
end

function Property:GetDoorCoords()
    local coords = nil

    local dataApartment = self.propertyData.apartment
    if dataApartment then
        local apartment = dataApartment
        coords = Config.Apartments[apartment].door
    else
        coords = self.propertyData.door_data
    end

    return coords
end

function Property:CreateShell()
    local coords = self:GetDoorCoords()

    coords = vec3(coords.x, coords.y, coords.z - 25.0)
    self.shell = Shell:CreatePropertyShell(self.propertyData.shell, coords)

    self.shellObj = self.shell.entity

    local doorOffset = self.shellData.doorOffset
    local offset = GetOffsetFromEntityInWorldCoords(self.shellObj, doorOffset.x, doorOffset.y, doorOffset.z)
    self:RegisterDoorZone(offset)

    SetEntityCoordsNoOffset(cache.ped, offset.x, offset.y, offset.z, false, false, true)
    SetEntityHeading(cache.ped, self.shellData.doorOffset.h)
end

function Property:RegisterDoorZone(offset)
    local function leave()
        self:LeaveShell()
    end

    local function checkDoor()
        self:OpenDoorbellMenu()
    end

    local coords = offset
    local size = vector3(1.0, self.shellData.doorOffset.width, 3.0)
    local heading = self.shellData.doorOffset.h

    self.exitTarget = Framework[Config.Target].AddDoorZoneInside(coords, size, heading, leave, checkDoor)
end

function Property:RegisterPropertyEntrance()
    local door = self.propertyData.door_data
    local size = vector3(door.length, door.width, 2.5)
    local heading = door.h
    --Can be anon functions but I like to keep them named its more readable
    local function enter()
        TriggerServerEvent("ps-housing:server:enterProperty", self.property_id)
    end

    local function raid()
        TriggerServerEvent("ps-housing:server:raidProperty", self.property_id)
    end

    local function showcase()
        TriggerServerEvent("ps-housing:server:showcaseProperty", self.property_id)
    end

    local function showData()
        local data = lib.callback.await("ps-housing:cb:getPropertyInfo", source, self.property_id)
        if not data then return end

        local content = "**Ejer:** " .. data.owner .. "  \n" .. "**Beskrivelse:** " .. data.description .. "  \n" .. "**Gade:** " .. data.street .. "  \n" .. "**Region:** " .. data.region .. "  \n" .. "**Interiør:** " .. data.shell .. "  \n" .. "**Til salg:** " .. (data.for_sale and "Ja" or "Nej")

        if data.for_sale then
            content = content .. "  \n" .. "**Pris:** " .. data.price.. ",-"
        end

        lib.alertDialog({
            header = data.street .. " " .. data.property_id,
            content = content,
            centered = true,
        })
    end

    local targetName = string.format("%s_%s", self.propertyData.street, self.property_id)

    self.entranceTarget = Framework[Config.Target].AddEntrance(door, size, heading, self.property_id, enter, raid, showcase, showData, targetName)

    if self.owner or self.has_access then
        self:CreateBlip()
    end
end

function Property:UnregisterPropertyEntrance()
    if not self.entranceTarget then return end

    Framework[Config.Target].RemoveTargetZone(self.entranceTarget)
    self.entranceTarget = nil
end

function Property:RegisterGarageZone()
    if not next(self.propertyData.garage_data) then return end

    if not (self.has_access or self.owner) or not self.owner then
        return
    end

    local garageData = self.propertyData.garage_data
    local garageName = string.format("property-%s-garage", self.property_id)

    local data = {
        takeVehicle = {
            x = garageData.x,
            y = garageData.y,
            z = garageData.z,
            w = garageData.h
        },
        type = "house",
        label = self.propertyData.street .. self.property_id .. " Garage",
    }

    TriggerEvent("qb-garages:client:addHouseGarage", self.property_id, data)

    self.garageZone = lib.zones.box({
        coords = vec3(garageData.x, garageData.y, garageData.z),
        size = vector3(garageData.length + 5.0, garageData.width + 5.0, 3.5),
        rotation = garageData.h,
        debug = Config.DebugMode,
        onEnter = function()
            TriggerEvent('qb-garages:client:setHouseGarage', self.property_id, true)
        end,
    })
end

function Property:UnregisterGarageZone()
    if not self.garageZone then return end

    TriggerEvent("qb-garages:client:removeHouseGarage", self.property_id)

    self.garageZone:remove()
    self.garageZone = nil
end

function Property:EnterShell()
    DoScreenFadeOut(250)
    TriggerServerEvent("InteractSound_SV:PlayOnSource", "houses_door_open", 0.25)
    Wait(250)

    self.inProperty = true

    self.shellData = Config.Shells[self.propertyData.shell]
    self:CreateShell()

    self:LoadFurnitures()

    self:GiveMenus()

    Wait(250)
    DoScreenFadeIn(250)
end

function Property:LeaveShell()
    if not self.inProperty then return end

    DoScreenFadeOut(250)
    TriggerServerEvent("InteractSound_SV:PlayOnSource", "houses_door_open", 0.25)
    Wait(250)

    local coords = self:GetDoorCoords()
    SetEntityCoordsNoOffset(cache.ped, coords.x, coords.y, coords.z, false, false, true)

    TriggerServerEvent("ps-housing:server:leaveProperty", self.property_id)

    self:UnloadFurnitures()
    self.propertyData.furnitures = {}

    self.shell:DespawnShell()
    self.shell = nil
    if self.exitTarget then
        Framework[Config.Target].RemoveTargetZone(self.exitTarget)
        self.exitTarget = nil
    end

    self:RemoveBlip()

    self:RemoveMenus()

    self.doorbellPool = {}

    self.inProperty = false
    Wait(250)
    DoScreenFadeIn(250)
end

function Property:GiveMenus()
    if not self.inProperty then return end

    local accessAndConfig = self.has_access and Config.AccessCanEditFurniture

    if self.owner or accessAndConfig then
        Framework[Config.Radial].AddRadialOption(
            "furniture_menu",
            "Indretning",
            "house-user",
            function()
                Modeler:OpenMenu(self.property_id)
            end,
            "ps-housing:client:openFurnitureMenu",
            { propertyId = self.property_id }
        )
    end

    if self.owner then
        Framework[Config.Radial].AddRadialOption(
            "access_menu",
            "Ejendomsadgang",
            "key",
            function()
                self:ManageAccessMenu()
            end,
            "ps-housing:client:openManagePropertyAccessMenu",
            { propertyId = self.property_id }
        )
    end
end

function Property:RemoveMenus()
    if not self.inProperty then return end

    Framework[Config.Radial].RemoveRadialOption("furniture_menu")

    if self.owner then
        Framework[Config.Radial].RemoveRadialOption("access_menu")
    end
end

function Property:ManageAccessMenu()
    if not self.inProperty then return end

    if not self.owner then
        Framework[Config.Notify].Notify("Kun ejeren kan gøre dette.", "error")
        return
    end

    --Fuck qb-menu
    local id = "property-" .. self.property_id .. "-access"
    local menu = {
        id = id,
        title = "Administrer adgang",
        options = {},
    }

    menu.options[#menu.options + 1] = {
        title = "Giv Adgang",
        onSelect = function()
            self:GiveAccessMenu()
        end,
    }

    menu.options[#menu.options + 1] = {
        title = "Fjern Adgang",
        onSelect = function()
            self:RevokeAccessMenu()
        end,
    }

    lib.registerContext(menu)
    lib.showContext(id)
end

function Property:GiveAccessMenu()
    if not self.inProperty then return end

    if not self.owner then
        return
    end

    local id = "property-" .. self.property_id .. "-access-give"
    local menu = {
        id = id,
        title = "Giv Adgang",
        options = {},
    }

    local players = lib.callback.await("ps-housing:cb:getPlayersInProperty", source, self.property_id) or {}

    if #players > 0 then
        for i = 1, #players do
            local v = players[i]
            menu.options[#menu.options + 1] = {
                title = v.name,
                description = "Giv Adgang",
                onSelect = function()
                    TriggerServerEvent("ps-housing:server:addAccess", self.property_id, v.src)
                end,
            }
        end

        lib.registerContext(menu)
        lib.showContext(id)
    else
        Framework[Config.Notify].Notify("Der er ingen i boligen", "error")
    end
end

function Property:RevokeAccessMenu()
    if not self.owner then
        return
    end

    local id = "property-" .. self.property_id .. "-access-already"
    local alreadyAccessMenu = {
        id = id,
        title = "Revoke Access",
        options = {},
    }

    local playersWithAccess = lib.callback.await("ps-housing:cb:getPlayersWithAccess", source, self.property_id) or {}

    -- only stores names and citizenids in a table so if their offline you can still remove them
    if #playersWithAccess > 0 then
        for i = 1, #playersWithAccess do
            local v = playersWithAccess[i]
            alreadyAccessMenu.options[#alreadyAccessMenu.options + 1] = {
                title = v.name,
                description = "Fjern Adgang",
                onSelect = function()
                    TriggerServerEvent("ps-housing:server:removeAccess", self.property_id, v.citizenid)
                end,
            }
        end

        lib.registerContext(alreadyAccessMenu)
        lib.showContext(id)
    else
        Framework[Config.Notify].Notify("Ingen har adgang til denne ejendom", "error")
    end
end

function Property:OpenDoorbellMenu()
    if not self.inProperty then return end

    if not next(self.doorbellPool) then
        Framework[Config.Notify].Notify("Der er ingen ved døren", "error")
        return
    end

    local id = string.format("property-%s-doorbell", self.property_id)
    local menu = {
        id = id,
        title = "Gæster ved døren",
        options = {},
    }

    for k, v in pairs(self.doorbellPool) do
        menu.options[#menu.options + 1] = {
            title = v.name,
            onSelect = function()
                TriggerServerEvent(
                    "ps-housing:server:doorbellAnswer",
                    { targetSrc = v.src, property_id = self.property_id }
                )
            end,
        }
    end

    lib.registerContext(menu)
    lib.showContext(id)
end

function Property:LoadFurniture(furniture)
    local coords = GetOffsetFromEntityInWorldCoords(self.shellObj, furniture.position.x, furniture.position.y, furniture.position.z)
    local hash = furniture.object

    lib.requestModel(hash)
    local entity = CreateObjectNoOffset(hash, coords.x, coords.y, coords.z, false, true, false)
    SetModelAsNoLongerNeeded(hash)
    SetEntityRotation(entity, furniture.rotation.x, furniture.rotation.y, furniture.rotation.z, 2, true)

    if furniture.type == 'door' and Config.DynamicDoors then
        Debug("Object: "..furniture.label.." wont be frozen")
    else
        FreezeEntityPosition(entity, true)
    end

    if furniture.type and Config.FurnitureTypes[furniture.type] then
        Config.FurnitureTypes[furniture.type](entity, self.property_id, self.propertyData.shell)
    end

    self.furnitureObjs[#self.furnitureObjs + 1] = {
        entity = entity,
        id = furniture.id,
        label = furniture.label,
        object = furniture.object,
        position = {
            x = coords.x,
            y = coords.y,
            z = coords.z,
        },
        rotation = furniture.rotation,
        type = furniture.type,
    }
end

function Property:LoadFurnitures()
    self.propertyData.furnitures = lib.callback.await('ps-housing:cb:getFurnitures', source, self.property_id) or {}
    
    for i = 1, #self.propertyData.furnitures do
        local furniture = self.propertyData.furnitures[i]
        self:LoadFurniture(furniture)
    end
end

function Property:UnloadFurniture(furniture, index)
    local entity = furniture?.entity
    if not entity then 
        for i = 1, #self.furnitureObjs do
            if self.furnitureObjs[i]?.id and furniture?.id and self.furnitureObjs[i].id == furniture.id then
                entity = self.furnitureObjs[i]?.entity
                break
            end
        end
    end

    if self.clothingTarget == entity or self.storageTarget == entity then
        Framework[Config.Target].RemoveTargetEntity(entity)

        if self.clothingTarget == entity then
            self.clothingTarget = nil
        elseif self.storageTarget == entity then
            self.storageTarget = nil
        end
    end

    if index and self.furnitureObjs?[index] then
        table.remove(self.furnitureObjs, index)
    else 
        for i = 1, #self.furnitureObjs do
            if self.furnitureObjs[i]?.id and furniture?.id and self.furnitureObjs[i].id == furniture.id then
                table.remove(self.furnitureObjs, i)
                break
            end
        end
    end

    DeleteObject(entity)
end

function Property:UnloadFurnitures()
    for i = 1, #self.furnitureObjs do
        local furniture = self.furnitureObjs[i]
        self:UnloadFurniture(furniture, i)
    end
    self.furnitureObjs = {}
end

function Property:CreateBlip()
    local door_data = self.propertyData.door_data
    local blip = AddBlipForCoord(door_data.x, door_data.y, door_data.z)
    if self.propertyData.garage_data.x ~= nil then
        SetBlipSprite(blip, 492)
    else
        SetBlipSprite(blip, 40)
    end
    SetBlipScale(blip, 0.8)
    SetBlipColour(blip, 2)
    SetBlipAsShortRange(blip, true)
    BeginTextCommandSetBlipName("STRING")
    AddTextComponentString(self.propertyData.street .. " " .. self.property_id)
    EndTextCommandSetBlipName(blip)
    self.blip = blip
end

function Property:RemoveBlip()
    if not self.blip then return end
    RemoveBlip(self.blip)
    self.blip = nil
end

function Property:RemoveProperty()
    local targetName = string.format("%s_%s", self.propertyData.street, self.property_id)

    Framework[Config.Target].RemoveTargetZone(targetName)

    self:RemoveBlip()

    self:LeaveShell()

    --@@ comeback to this
    -- Think it works now
    if self.propertyData.apartment then
        ApartmentsTable[self.propertyData.apartment]:RemoveProperty()
    end

    self = nil
end

local function findFurnitureDifference(new, old)
    local added = {}
    local removed = {}

    for i = 1, #new do
        local found = false
        for j = 1, #old do
            if new[i].id == old[j].id then
                found = true
                break
            end
        end
        if not found then
            added[#added + 1] = new[i]
        end
    end

    for i = 1, #old do
        local found = false
        for j = 1, #new do
            if old[i].id == new[j].id then
                found = true
                break
            end
        end
        if not found then
            removed[#removed + 1] = old[i]
        end
    end

    return added, removed
end

-- I think this whole furniture sync is a bit shit, but I cbf thinking 
function Property:UpdateFurnitures(newFurnitures)
    if not self.inProperty then return end

    local oldFurnitures = self.propertyData.furnitures
    local added, removed = findFurnitureDifference(newFurnitures, oldFurnitures)

    for i = 1, #added do
        local furniture = added[i]
        self:LoadFurniture(furniture)
    end

    for i = 1, #removed do
        local furniture = removed[i]
        self:UnloadFurniture(furniture)
    end

    self.propertyData.furnitures = newFurnitures

    Modeler:UpdateFurnitures()
end

function Property:UpdateDescription(newDescription)
    self.propertyData.description = newDescription
end

function Property:UpdatePrice(newPrice)
    self.propertyData.price = newPrice
end

function Property:UpdateForSale(forSale)
    self.propertyData.for_sale = forSale
end

function Property:UpdateShell(newShell)
    self:LeaveShell()
    self.propertyData.shell = newShell

    if self.inProperty then
        self:EnterShell()
    end
end

function Property:UpdateOwner(newOwner)
    self.propertyData.owner = newOwner

    local citizenid = PlayerData.citizenid

    self.owner = newOwner == citizenid

    self:UnregisterGarageZone()
    self:RegisterGarageZone()

    self:CreateBlip()

    if not self.inProperty then return end

    self:RemoveMenus()
    self:GiveMenus()
end

function Property:UpdateImgs(newImgs)
    self.propertyData.imgs = newImgs
end

function Property:UpdateDoor(newDoor, newStreet, newRegion)
    self.propertyData.door_data = newDoor
    self.propertyData.street = newStreet
    self.propertyData.region = newRegion

    self:UnregisterPropertyEntrance()
    self:RegisterPropertyEntrance()
end

function Property:UpdateHas_access(newHas_access)
    local citizenid = PlayerData.citizenid
    self.propertyData.has_access = newHas_access
    self.has_access = lib.table.contains(newHas_access, citizenid)

    if not self.inProperty then return end

    self:RemoveMenus()
    self:GiveMenus()
end

function Property:UpdateGarage(newGarage)
    self.propertyData.garage_data = newGarage

    self:UnregisterGarageZone()
    self:RegisterGarageZone()
end

function Property:UpdateApartment(newApartment)
    self:LeaveShell()

    local oldAptName = self.propertyData.apartment
    local oldApt = ApartmentsTable[oldAptName]
    if oldApt then
        oldApt:RemoveProperty(self.property_id)
    end

    self.propertyData.apartment = newApartment

    local newApt = ApartmentsTable[newApartment]

    if newApt then
        newApt:AddProperty(self.property_id)
    end

    TriggerEvent("ps-housing:client:updateApartment", oldAptName, newApartment)
end

function Property.Get(property_id)
    return PropertiesTable[tostring(property_id)]
end

RegisterNetEvent("ps-housing:client:enterProperty", function(property_id)
    local property = Property.Get(property_id)

    if property ~= nil then
        property:EnterShell()
    end
end)

RegisterNetEvent("ps-housing:client:updateDoorbellPool", function(property_id, data)
    local property = Property.Get(property_id)
    property.doorbellPool = data
end)

RegisterNetEvent("ps-housing:client:updateFurniture", function(property_id, furnitures)
    local property = Property.Get(property_id)
    if not property then return end
    property:UpdateFurnitures(furnitures)
end)

RegisterNetEvent("ps-housing:client:updateProperty", function(type, property_id, data)
    local property = Property.Get(property_id)

    if not property then return end

    property[type](property, data)

    TriggerEvent("ps-housing:client:updatedProperty", property_id)
end)

RegisterNetEvent("ps-housing:client:openFurnitureMenu", function(data)
    Modeler:OpenMenu(data.options.propertyId)
end)

RegisterNetEvent("ps-housing:client:openManagePropertyAccessMenu", function(data)
    local property = Property.Get(data.options.propertyId)
    if not property then return end

    property:ManageAccessMenu()
end)