local super = require "Chart"

Table = super:new()

local handles = {
    MultiResizerHandle:new{
        actionName = "Resize Column",
        track = Hook:new(
            function(self)
                local rect = self:contentRect()
                local y = rect.top + self:columnHeaderHeight() / 2
                return rect.left, y, rect.right, y
            end,
            nil),
        trackThickness = Hook:new(
            function(self)
                return self:columnHeaderHeight()
            end,
            nil),
        tokenPositions = Hook:new(
            function(self)
                local rect = self:rect()
                local tokens, positions = {}, {}
                local columnPadding = self:getColumnPadding()
                local freeWidth = rect:width()
                local totalWeight = 0
                for column in self._columns:iter() do
                    tokens[#tokens + 1] = column
                    freeWidth = freeWidth - column:getWidth()
                    totalWeight = totalWeight + column:getWeight()
                end
                freeWidth = freeWidth - columnPadding * (#tokens + 1)
                local cumulativeWidth = columnPadding
                local cumulativeWeight = 0
                for index = 1, #tokens do
                    cumulativeWidth = cumulativeWidth + tokens[index]:getWidth()
                    cumulativeWeight = cumulativeWeight + tokens[index]:getWeight()
                    positions[index] = rect:minx() + cumulativeWidth + freeWidth * (cumulativeWeight / totalWeight)
                    cumulativeWidth = cumulativeWidth + columnPadding
                end
                return tokens, positions
            end,
            function(self, token, position)
                local rect = self:rect()
                local columnPadding = self:getColumnPadding()
                local freeWidth = rect:width() - columnPadding
                local totalWeight = 0
                for column in self._columns:iter() do
                    freeWidth = freeWidth - column:getWidth() - columnPadding
                    totalWeight = totalWeight + column:getWeight()
                end
                local columnLeft = rect:minx() + columnPadding
                local oldWidth = 0
                for column in self._columns:iter() do
                    local columnWidth = column:getWidth() + freeWidth * column:getWeight() / totalWeight
                    if token == column then
                        oldWidth = columnWidth
                        break
                    end
                    columnLeft = columnLeft + columnWidth + columnPadding
                end
                local width = math.max(position - columnLeft, rect:width() / 100)
                freeWidth = freeWidth + token:getWidth() + (width - oldWidth)
                totalWeight = totalWeight - token:getWeight()
                token:setProperty('width', 0)
                token:setProperty('weight', (width * totalWeight) / (freeWidth - width))
                rect.right = rect.right + width - oldWidth
                self:setRect(rect)
            end),
    },
    ScrollerHandle:new{
        actionName = "Scroll Table",
        track = Hook:new(
            function(self)
                local rect = self:contentRect()
                return rect.right, rect.top, rect.right, rect.bottom
            end,
            nil),
        range = Hook:new(
            function(self)
                local dataset = self:getProperty('dataset')
                if dataset then
                    return 1, 1 + dataset:entryCount()
                else
                    return 0, 0
                end
            end,
            nil),
        visibleRange = Hook:new(
            function(self)
                local rect = self:contentRect()
                local scrollValue = self:getProperty('scrollValue')
                local rowHeight = self:getRowHeight()
                local edgePadding = self:getEdgePadding()
                return scrollValue, scrollValue + (rect:height() - edgePadding) / rowHeight
            end,
            function(self, min, max)
                self:setProperty('scrollValue', min)
            end),
    },
}

local defaults = {
    scrollValue = 1,
}

local nilDefaults = {
}

function Table:new()
    self = super.new(self)
    
    for k, v in pairs(defaults) do
        self:addProperty(k, v)
    end
    for _, k in pairs(nilDefaults) do
        self:addProperty(k)
    end
    
    self._columns = List:new()
    self._columns:setUndoable(true)
    self._columns:addObserver(self)
    
    self._columnRects = {}
    
    self._addColumnObserver = function(item)
        item:setDatasetHook(self:getPropertyHook('dataset'))
        item:setColorSchemeHook(self:getPropertyHook('colorScheme'))
        item:setTypographySchemeHook(self:getPropertyHook('typographyScheme'))
        item:setParent(self)
        item:addObserver(self)
    end
    self._columns:addEventObserver('add', self._addColumnObserver)
    
    return self
end

function Table:unarchiveColumns(archived)
    self._columns:setUndoable(false)
    for index = 1, #archived do
        local column = unarchive(archived[index])
        if Object.isa(column, TableColumn) then
            self._columns:add(column)
        end
    end
    self._columns:setUndoable(true)
end

-- NOTE: Version 1.3.2 and earlier saved a 'headerPadding' property.
function Table:unarchiveHeaderPadding(archived)
end

-- NOTE: Version 1.4.2 and earlier saved 'rowHeight', 'rowPadding', and 'columnPadding' properties.
function Table:unarchiveRowHeight(archived)
end
function Table:unarchiveRowPadding(archived)
end
function Table:unarchiveColumnPadding(archived)
end

function Table:unarchiveHeaderFont(archived)
    -- NOTE: Version 1.4.2 and earlier stored table header fonts without a typography scheme.
    self._legacySubtitleFont = unarchive(archived)
end

function Table:unarchiveRowFont(archived)
    -- NOTE: Version 1.4.2 and earlier stored table row fonts without a typography scheme.
    self._legacyLabelFont = unarchive(archived)
end

function Table:unarchived()
    if self._legacySubtitleFont then
        self:setFont(TypographyScheme.subtitleFont, self._legacySubtitleFont)
    end
    if self._legacyLabelFont then
        self:setFont(TypographyScheme.labelFont, self._legacyLabelFont)
    end
    super.unarchived(self)
end

function Table:archive()
    local typeName, properties = super.archive(self)
    local columns = {}
    for column in self._columns:iter() do
        columns[#columns + 1] = column
    end
    properties.columns = columns
    return typeName, properties
end

function Table:getHandles()
    return appendtables({}, handles, super.getHandles(self))
end

function Table:getInspectors()
    local list = super.getInspectors(self)
    local inspector, hook
    inspector = Inspector:new{
        title = 'Column',
        target = self._columns,
        type = 'List.Sort',
        constraint = function()
            return TableColumn:getSubclasses()
        end,
    }
    list:add(inspector)
    return list
end

function Table:getColorInspectors()
    local list = super.getColorInspectors(self)
    list:add(self:createColorInspector(ColorScheme.strokePaint, 'Title Underline'))
    list:add(self:createColorInspector(ColorScheme.backgroundPaint, 'Rows'))
    list:add(self:createColorInspector(ColorScheme.alternateBackgroundPaint, 'Rows (Alternate)'))
    list:add(self:createColorInspector(ColorScheme.labelPaint, 'Text'))
    list:add(self:createColorInspector(ColorScheme.emptyFillPaint, 'Empty'))
    return list
end

function Table:getDataColorInspectors()
    local list = super.getDataColorInspectors(self)
    list:add(self:createAccentColorInspector(1, 'Accent 1'))
    list:add(self:createAccentColorInspector(2, 'Accent 2'))
    return list
end

function Table:getFontInspectors()
    local list = super.getFontInspectors(self)
    list:add(self:createFontInspector(TypographyScheme.subtitleFont, 'Column Titles'))
    list:add(self:createFontInspector(TypographyScheme.labelFont, 'Rows'))
    return list
end

local function drawThumbnail(canvas, rect, fonts, paints)
    if paints.background then
        canvas:setPaint(paints.background)
            :fill(Path.rect(canvas:metrics():rect(), 3))
    end
    canvas:setPaint(paints.fill)
        :fill(Path.rect(rect))
    rect = rect:insetXY(12, 1)
    local PADY = 2
    canvas:setPaint(paints.title)
        :setFont(fonts.title)
        :drawText('Title', rect:midx(), rect:maxy() - fonts.title:ascent(), 0.5)
        :setPaint(paints.columnTitle)
        :setFont(fonts.columnTitle)
        :drawText('Column', rect:minx(), rect:maxy() - (fonts.title:ascent() + fonts.title:descent() + PADY + PADY + fonts.columnTitle:ascent()), 0)
    if paints.emptyFill and paints.accent1 then
        canvas:drawText('Column', rect:minx() + 0.8 * rect:width(), rect:maxy() - (fonts.title:ascent() + fonts.title:descent() + PADY + PADY + fonts.columnTitle:ascent()), 0.5)
    else
        canvas:drawText('Column', rect:maxx(), rect:maxy() - (fonts.title:ascent() + fonts.title:descent() + PADY + PADY + fonts.columnTitle:ascent()), 1)
    end
    rect = rect:inset{
        left = 0,
        bottom = fonts.label:descent() + PADY,
        right = 0,
        top = fonts.title:ascent() + fonts.title:descent() + PADY + PADY + fonts.columnTitle:ascent() + fonts.columnTitle:descent() + PADY + 1,
    }
    local minx, miny, maxx, maxy = rect:minx(), rect:miny(), rect:maxx(), rect:maxy()
    canvas:setPaint(paints.stroke)
        :stroke(Path.line{ x1 = minx, x2 = maxx, y1 = maxy + 0.5, y2 = maxy + 0.5 })
    local ascent, descent = fonts.label:ascent(), fonts.label:descent()
    local y1 = maxy - (PADY + ascent)
    local rowHeight = PADY + ascent + descent + PADY
    local rows = {
        { name = 'Alfa', value = '300' },
        { name = 'Bravo', value = '50' },
        { name = 'Charlie', value = '-200' },
        { name = 'Delta', value = '1,200' },
        { name = 'Echo', value = '-40' },
    }
    for index = 1, #rows do
        local y = y1 - rowHeight * (index - 1)
        if y > miny then
            local row = rows[index]
            canvas:setPaint(paints.label)
                :setFont(fonts.label)
                :drawText(row.name, minx, y, 0)
            if paints.emptyFill and paints.accent1 and paints.accent2 then
                local fraction1 = (1 - (index - 1) / #rows) ^ 1.5
                local fraction2 = ((2.375 + math.abs(index - 2.375)) / #rows) ^ 1.5
                canvas:setPaint(paints.accent1)
                    :fill(Path.rect{ left = minx + (0.8 - 0.2 * fraction1) * rect:width(), bottom = y - (descent - PADY), right = minx + 0.8 * rect:width() - 1, top = y + (ascent - PADY) })
                    :setPaint(paints.accent2)
                    :fill(Path.rect{ left = minx + 0.8 * rect:width() + 1, bottom = y - (descent - PADY), right = minx + (0.8 + 0.2 * fraction2) * rect:width(), top = y + (ascent - PADY) })
            else
                canvas:drawText(row.value, maxx, y, 1)
            end
        end
    end
end

function Table:drawTypographySchemePreview(canvas, rect, typographyScheme)
    local SIZE = 12
    local fonts = {
        title = typographyScheme:getFont(TypographyScheme.titleFont, SIZE),
        columnTitle = typographyScheme:getFont(TypographyScheme.subtitleFont, SIZE),
        label = typographyScheme:getFont(TypographyScheme.labelFont, SIZE),
    }
    local paints = {
        fill = Color.invisible,
        title = Color.gray(0, 1),
        columnTitle = Color.gray(0, 1),
        label = Color.gray(0, 1),
        stroke = Color.gray(0, 0.4),
    }
    drawThumbnail(canvas, rect, fonts, paints)
end

function Table:drawColorSchemePreview(canvas, rect, colorScheme)
    local SIZE = 12
    local typographyScheme = self:getTypographyScheme()
    local fonts = {
        title = typographyScheme:getFont(TypographyScheme.titleFont, SIZE),
        columnTitle = typographyScheme:getFont(TypographyScheme.subtitleFont, SIZE),
        label = typographyScheme:getFont(TypographyScheme.labelFont, SIZE),
    }
    local paints = {
        background = colorScheme:getPaint(ColorScheme.pageBackgroundPaint),
        fill = colorScheme:getPaint(ColorScheme.backgroundPaint),
        title = colorScheme:getPaint(ColorScheme.titlePaint),
        columnTitle = colorScheme:getPaint(ColorScheme.titlePaint),
        label = colorScheme:getPaint(ColorScheme.labelPaint),
        stroke = colorScheme:getPaint(ColorScheme.strokePaint),
        emptyFill = colorScheme:getPaint(ColorScheme.emptyFillPaint),
        accent1 = colorScheme:getDataSeriesPaint(colorScheme:getAccentDataPaintIndex(1), colorScheme:getDataPaintCount(), true),
        accent2 = colorScheme:getDataSeriesPaint(colorScheme:getAccentDataPaintIndex(2), colorScheme:getDataPaintCount(), true),
    }
    drawThumbnail(canvas, rect, fonts, paints)
end

function Table:getColumnList()
    return self._columns
end

function Table:contentRect()
    local rect = super.contentRect(self)
    rect.top = rect.top - self:columnHeaderHeight()
    return rect
end

function Table:columnHeaderHeight()
    local font = self:getFont(TypographyScheme.subtitleFont)
    if font then
        return 1.5 * font:height()
    else
        return 0
    end
end

function Table:getColumnPaint()
    return self:getPaint(ColorScheme.labelPaint)
end

function Table:getEmptyFillPaint()
    return self:getPaint(ColorScheme.emptyFillPaint)
end

function Table:getAccentPaint()
    local colorScheme = self:getColorScheme()
    return colorScheme:getDataSeriesPaint(colorScheme:getAccentDataPaintIndex(1), colorScheme:getDataPaintCount(), true)
end

function Table:getAccentPaint2()
    local colorScheme = self:getColorScheme()
    return colorScheme:getDataSeriesPaint(colorScheme:getAccentDataPaintIndex(2), colorScheme:getDataPaintCount(), true)
end

function Table:getLabelFont()
    return self:getFont(TypographyScheme.labelFont)
end

function Table:getTitleRect()
    return super.getTitleRect(self):insetXY(self:getColumnPadding(), 0)
end

-- NOTE: Don't call this function directly! Give it to pcall.
local function __PROTECTED__drawCell(canvas, rect, column, propertySequence, rowIndex)
    canvas:clipRect(rect)
    column:drawCell(canvas, rect, propertySequence:getValue(rowIndex))
end

function Table:getRowPadding()
    return 2 * self:getBaseSize()
end

function Table:getRowHeight()
    local rowFont = self:getFont(TypographyScheme.labelFont)
    local rowPadding = self:getRowPadding()
    return rowPadding + rowFont:ascent() + rowFont:descent() + rowPadding
end

function Table:getColumnPadding()
    return 12 * self:getBaseSize()
end

function Table:getEdgePadding()
    return 10 * self:getBaseSize()
end

function Table:draw(canvas)
    if canvas:isHitTest() then
        canvas:fill(Path.rect(self:rect()))
        return
    end
    local contentRect = self:contentRect()
    local dataset = self:getProperty('dataset')
    local rows = dataset and dataset:entryCount() or 0
    local columns = self._columns
    if not columns:get(1) then
        return
    end
    -- drawn location
    local left   = self.left
    local right  = self.right
    local bottom = self.bottom
    local top    = self.top
    local scrollValue = self:getProperty('scrollValue')
    -- miscellaneous
    local baseSize = self:getBaseSize()
    local columnHeaderFont = self:getFont(TypographyScheme.subtitleFont)
    local rowFont = self:getFont(TypographyScheme.labelFont)
    local columnHeaderPaint = self:getPaint(ColorScheme.titlePaint)
    local strokePaint = self:getPaint(ColorScheme.strokePaint)
    local stripePaints = { self:getPaint(ColorScheme.alternateBackgroundPaint), Color.invisible }
    local textPaints = { self:getPaint(ColorScheme.labelPaint), self:getPaint(ColorScheme.labelPaint) }
    local styleCount = #stripePaints
    local function rowFormatGetter(rowIndex)
        local formatIndex = ((rowIndex - 1) % styleCount) + 1
        return stripePaints[formatIndex], textPaints[formatIndex], rowFont
    end
    local rowPadding = self:getRowPadding()
    local rowHeight = self:getRowHeight()
    local columnPadding = self:getColumnPadding()
    local edgePadding = self:getEdgePadding()
    -- column data
    local localColumns = {}
    do
        self._columnRects = {}
        local freeWidth = right - left
        local totalWeight = 0
        for column in columns:iter() do
            local width = column:getWidth()
            local weight = column:getWeight()
            local title = column:getTitle()
            local alignment = column:getAlignment()
            localColumns[#localColumns + 1] = {
                column = column,
                width = width,
                weight = weight,
                title = title,
                alignment = alignment,
                propertySequence = ezpcall(function() return column:makePropertySequence() end)
            }
            freeWidth = freeWidth - width
            totalWeight = totalWeight + weight
        end
        freeWidth = freeWidth - columnPadding * (#localColumns + 1)
        local columnLeft = left + columnPadding
        for i = 1, #localColumns do
            local column = localColumns[i]
            if totalWeight > 0 then
                column.width = column.width + freeWidth * (column.weight / totalWeight)
            end
            column.left = columnLeft
            columnLeft = columnLeft + column.width + columnPadding
            self._columnRects[#self._columnRects + 1] = Rect:new{
                left = column.left,
                bottom = contentRect.top,
                right = column.left + column.width,
                top = contentRect.top + self:columnHeaderHeight() * 2/3,
            }:insetXY(0, -3)
        end
    end
    
    -- draw background
    local backgroundRect = Rect:new{
        left = contentRect.left,
        bottom = math.max(bottom, contentRect.top - (rowPadding + rows * rowHeight + edgePadding)),
        right = contentRect.right,
        top = top,
    }
    canvas:setPaint(self:getPaint(ColorScheme.backgroundPaint)):fill(Path.rect(backgroundRect))
    
    -- draw heading
    self:drawTitle(canvas)
    for i = 1, #localColumns do
        local column = localColumns[i]
        local path = Path.line{
            x1 = column.left,
            x2 = column.left + column.width,
            y1 = contentRect.top + 0.5 * baseSize,
            y2 = contentRect.top + 0.5 * baseSize,
        }
        canvas:setPaint(strokePaint)
            :setThickness(baseSize)
            :stroke(path)
        local columnTitle = StyledString.new(column.title, { font = columnHeaderFont })
        local titleRect = Rect:new{
            left = column.left,
            bottom = contentRect.top,
            right = column.left + column.width,
            top = contentRect.top + self:columnHeaderHeight(),
        }
        TruncatedStyledStringStamp(canvas, titleRect, columnTitle, columnHeaderPaint, column.alignment, 0)
    end
    
    -- draw data
    canvas:clipRect(contentRect)
    local cellRect = contentRect:copy()
    for r = 1, rows do
        local rowTop = contentRect.top - rowPadding - (r - scrollValue) * rowHeight
        if r >= math.floor(scrollValue) and rowTop >= bottom then
            local backgroundPaint, newPaint, newFont = rowFormatGetter(r)
            canvas:preserve(function(canvas)
                cellRect.left = left
                cellRect.right = right
                cellRect.top = rowTop
                cellRect.bottom = rowTop - rowHeight
                canvas:setPaint(backgroundPaint):fill(Path.rect(cellRect))
                canvas:setPaint(newPaint):setFont(newFont)
                for c = 1, #localColumns do
                    local column = localColumns[c]
                    cellRect.left = column.left
                    cellRect.right = column.left + column.width
                    Profiler.time(column.column:class() .. ":draw", function()
                        canvas:pcall(function() return __PROTECTED__drawCell(canvas, cellRect, column.column, column.propertySequence, r) end)
                    end)
                end
            end)
        end
    end
end

function Table:getColumnTitleComponent(index)
    local column = self._columns:get(index)
    local inspector, hook
    inspector = self:createInspector('string', {}, 'Title')
    hook = Hook:new(
        function()
            return column:getTitle()
        end,
        function(value)
            column:setTitle(value)
        end)
    inspector:addHook(hook, 'text')
    inspector:addHook(self:getPaintHook(ColorScheme.titlePaint), 'paint')
    inspector:addHook(self:getFontHook(TypographyScheme.subtitleFont), 'font')
    hook = Hook:new(
        function()
            return column:getAlignment()
        end,
        function(value) end)
    column:addObserver(hook)
    inspector:addHook(hook, 'halign')
    inspector:addHook(Hook:new(0.5), 'valign')
    hook = Hook:new(
        function()
            local index = self._columns:index(column)
            return self._columnRects[index]
        end,
        function(value) end)
    self:addDidDrawObserver(hook)
    inspector:addHook(hook, 'rect')
    inspector:addHook(Hook:new(false), 'multiline')
    return inspector
end

function Table:getEditableComponent(x, y)
    local superResult = super.getEditableComponent(self, x, y)
    if superResult then
        return superResult
    end
    if not (x and y) then
        return
    end
    for index = 1, #self._columnRects do
        if self._columnRects[index]:contains(x, y) then
            return self:getColumnTitleComponent(index)
        end
    end
end

return Table
