local super = require "Object"

require "Handle"

View = super:new()

local _min = math.min
local _max = math.max

local handles = {
    RectHandle:new{
        actionName = "Resize",
        location = Hook:new(
            function(self)
                return self.left, self.bottom, self.right, self.top
            end,
            function(self, left, bottom, right, top)
                self:setRect(Rect:new{left = left, bottom = bottom, right = right, top = top})
            end),
    },
}

local draggingHandle = DraggingHandle:new{
    actionName = "Move",
    location = Hook:new(
        function(self)
            local rect = self:rect()
            return rect.left, rect.bottom
        end,
        function(self, x, y)
            local rect = self:rect()
            local dx = x - rect.left
            local dy = y - rect.bottom
            rect.left = rect.left + dx
            rect.bottom = rect.bottom + dy
            rect.right = rect.right + dx
            rect.top = rect.top + dy
            self:setRect(rect)
        end),
}

function View:new()
    self = super.new(self)
    
    self.left = 0
    self.right = 0
    self.bottom = 0
    self.top = 0
    
    if self:hasStyles() then
        self:addProperty('colorScheme', ColorScheme:new())
        self:addProperty('typographyScheme', TypographyScheme:new())
    end
    
    self.parent = nil
    self.draggingHandle = draggingHandle
    self:setUndoable(true)
    
    return self
end

function View:initialize(properties)
    local datasetUnarchiver = _Dataset
    _Dataset = _DatasetRef
    super.initialize(self, properties)
    _Dataset = datasetUnarchiver
end

function View:unarchiveRect(archived)
    local rect = unarchive(archived)
    if Object.isa(rect, Rect) then
        self.left = rect.left
        self.right = rect.right
        self.bottom = rect.bottom
        self.top = rect.top
    end
end

function View:archive()
    local typeName, properties = super.archive(self)
    properties.rect = self:rect()
    return typeName, properties
end

function View:invalidate(sender)
    if not (sender and sender.isa and sender:isa(View)) then
        sender = self
    end
    super.invalidate(self, sender)
end

function View:hasStyles()
    return true
end

function View:getInspectors()
    return List:new()
end

function View:getColorScheme()
    return self:getProperty('colorScheme')
end

function View:getTypographyScheme()
    return self:getProperty('typographyScheme')
end

function View:getBaseSize()
    return self:getTypographyScheme():getSize() / 16
end

function View:createColorInspector(paintName, title, undoTitle)
    local colorScheme = self:getColorScheme()
    local inspector, hook
    inspector = Inspector:new{
        type = 'Color',
        title = title,
        undoTitle = undoTitle,
    }
    hook = Hook:new(function()
            return colorScheme:getPaint(paintName)
        end,
        function(value)
            colorScheme:setPaint(paintName, value)
        end)
    inspector:addHook(hook)
    hook = Hook:new(function()
            return (colorScheme:getExplicitPaint(paintName) ~= nil)
        end)
    inspector:addHook(hook, 'custom')
    colorScheme:addObserver(inspector)
    return inspector
end

function View:createDataColorInspector(index, title, undoTitle)
    local colorScheme = self:getColorScheme()
    local inspector, hook
    inspector = Inspector:new{
        type = 'Color',
        title = title,
        undoTitle = undoTitle,
    }
    hook = Hook:new(function()
            return colorScheme:getDataPaint(index)
        end,
        function(value)
            colorScheme:setDataPaint(index, value)
        end)
    inspector:addHook(hook)
    hook = Hook:new(function()
            return (colorScheme:getExplicitDataPaintParams(index, colorScheme:isReversed()) ~= nil)
        end)
    inspector:addHook(hook, 'custom')
    colorScheme:addObserver(inspector)
    return inspector
end

function View:createAccentColorInspector(index, title, undoTitle)
    local colorScheme = self:getColorScheme()
    local inspector, hook
    inspector = Inspector:new{
        type = 'Color',
        title = title,
        undoTitle = undoTitle,
    }
    hook = Hook:new(function()
            local adjustedIndex = colorScheme:getAccentDataPaintIndex(index)
            return colorScheme:getDataPaint(adjustedIndex)
        end,
        function(value)
            local adjustedIndex = colorScheme:getAccentDataPaintIndex(index)
            colorScheme:setDataPaint(adjustedIndex, value)
        end)
    inspector:addHook(hook)
    hook = Hook:new(function()
            local adjustedIndex = colorScheme:getAccentDataPaintIndex(index)
            return (colorScheme:getExplicitDataPaintParams(adjustedIndex, colorScheme:isReversed()) ~= nil)
        end)
    inspector:addHook(hook, 'custom')
    colorScheme:addObserver(inspector)
    return inspector
end

function View:createFontInspector(fontName, title, undoTitle)
    local typographyScheme = self:getTypographyScheme()
    local inspector, hook
    inspector = Inspector:new{
        type = 'Font',
        title = title,
        undoTitle = undoTitle,
    }
    hook = Hook:new(function()
            return self:getFont(fontName)
        end,
        function(value)
            self:setFont(fontName, value)
        end)
    inspector:addHook(hook)
    hook = Hook:new(function()
            return (typographyScheme:getExplicitFont(fontName) ~= nil)
        end)
    inspector:addHook(hook, 'custom')
    typographyScheme:addObserver(inspector)
    return inspector
end

function View:getColorSchemeInspectors()
    local colorScheme = self:getColorScheme()
    local getPresets = function()
        return colorScheme:getPresets(self)
    end
    local list = List:new()
    local inspector, hook

    inspector = Inspector:new{
        title = 'Color Scheme',
        type = 'Thumbnail',
        constraint = getPresets,
    }
    hook = Hook:new(function()
            local name = colorScheme:getName()
            if name == ColorScheme.INHERIT then
                local parent = self:getParent()
                name = parent and parent:getColorScheme():getName()
            end
            local presets = getPresets()
            for index = 1, #presets do
                if presets[index] and presets[index]:getName() == name then
                    return index
                end
            end
        end)
    inspector:addHook(hook, 'selectedIndex')
    hook = Hook:new(function()
            local parent = self:getParent()
            local name
            if parent then
                name = parent:getColorScheme():getName()
            end
            local presets = getPresets()
            for index = 1, #presets do
                if presets[index] and presets[index]:getName() == name then
                    return index
                end
            end
        end)
    inspector:addHook(hook, 'hintedIndex')
    hook = Hook:new(
        function()
            return colorScheme
        end,
        function(value)
            if value then
                colorScheme:load(value:getName())
            else
                colorScheme:load(ColorScheme.INHERIT)
            end
        end)
    inspector:addHook(hook)
    hook = Hook:new(function()
            return function(colorScheme)
                return function(canvas, padding)
                    local rect = Rect:new(canvas:metrics():rect()):insetXY(padding, padding)
                    return self:drawColorSchemePreview(canvas, rect, colorScheme)
                end
            end
        end)
    inspector:addHook(hook, 'drawFunction')
    self:getPropertyHook('colorScheme'):addObserver(inspector)
    list:add(inspector)

    inspector = Inspector:new{
        title = 'Customize…',
        type = 'Details',
        target = function()
            local list = self:getColorInspectors()
            list:add(colorScheme:getDataTypeInspector())
            list:add(self:getDataColorInspector())
            return list
        end,
    }
    local dataTypeHook = self:getColorScheme():getDataTypeHook()
    -- TODO: keep this function from being collected too soon, but do it less awkwardly.
    inspector.__dataTypeObserver = function(sender)
        inspector:invalidate(inspector)
    end
    dataTypeHook:addObserver(inspector.__dataTypeObserver)
    list:add(inspector)

    return list
end

function View:getColorInspectors()
    return List:new()
end

function View:getDataColorInspectors()
    return List:new()
end

function View:getDataColorInspector()
    local inspector = Inspector:new{
        title = 'Data',
        type = 'List.Group',
        target = function()
            local list = self:getDataColorInspectors()
            list:join(self:getColorScheme():getDataColorExtraInspectors())
            return list
        end,
    }
    inspector:addHook(Hook:new(true), 'compact')
    return inspector
end

function View:getTypographySchemeInspectors()
    local typographyScheme = self:getTypographyScheme()
    local getPresets = function()
        return typographyScheme:getPresets(self)
    end
    local list = List:new()
    local inspector, hook

    inspector = Inspector:new{
        title = 'Font Scheme',
        type = 'Thumbnail',
        constraint = getPresets,
    }
    hook = Hook:new(function()
            local name = typographyScheme:getName()
            if name == TypographyScheme.INHERIT then
                local parent = self:getParent()
                name = parent and parent:getTypographyScheme():getName()
            end
            local presets = getPresets()
            for index = 1, #presets do
                if presets[index] and presets[index]:getName() == name then
                    return index
                end
            end
        end)
    inspector:addHook(hook, 'selectedIndex')
    hook = Hook:new(function()
            local parent = self:getParent()
            local name
            if parent then
                name = parent:getTypographyScheme():getName()
            end
            local presets = getPresets()
            for index = 1, #presets do
                if presets[index] and presets[index]:getName() == name then
                    return index
                end
            end
        end)
    inspector:addHook(hook, 'hintedIndex')
    hook = Hook:new(
        function()
            return typographyScheme
        end,
        function(value)
            if value then
                typographyScheme:load(value:getName())
            else
                typographyScheme:load(TypographyScheme.INHERIT)
            end
        end)
    inspector:addHook(hook)
    hook = Hook:new(function()
            return function(typographyScheme)
                return function(canvas, padding)
                    local rect = Rect:new(canvas:metrics():rect()):insetXY(padding, padding)
                    return self:drawTypographySchemePreview(canvas, rect, typographyScheme)
                end
            end
        end)
    inspector:addHook(hook, 'drawFunction')
    inspector:addHook(Hook:new(true), 'drawClipped')
    self:getPropertyHook('typographyScheme'):addObserver(inspector)
    list:add(inspector)

    inspector = Inspector:new{
        title = 'Size',
        type = 'Size',
    }
    hook = Hook:new(function()
            return typographyScheme:getSize()
        end,
        function(size)
            local parent = self:getParent()
            if parent and size == parent:getTypographyScheme():getSize() then
                size = nil
            end
            typographyScheme:setSize(size)
        end)
    inspector:addHook(hook)
    hook = Hook:new({ 9, 10, 11, 12, 14, 16, 18, 20, 24, 28, 36 })
    inspector:addHook(hook, 'sizes')
    hook = Hook:new(function()
        return function(canvas, size)
            local rect = Rect:new(canvas:metrics():rect())
            size = math.max(9, size)
            local y = 7 - size / 4.5
            canvas:setPaint(Color.black)
                :setFont(Font.boldSystem(size))
                :drawText('A', rect:midx(), y, 0.5)
        end
    end)
    inspector:addHook(hook, 'iconFunction')
    self:getPropertyHook('typographyScheme'):addObserver(inspector)
    list:add(inspector)

    inspector = Inspector:new{
        title = 'Customize…',
        type = 'Details',
        target = function()
            return self:getFontInspectors()
        end,
    }
    list:add(inspector)

    return list
end

function View:getFontInspectors()
    return List:new()
end

function View:drawTypographySchemePreview(canvas, rect, typographyScheme)
    local y1 = rect:miny() + 0.55 * rect:height()
    local y2 = rect:miny() + 0.35 * rect:height()
    canvas:setPaint(Color.gray(0.3))
        :setFont(typographyScheme:getFont(TypographyScheme.titleFont))
        :drawText(typographyScheme:getName(), rect:midx(), y1, 0.5)
        :setFont(typographyScheme:getFont(TypographyScheme.blockFont))
        :drawText(typographyScheme:getName(), rect:midx(), y2, 0.5)
end

function View:drawColorSchemePreview(canvas, rect, colorScheme)
    canvas:setPaint(colorScheme:getPaint(ColorScheme.backgroundPaint))
        :fill(Path.rect(rect, 5))
    local minx, width = rect:minx(), rect:width()
    local miny, height = rect:miny(), rect:height()
    local SIZE = 12
    local typographyScheme = self:getTypographyScheme()
    canvas:setPaint(colorScheme:getPaint(ColorScheme.titlePaint))
        :setFont(typographyScheme:getFont(TypographyScheme.titleFont, SIZE))
        :drawText('Titles', minx + width * 1 / 8, miny + height * 6 / 8)
        :setPaint(colorScheme:getPaint(ColorScheme.labelPaint))
        :setFont(typographyScheme:getFont(TypographyScheme.labelFont, SIZE))
        :drawText('Data & Labels', minx + width * 1 / 8, miny + height * 5 / 8)
    local r = 8
    local index = 1
    for yi = 1, 2 do
        local y = miny + height * (3 - yi) / 4.5
        for xi = 1, 4 do
            local x = minx + width * xi / 5
            canvas:setPaint(colorScheme:getDataSeriesPaint(index, 8))
                :fill(Path.rect({ left = x - r, bottom = y - r, right = x + r, top = y + r }, 3))
            index = index + 1
        end
    end
end

function Object:createInspector(inspectorType, hooks, title, undoTitle)
    local inspector = Inspector:new{
        title = title,
        type = inspectorType,
        undoTitle = undoTitle,
    }
    for hookName, propertyName in pairs(hooks) do
        local hook
        local colon = propertyName:find(':')
        if colon then
            local getterName = propertyName:sub(1, colon - 1)
            local setterName = propertyName:sub(colon + 1)
            hook = Hook:new(
                function()
                    if type(self[getterName]) == 'function' then
                        return self[getterName](self)
                    end
                end,
                function(value)
                    if type(self[setterName]) == 'function' then
                        self[setterName](self, value)
                    end
                end)
            self:addObserver(hook)
        else
            hook = self:getPropertyHook(propertyName)
        end
        if hook then
            inspector:addHook(hook, hookName)
        end
    end
    return inspector
end

function View:getHandles()
    return appendtables({}, handles)
end

function View:draw(canvas)
    local rect = self:rect():inset({left = 1, bottom = 1, top = 1, right = 1})
    local path = Path.rect(rect)
    local white = Color.gray(1, 0.5)
    local black = Color.gray(0, 0.5)
    local font = Font.get{ name = "Helvetica", size = 14 }
    canvas:setPaint(white):setThickness(2):fill(path)
    canvas:setPaint(black):stroke(path)
    TextStamp(canvas, rect, self:class(), black, font, 0.5, 0.5)
end

function View:drawHandles(canvas)
    local handles = self:getHandles()
    local clippingRect = self:clippingRect(canvas:metrics())
    canvas:preserve(function(canvas)
        canvas:clipRect(clippingRect)
        for i = 1, #handles do
            local handle = handles[i]
            handle:draw(canvas, self)
        end
    end)
end

function View:drawHandlesAndTest(canvas)
    local result = nil
    local handles = self:getHandles()
    local clippingRect = self:clippingRect(canvas:metrics())
    canvas:preserve(function(canvas)
        canvas:clipRect(clippingRect)
        for i = #handles, 1, -1 do
            local handle = handles[i]
            handle:draw(canvas, self)
            if canvas:test() then
                result = handle
                return
            end
        end
    end)
    return result
end

function View:addDidDrawObserver(observer)
    if not self._drawCountHook then
        self._drawCountHook = PropertyHook:new(0)
            :setUndoable(false)
    end
    self._drawCountHook:addObserver(observer)
end

function View:didDraw()
    if self._drawCountHook then
        self._drawCountHook:setValue(self._drawCountHook:getValue() + 1)
    end
end

function View:cursorRects(metrics)
    local handles = self:getHandles()
    local cursorRects = {}
    for i = 1, #handles do
        local handleRects = handles[i]:cursorRects(metrics, self)
        for j = 1, #handleRects do
            cursorRects[#cursorRects + 1] = handleRects[j]
        end
    end
    return cursorRects
end

function View:setRect(rect)
    local oldRect = self:rect()
    self:addUndo(function() self:setRect(oldRect) end)
    self:invalidate(self)
    self.left = _max(rect.left, 0)
    self.right = self.left + (rect.right - rect.left)
    self.top = _min(rect.top, 0)
    self.bottom = self.top - (rect.top - rect.bottom)
    self:invalidate(self)
end

function View:rect()
    return Rect:new(self)
end

function View:padding()
    return {
        left = 0,
        bottom = 0,
        right = 0,
        top = 0,
    }
end

function View:clippingRect(metrics)
    local rect = self:rect()
    local paddingRect = rect:expand(self:padding())
    local adjust = metrics:growthAdjustment()
    local handleRect = rect:expand({
        left = 7 * adjust,
        bottom = 7 * adjust,
        right = 7 * adjust,
        top = 7 * adjust,
    })
    return paddingRect:union(handleRect)
end

function View:getEditableComponent(x, y)
end

function View:setEditing(editing)
    self.editing = editing
    self:invalidate(self)
end

function View:isEditing()
    return self.editing
end

function View:setMagnet(magnet)
    self.magnet = magnet
end

function View:alignedPoint(x, y)
    local magnet = self.magnet
    if magnet then
        return magnet(x, y)
    end
end

function View:setParent(parent)
    if self:hasStyles() then
        self:getColorScheme():setParent(parent:getColorScheme())
        self:getTypographyScheme():setParent(parent:getTypographyScheme())
    end
    self.parent = parent
end

function View:getParent()
    return self.parent
end

function View:getPaint(name)
    return self:getColorScheme():getPaint(name)
end

function View:setPaint(name, paint)
    local colorScheme = self:getColorScheme()
    if colorScheme then
        colorScheme:setPaint(name, paint)
    end
end

function View:getPaintHook(name)
    local colorScheme = self:getColorScheme()
    local hook = Hook:new(
        function()
            return colorScheme:getPaint(name)
        end,
        function(value) end)
    colorScheme:addObserver(hook)
    return hook
end

function View:getFont(name)
    local typographyScheme = self:getProperty('typographyScheme')
    return typographyScheme:getFont(name)
end

function View:setFont(name, font)
    local typographyScheme = self:getProperty('typographyScheme')
    typographyScheme:setFont(name, font)
end

function View:getFontHook(name)
    local typographyScheme = self:getTypographyScheme()
    local hook = Hook:new(
        function()
            return typographyScheme:getFont(name)
        end,
        function(value) end)
    typographyScheme:addObserver(hook)
    return hook
end

return View
