local super = require "Object"

Axis = super:new()

local defaults = {
    line = true,
    ticks = true,
    grid = false,
}

local nilDefaults = {
    'labelFormatter',
    'labelRotation',
    'title',
}

function Axis: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
    
    local validator = function(formatter)
        return formatter == nil
            or (self:allowsNumberValues() and Object.isa(formatter, NumberFormatter))
            or (self:allowsDateValues() and Object.isa(formatter, DateFormatter))
    end
    self:getPropertyHook('labelFormatter'):setValidator(validator)
    
    self.labelInspectorInfo = {}
    
    self._orientationHook = PropertyHook:new(Graph.horizontalOrientation)
    
    -- TODO: rebuild any open inspectors, but do it less awkwardly.
    local labelFormatterHook = self:getPropertyHook('labelFormatter')
    self._formatterObserver = function(sender)
        if sender == labelFormatterHook and self:allowsNumberValues() then
            sender = self
        end
        self:invalidate(sender)
    end
    labelFormatterHook:removeObserver(self)
    labelFormatterHook:addObserver(self._formatterObserver)
    
    self._labelSize = nil
    
    return self
end

function Axis:getFormatters()
    local formatters = { { nil, 'Auto Format' } }
    if self:allowsNumberValues() then
        appendtables(formatters, {false}, NumberFormatter:getSubclasses())
    end
    if self:allowsDateValues() then
        -- TODO: find a straightforward way to combine formatter classes and instances in the list.
        if self:allowsNumberValues() then
            appendtables(formatters, {false}, DateFormatter:getSubclasses())
        else
            appendtables(formatters, {false}, DateFormatter:getTemplates())
        end
    end
    return formatters
end

function Axis:isQuantitative()
    return false
end

function Axis:getPreferredType()
    return 'nil'
end

function Axis:allowsNumberValues()
    return false
end

function Axis:requiresNumberValues()
    return false
end

function Axis:allowsDateValues()
    return false
end

function Axis:requiresDateValues()
    return false
end

function Axis:createInspector(type, hooks, title, undoTitle)
    local inspector = Inspector:new{
        title = title,
        undoTitle = undoTitle,
        type = type,
    }
    for hookName, propertyName in pairs(hooks) do
        local hook = self:getPropertyHook(propertyName)
        if hook then
            inspector:addHook(hook, hookName)
        end
    end
    return inspector
end

function Axis:getTitleInspectors(parent, axisName)
    local list = List:new()
    list:add(self:createInspector('string', {'title'}, axisName, 'Title'))
    return list
end

function Axis:getValueInspectors(parent, axisName)
    local list = List:new()
    return list
end

function Axis:getLabelInspectors(parent, axisName)
    local list = List:new()
    local inspector, hook
    for i = 1, #self.labelInspectorInfo do
        local info = self.labelInspectorInfo[i]
        list:add(self:createInspector(unpack(info)))
    end
    inspector = Inspector:new{
        title = axisName,
        type = 'List.Group',
        target = function()
            local list = List:new()
            local inspector, hook
            inspector = Inspector:new{
                title = 'Format',
                type = 'Class',
                constraint = function()
                    return self:getFormatters()
                end,
            }
            hook = Hook:new(
                function()
                    local formatter = Axis.getFormatter(self)
                    if Object.isa(formatter, DateFormatter) and not self:allowsNumberValues() then
                        return formatter:getTemplate()
                    elseif formatter then
                        return _G[formatter:class()]
                    end
                end,
                function(value)
                    local formatter
                    if Object.isa(value, Formatter) then
                        formatter = value:new()
                        formatter:mirror(self:getFormatter())
                    elseif type(value) == 'string' and self:allowsDateValues() then
                        formatter = DateFormatter:new(value)
                    end
                    self:setProperty('labelFormatter', formatter)
                end)
            inspector:addHook(hook)
            self:getPropertyHook('labelFormatter'):addObserver(inspector)
            list:add(inspector)
            local formatter = self:getFormatter()
            if formatter and formatter.getInspectors and self:allowsNumberValues() then
                local formatterInspectorList = formatter:getInspectors()
                list:join(formatterInspectorList)
            end
            return list
        end,
    }
    list:add(inspector)
    inspector = Inspector:new{
        type = 'List.Group',
        target = function()
            local list = List:new()
            list:add(self:createInspector('Rotation', {'labelRotation'}, 'Rotation'))
            list:add(self:createInspector('boolean', {'grid'}, 'Draw Grid'))
            return list
        end,
    }
    list:add(inspector)
    return list
end

function Axis:setTitle(title)
    self:setProperty('title', title)
end

function Axis:setOrientation(orientation)
    self._orientationHook:setValue(orientation)
end

function Axis:getOrientation()
    return self._orientationHook:getValue()
end

function Axis:getFormatter()
    return self:getProperty('labelFormatter')
end

local _getLabelParameters = function(orientation, rotation)
    local halign, valign, px, py
    rotation = rotation % 180
    if orientation == Graph.verticalOrientation then
        halign, px = 1, -1/3
        if rotation < 60 then
            valign = 1
        elseif rotation <= 120 then
            valign = 0.5
        else
            valign = 0
        end
        py = valign - 0.5
    else
        valign, py = 1, -1/3
        if rotation <= 30 or rotation >= 150 then
            halign = 0.5
        elseif rotation <= 90 then
            halign = 1
        else
            halign = 0
        end
        px = halign - 0.5
    end
    return halign, valign, px, py
end

function Axis:padding(parent)
    local _, __, labelValues, labelPositions = self:distribute(Rect:zero())
    local textSequence = Sequence:newWithArray(labelValues, #labelPositions)
    
    local formatter = self:getFormatter()
    if formatter then
        textSequence = formatter:evaluate(textSequence)
    end
    
    local labelStyle = { font = self:getLabelFont(parent) }
    local textRectSequence = textSequence:map(function(text)
        text = todisplaystring(text)
        local styledString = StyledString.new(text, labelStyle)
        local textRect = styledString:measure()
        return textRect
    end)
    
    local labelRotation = self:getProperty('labelRotation')
    if labelRotation == nil then
        labelRotation = 0
        if self:getOrientation() == Graph.horizontalOrientation then
            for _, textRect in textRectSequence:iter() do
                if textRect:width() > self:getLabelSize() * 1.5 then
                    labelRotation = 45
                    break
                end
            end
        end
    end
    self._labelRotation = labelRotation
    
    local baseSize = parent:getBaseSize()
    local rect = Rect:new{
        left = - 2 * baseSize,
        bottom = - 2 * baseSize,
        right = 2 * baseSize,
        top = 2 * baseSize,
    }
    local labelDistance = parent:getAxisLabelDistance()
    local labelTransformation = Transformation:identity():rotate(math.rad(labelRotation))
    local halign, valign, px, py = _getLabelParameters(self:getOrientation(), labelRotation)
    for _, textRect in textRectSequence:iter() do
        local padding = textRect:height()
        textRect = labelTransformation:transformRect(textRect)
        textRect = textRect:offset(px * padding - textRect:width() * halign - textRect:minx(), py * padding - textRect:height() * valign - textRect:miny())
        rect = rect:union(textRect)
    end
    return {
        left = -rect.left,
        bottom = -rect.bottom,
        right = rect.right,
        top = rect.top,
    }
end

function Axis:getLength(rect)
    local length
    if self:getOrientation() == Graph.horizontalOrientation then
        length = rect:width()
    else
        length = rect:height()
    end
    return length
end

function Axis:setLabelSize(labelSize)
    self._labelSize = labelSize
end

function Axis:getLabelSize()
    return self._labelSize
end

function Axis:getLabelFont(parent)
    return parent:getAxisValueFont()
end

function Axis:prepareDraw(rect, parent, crossing)
    local tickValues, tickPositions, labelValues, labelPositions, minorTickPositions = self:distribute(rect, crossing)
    local drawState = {
        parent = parent,
        crossing = crossing,
        line = {
            x1 = rect.left,
            y1 = rect.bottom,
            x2 = rect.right,
            y2 = rect.top,
        },
        tickValues = tickValues,
        tickPositions = tickPositions,
        labelValues = labelValues,
        labelPositions = labelPositions,
        minorTickPositions = minorTickPositions,
    }
    return drawState
end

function Axis:drawBackground(canvas, drawState)
    local baseSize = drawState.parent:getBaseSize()
    local paint = drawState.parent:getAxisPaint()
    if self:getProperty('line') then
        canvas:setThickness(1.5 * baseSize)
        canvas:setPaint(paint):stroke(Path.line(drawState.line))
    end
    if self:getProperty('ticks') then
        local stamp
        if self:getOrientation() == Graph.horizontalOrientation then
            stamp = VerticalTickPointStamp
        else
            stamp = HorizontalTickPointStamp
        end
        local tickGetter = function()
            return 2 * baseSize, 1.5 * baseSize, paint
        end
        AxisStamp(canvas, drawState.line, drawState.tickPositions, stamp, tickGetter)
        if drawState.minorTickPositions then
            tickGetter = function()
                return 1.5 * baseSize, 1.5 * baseSize, paint
            end
            AxisStamp(canvas, drawState.line, drawState.minorTickPositions, stamp, tickGetter)
        end
    end
end

function Axis:drawForeground(canvas, drawState)
    local baseSize = drawState.parent:getBaseSize()
    local labelDistance = drawState.parent:getAxisLabelDistance()
    local labelRotation = self:getProperty('labelRotation') or self._labelRotation
    local labelTransformation = Transformation:identity():rotate(math.rad(labelRotation))
    local halign, valign, px, py = _getLabelParameters(self:getOrientation(), labelRotation)
    local labelPaint = drawState.parent:getAxisLabelPaint()
    local backgroundPaint = drawState.parent:getBackgroundPaint()
    local labelFont = self:getLabelFont(drawState.parent)
    local formatter = self:getFormatter()
    local textSequence = Sequence:newWithArray(drawState.labelValues, #drawState.labelPositions)
    if formatter then
        textSequence = formatter:evaluate(textSequence) or textSequence
    end
    if drawState.crossing then
        local textIter = textSequence:iter()
        local textGetter = function()
            local _, text = textIter()
            return text, backgroundPaint, labelFont, halign, valign, labelTransformation, px, py, 2 * baseSize
        end
        canvas:preserve(function(canvas)
            canvas:clipRect(drawState.parent:getContentRect())
            AxisStamp(canvas, drawState.line, drawState.labelPositions, TextStrokePointStamp, textGetter)
        end)
    end
    local textIter = textSequence:iter()
    local textGetter = function()
        local _, text = textIter()
        return text, labelPaint, labelFont, halign, valign, labelTransformation, px, py
    end
    AxisStamp(canvas, drawState.line, drawState.labelPositions, TextPointStamp, textGetter)
end

function Axis:origin()
    return nil
end

function Axis:distribute(rect, crossing)
    local tickValues, tickPositions, labelValues, labelPositions, minorTickPositions
    return tickValues, tickPositions, labelValues, labelPositions, minorTickPositions
end

function Axis:getScaler(rect)
    return function(value) return nil end, function(value, size) return nil end
end

function Axis:scale(rect, value)
    local scaler = self:getScaler(rect)
    return scaler(value)
end

function Axis:scaled(rect, value)
    return 0
end

return Axis
