ZO_SceneGraphNode = ZO_Object:Subclass()

function ZO_SceneGraphNode:New(...)
    local obj = ZO_Object.New(self)
    obj:Initialize(...)
    return obj
end

function ZO_SceneGraphNode:Initialize(sceneGraph, name)
    self.sceneGraph = sceneGraph
    self.name = name
    self.translateX = 0
    self.translateY = 0
    self.translateZ = 0
    self.rotation = 0
    self.scale = 1
    self.anchorPoint = CENTER
    self:SetDirty(true)
end

function ZO_SceneGraphNode:GetSceneGraph()
    return self.sceneGraph
end

function ZO_SceneGraphNode:GetName()
    return self.name
end

function ZO_SceneGraphNode:SetParent(parent)
    if self.parent then
        self.parent:OnChildRemoved(self)
    end
    self.parent = parent
    if self.parent then
        self.parent:OnChildAdded(self)
    end
    self:SetDirty(true)
end

function ZO_SceneGraphNode:GetChildren()
    return self.children
end

function ZO_SceneGraphNode:SetDirty(dirty)
    self.dirty = dirty
    if dirty then
        self.sceneGraph:OnSceneNodeDirty()
    end
end

function ZO_SceneGraphNode:GetX()
    return self.translateX
end

function ZO_SceneGraphNode:AddX(dx)
    self:SetX(self.translateX + dx)
end

function ZO_SceneGraphNode:SetX(x)
    if self.translateX ~= x then
        self.translateX = x
        self:SetDirty(true)
    end
end

function ZO_SceneGraphNode:GetY()
    return self.translateY
end

function ZO_SceneGraphNode:AddY(dy)
    self:SetY(self.translateY + dy)
end

function ZO_SceneGraphNode:SetY(y)
    if self.translateY ~= y then
        self.translateY = y
        self:SetDirty(true)
    end
end

function ZO_SceneGraphNode:GetZ()
    return self.translateZ
end

function ZO_SceneGraphNode:SetZ(z)
    if self.translateZ ~= z then
        self.translateZ = z
        self:SetDirty(true)
    end
end

function ZO_SceneGraphNode:GetRotation()
    return self.rotation
end

function ZO_SceneGraphNode:AddRotation(radians)
    self:SetRotation(self.rotation + radians)
end

function ZO_SceneGraphNode:SetRotation(radians)
    if self.rotation ~= radians then
        self.rotation = radians % ZO_TWO_PI
        self:SetDirty(true)
    end
end

function ZO_SceneGraphNode:SetScale(scale)
    if self.scale ~= scale then
        self.scale = scale
        self:SetDirty(true)
    end
end

function ZO_SceneGraphNode:ComputeSizeForDepth(x, y, z, referenceCameraZ)
    local distanceFromCamera = self.translateZ + z
    local parent = self.parent
    while parent do
        distanceFromCamera = distanceFromCamera + parent.translateZ
        parent = parent.parent
    end
    if referenceCameraZ == nil then
        referenceCameraZ = self.sceneGraph:GetCameraZ()
    end
    distanceFromCamera = distanceFromCamera - referenceCameraZ
    local scale = 1
    if distanceFromCamera > 0 then
        scale = 1 / distanceFromCamera
    end

    return x / scale, y / scale
end

function ZO_SceneGraphNode:IsDirty()
    return self.dirty
end

function ZO_SceneGraphNode:AcquireWorkingMatrix()
    if not self.workingMatrix then
       self.workingMatrix = hmake Matrix33 { }
    end
    return self.workingMatrix
end

function ZO_SceneGraphNode:AcquireResultMatrix()
    if not self.resultMatrix then
        self.resultMatrix = hmake Matrix33 { }
    end
    return self.resultMatrix
end

function ZO_SceneGraphNode:GetWorldViewMatrix()
    return self.worldViewMatrix
end

function ZO_SceneGraphNode:BuildWorldViewMatrix()
    local resultMatrix : Matrix33
    self.finalRotation = self.rotation
    self.finalScale = self.scale
    self.finalTranslateZ = self.translateZ

    if self.scale ~= 1 then
        resultMatrix = self:AcquireResultMatrix()
        zo_setToScaleMatrix2D(resultMatrix, self.scale)
    end
    if self.translateX ~= 0 or self.translateY ~= 0 then
        if resultMatrix then
            local workingMatrix : Matrix33 = self:AcquireWorkingMatrix()
            zo_setToTranslationMatrix2D(workingMatrix, self.translateX, self.translateY)
            zo_matrixMultiply33x33(workingMatrix, resultMatrix, resultMatrix)
        else
            resultMatrix = self:AcquireResultMatrix()
            zo_setToTranslationMatrix2D(resultMatrix, self.translateX, self.translateY)
        end
    end
    if self.rotation ~= 0 then
        if resultMatrix then
            local workingMatrix : Matrix33 = self:AcquireWorkingMatrix()
            zo_setToRotationMatrix2D(workingMatrix, self.rotation)
            zo_matrixMultiply33x33(workingMatrix, resultMatrix, resultMatrix)
        else
            resultMatrix = self:AcquireResultMatrix()
            zo_setToRotationMatrix2D(resultMatrix, self.rotation)
        end
    end

    local parent = self.parent
    if parent then
        if resultMatrix then
            zo_matrixMultiply33x33(parent.worldViewMatrix, resultMatrix, resultMatrix)
        else
            resultMatrix = parent.worldViewMatrix
        end
        self.finalRotation = self.finalRotation + parent.finalRotation
        self.finalScale = self.finalScale * parent.finalScale
        self.finalTranslateZ = self.finalTranslateZ + parent.finalTranslateZ
    end

    if resultMatrix then
        self.worldViewMatrix = resultMatrix
    else
        self.worldViewMatrix = self:AcquireResultMatrix()
        zo_setToIdentityMatrix33(self.worldViewMatrix)
    end

end

local ANCHOR_TO_NORMALIZED_X =
{
    [TOPLEFT] = 0,
    [LEFT] = 0,
    [BOTTOMLEFT] = 0,
    [TOP] = 0.5,
    [CENTER] = 0.5,
    [BOTTOM] = 0.5,
    [TOPRIGHT] = 1,
    [RIGHT] = 1,
    [BOTTOMRIGHT] = 1,
}

local ANCHOR_TO_NORMALIZED_Y =
{
    [TOPLEFT] = 0,
    [TOP] = 0,
    [TOPRIGHT] = 0,
    [LEFT] = 0.5,
    [CENTER] = 0.5,
    [RIGHT] = 0.5,
    [BOTTOMLEFT] = 1,
    [BOTTOM] = 1,
    [BOTTOMRIGHT] = 1,
}

local DRAW_LEVEL_BASE = ZO_ABOVE_SCENEGRAPH_DRAW_LEVEL
local Z_TO_DRAW_LEVEL_FACTOR = 100 -- draw levels are integers, so this is effectively 2 digits of precision
local function ComputeDrawLevel(z)
    return DRAW_LEVEL_BASE - z * Z_TO_DRAW_LEVEL_FACTOR
end

function ZO_SceneGraphNode:Render()
    local customScale = GetUICustomScale()
    local worldViewMatrix : Matrix33 = self.worldViewMatrix
    local cameraPositionZ = self.sceneGraph:GetCameraZ()

    if self.textures then
        for _, textureControl in ipairs(self.textures) do
            local positionZ = textureControl.sceneZ
            local distanceFromCamera = self.finalTranslateZ + positionZ - cameraPositionZ
            if distanceFromCamera > 0 then
                local depthScale = 1 / (distanceFromCamera * customScale)
                local positionX = textureControl.sceneX
                local positionY = textureControl.sceneY

                local finalX, finalY = zo_matrixTransformPoint(worldViewMatrix, positionX, positionY)

                finalX = finalX * depthScale
                finalY = finalY * depthScale

                local anchorPoint = textureControl.sceneAnchorPoint
                textureControl:SetAnchor(anchorPoint, nil, CENTER, finalX, finalY)
                textureControl:SetScale(self.finalScale * depthScale * textureControl.sceneScale)
                textureControl:SetDrawLevel(ComputeDrawLevel(self.finalTranslateZ + positionZ))
                textureControl:SetHidden(textureControl.sceneHidden)

                if self.finalRotation ~= 0 and textureControl.sceneUseRotation then
                    textureControl:SetTextureRotation(self.finalRotation, ANCHOR_TO_NORMALIZED_X[anchorPoint], ANCHOR_TO_NORMALIZED_Y[anchorPoint])
                else
                    textureControl:SetTextureRotation(0)
                end
            else
                textureControl:SetHidden(true)
            end
        end
    end

    if self.composites then
        for _, compositeControl in ipairs(self.composites) do
            local positionZ = compositeControl.sceneZ
            local distanceFromCamera = self.finalTranslateZ + positionZ - cameraPositionZ
            if distanceFromCamera > 0 then
                local depthScale = 1 / (distanceFromCamera * customScale)
                local positionX = compositeControl.sceneX
                local positionY = compositeControl.sceneY

                local finalX, finalY = zo_matrixTransformPoint(worldViewMatrix, positionX, positionY)

                finalX = finalX * depthScale
                finalY = finalY * depthScale

                compositeControl:SetAnchor(compositeControl.sceneAnchorPoint, nil, CENTER, finalX, finalY)
                compositeControl:SetScale(self.finalScale * depthScale * compositeControl.sceneScale)
                compositeControl:SetDrawLevel(ComputeDrawLevel(self.finalTranslateZ + positionZ))
                compositeControl:SetHidden(compositeControl.sceneHidden)
            else
                compositeControl:SetHidden(true)
            end
        end
    end

    if self.lines then
        for _, lineControl in ipairs(self.lines) do
            local distanceFromCamera = self.finalTranslateZ + lineControl.sceneZ - cameraPositionZ
            if distanceFromCamera > 0 then
                local thickness = self.finalScale * lineControl.sceneThickness / distanceFromCamera / customScale
                lineControl:SetThickness(thickness)
                lineControl:SetDrawLevel(ComputeDrawLevel(self.finalTranslateZ + lineControl.sceneZ))
                lineControl:SetHidden(lineControl.sceneHidden)
            else
                lineControl:SetHidden(true)
            end
        end
    end
    self.dirty = false
end

function ZO_SceneGraphNode:TransformPoint(inputPointX, inputPointY, referencePlaneZ)
    if not self.finalTranslateZ then
        -- waiting for control to be rendered at least once
        return nil
    end
    local distanceFromCamera = self.finalTranslateZ + referencePlaneZ - self.sceneGraph:GetCameraZ()
    if distanceFromCamera <= 0 then
        return nil
    end

    local inputX, inputY = zo_matrixTransformPoint(self.worldViewMatrix, inputPointX, inputPointY)

    local depthScale = 1 / distanceFromCamera
    inputX = inputX * depthScale
    inputY = inputY * depthScale

    local customScale = GetUICustomScale()
    inputX = inputX / customScale
    inputY = inputY / customScale

    local canvasCenterX, canvasCenterY = self.sceneGraph:GetCanvasControl():GetCenter()
    return inputX + canvasCenterX, inputY + canvasCenterY
end

function ZO_SceneGraphNode:InvertPoint(screenPointX, screenPointY, referencePlaneZ)
    if not self.finalTranslateZ then
        -- waiting for control to be rendered at least once
        return nil
    end
    local distanceFromCamera = self.finalTranslateZ + referencePlaneZ - self.sceneGraph:GetCameraZ()
    if distanceFromCamera <= 0 then
        return nil
    end

    local canvasCenterX, canvasCenterY = self.sceneGraph:GetCanvasControl():GetCenter()
    local inputX, inputY = screenPointX - canvasCenterX, screenPointY - canvasCenterY

    local customScale = GetUICustomScale()
    inputX = inputX * customScale
    inputY = inputY * customScale

    local depthScale = 1 / distanceFromCamera
    inputX = inputX / depthScale
    inputY = inputY / depthScale

    local worldViewMatrix : Matrix33 = self.worldViewMatrix
    local inverseMatrix : Matrix33 = self:AcquireWorkingMatrix()
    zo_invertMatrix33(self.worldViewMatrix, inverseMatrix)

    inputX, inputY = zo_matrixTransformPoint(inverseMatrix, inputX, inputY)
    return inputX, inputY
end

function ZO_SceneGraphNode:GetWorldSpaceCoordinates(inputPointX, inputPointY, inputPointZ)
    local worldViewMatrix : Matrix33 = self.worldViewMatrix
    local cameraSpaceX, cameraSpaceY = zo_matrixTransformPoint(self.worldViewMatrix, inputPointX, inputPointY)
    local inverseViewMatrix : Matrix33 = self:AcquireWorkingMatrix()
    zo_invertMatrix33(self.sceneGraph:GetCameraNode():GetWorldViewMatrix(), inverseViewMatrix)
    local worldSpaceX, worldSpaceY = zo_matrixTransformPoint(inverseViewMatrix, cameraSpaceX, cameraSpaceY)
    local worldSpaceZ = self.finalTranslateZ + inputPointZ
    return worldSpaceX, worldSpaceY, worldSpaceZ
end

function ZO_SceneGraphNode:OnChildAdded(child)
    if self.children == nil then
        self.children = {}
    end
    table.insert(self.children, child)
end

function ZO_SceneGraphNode:OnChildRemoved(child)
    if self.children then
        ZO_RemoveFirstElementFromNumericallyIndexedTable(self.children, child)
    end
end

function ZO_SceneGraphNode:AddLine(lineControl, startControl, endControl, z)
    if not self.lines then
        self.lines = {}
    end
    table.insert(self.lines, lineControl)

    -- It's assumed that startControl and endControl are part of the
    -- scenegraph. The line is fixed to draw in between those two control points.
    lineControl.sceneZ = z
    lineControl.sceneThickness = lineControl:GetThickness()
    lineControl:ClearAnchors()
    lineControl:SetAnchor(TOPLEFT, startControl, CENTER)
    lineControl:SetAnchor(BOTTOMRIGHT, endControl, CENTER)
    self:SetDirty(true)
end

function ZO_SceneGraphNode:SetLineThickness(lineControl, thickness)
    lineControl.sceneThickness = thickness
    self:SetDirty(true)
end

function ZO_SceneGraphNode:AddTextureComposite(compositeControl, x, y, z)
    if not self.composites then
        self.composites = {}
    end
    table.insert(self.composites, compositeControl)

    compositeControl.sceneX = x
    compositeControl.sceneY = y
    compositeControl.sceneZ = z
    compositeControl.sceneScale = 1
    compositeControl.sceneHidden = false
    compositeControl.sceneAnchorPoint = CENTER
    self:SetDirty(true)
end

function ZO_SceneGraphNode:RemoveTextureComposite(compositeControl)
    ZO_RemoveFirstElementFromNumericallyIndexedTable(self.composites, compositeControl)
end

function ZO_SceneGraphNode:AddTexture(textureControl, x, y, z)
    if not self.textures then
        self.textures = {}
    end
    table.insert(self.textures, textureControl)

    textureControl.sceneX = x
    textureControl.sceneY = y
    textureControl.sceneZ = z
    textureControl.sceneScale = 1
    textureControl.sceneHidden = false
    textureControl.sceneAnchorPoint = CENTER
    textureControl.sceneUseRotation = true
    self:SetDirty(true)
end

function ZO_SceneGraphNode:RemoveTexture(textureControl)
    ZO_RemoveFirstElementFromNumericallyIndexedTable(self.textures, textureControl)
end

function ZO_SceneGraphNode:ClearControls()
    if self.textures then
        ZO_ClearNumericallyIndexedTable(self.textures)
    end

    if self.composites then
        ZO_ClearNumericallyIndexedTable(self.composites)
    end

    if self.lines then
        ZO_ClearNumericallyIndexedTable(self.lines)
    end
end

function ZO_SceneGraphNode:SetControlPosition(control, x, y, z)
    control.sceneX = x
    control.sceneY = y
    control.sceneZ = z
    self:SetDirty(true)
end

function ZO_SceneGraphNode:SetControlHidden(control, hidden)
    if control.sceneHidden ~= hidden then
        control.sceneHidden = hidden
        self:SetDirty(true)
    end
end

function ZO_SceneGraphNode:SetControlScale(control, scale)
    if control.sceneScale ~= scale then
        control.sceneScale = scale
        self:SetDirty(true)
    end
end

function ZO_SceneGraphNode:SetControlAnchorPoint(control, anchorPoint)
    if control.sceneAnchorPoint ~= anchorPoint then
        control.sceneAnchorPoint = anchorPoint
        self:SetDirty(true)
    end
end

function ZO_SceneGraphNode:SetTextureUseRotation(textureControl, useRotation)
    if textureControl.sceneUseRotation ~= useRotation then
        textureControl.sceneUseRotation = useRotation
        self:SetDirty(true)
    end
end