local super = require "NumericAxis"

LinearAxis = super:new()

local defaults = {
}

local nilDefaults = {
    'major',
}

local tonumber = tonumber
local _min = math.min
local _max = math.max
local countSteps = function(min, max, step)
    return math.ceil(max / step) - math.floor(min / step)
end

local validator = function(value)
    return value == nil or (type(value) == 'number' and (value > -math.huge) and (value < math.huge))
end

function LinearAxis: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:getPropertyHook('min'):setValidator(validator)
    self:getPropertyHook('max'):setValidator(validator)
    self:getPropertyHook('major'):setValidator(validator)
    
    self.valueInspectorInfo = { min = 'min', max = 'max', step = 'major' }
    self:setAutoRange(0, 10)
    self._autoStepOptions = { 1, 2, 5 }
    
    return self
end

function LinearAxis:getPreferredType()
    return 'number'
end

function LinearAxis:allowsNumberValues()
    return true
end

function LinearAxis:requiresNumberValues()
    return true
end

function LinearAxis:getRange(length)
    local min, max = super.getRange(self)
    local fixedMin, fixedMax = self:getExplicitRange()
    local step = self:getProperty('major')
    if not step then
        local stepOptions = self._autoStepOptions
        local minSteps = countSteps(min, max, stepOptions[#stepOptions])
        step = Array.detect(stepOptions, function(step)
            local steps = countSteps(min, max, step)
            local lengthPerStep = length / steps
            return (steps == minSteps or lengthPerStep >= self:getLabelSize() * 2)
        end)
    end
    
    if not fixedMin and min ~= 0 then
        min = (math.ceil(min / step) - 1) * step
    end
    if not fixedMax and max ~= 0 then
        max = (math.floor(max / step) + 1) * step
    end
    return min, max, step
end

function LinearAxis:setRange(min, max)
    local major = self:getProperty('major') or self._autoStepOptions[1]
    local snapMin = major * math.floor(0.5 + min / major)
    local snapMax = major * math.floor(0.5 + max / major)
    local tolerance = (max - min) / 64
    if math.abs(min - snapMin) < tolerance then
        min = snapMin
    end
    if math.abs(max - snapMax) < tolerance then
        max = snapMax
    end
    super.setRange(self, min, max)
end

local function getAutoStepOptions(min, max)
    local range = max - min
    if Meta.isThumbnail then
        return { 2 }
    elseif 2 < range and range <= 10 then
        return { 0.1, 0.2, 1, 2, 5 } -- steps of 0.5 are hard to read
    else
        local step = 10 ^ math.floor(math.log10(range))
        return { 0.1 * step, 0.2 * step, 0.5 * step, 1 * step, 2 * step, 5 * step }
    end
end

function LinearAxis:setAutoRangeForValueRange(min, max)
    local fixedMin, fixedMax = self:getExplicitRange()
    min = fixedMin or min
    max = fixedMax or max
    local similarityThreshold = math.min(math.abs(min), math.abs(max)) / 1e4
    if min < max - similarityThreshold then
        if min > 0 and max > 0 then
            if min / max < 5/6 then
                min = 0
            end
        elseif min < 0 and max < 0 then
            if max / min < 5/6 then
                max = 0
            end
        end
    elseif min <= max and max - min <= similarityThreshold and min ~= 0 then
        min = min - math.abs(min)
        max = max + math.abs(max)
    else
        if fixedMin and fixedMin ~= 0 then
            max = fixedMin + math.abs(fixedMin)
        elseif fixedMax and fixedMax ~= 0 then
            min = fixedMax - math.abs(fixedMax)
        elseif fixedMax == 0 then
            min = -10 + 1e-4
            max = 0
        else
            min = 0
            max = 10 - 1e-4
        end
    end
    min = fixedMin or min
    max = fixedMax or max
    self:setAutoRange(min, max)
    self._autoStepOptions = getAutoStepOptions(min, max)
end

function LinearAxis:getValueConverters()
    local negativeInfinity, positiveInfinity = -math.huge, math.huge
    return function(value)
        value = tonumber(value)
        if value and negativeInfinity < value and value < positiveInfinity then
            return value
        end
    end, function(value) return value end
end

function LinearAxis:origin()
    return 0
end

function LinearAxis:getFormatter()
    return super.getFormatter(self) or AutoNumberFormatter:new()
end

function LinearAxis:distribute(rect, crossing)
    local scaler = self:getScaler(rect)
    local length = self:getLength(rect)
    local min, max, major = self:getRange(length)
    if length == 0 then
        -- min and max cover their widest range, but use the smallest step to catch possible extra decimal digits
        local _
        _, _, major = self:getRange(math.huge)
    end
    while (max - min) / major > 100 do
        major = major * 10
    end
    local minor = 1
    local origin = self:origin()
    local values = {}
    local positions = {}
    local minorPositions = {}
    if major > 0 then
        local iters = 0
        local position = major * math.floor(min / major)
        local fudge = major / 1e4
        max = max + fudge
        while position <= max do
            local start = _max(0, math.ceil((min - position) * minor / major))
            local stop = _min(minor - 1, (max - position) * minor / major)
            for i = start, stop do
                local value = position + i * major / minor
                if value ~= crossing then
                    if i > 0 then
                        minorPositions[#minorPositions + 1] = scaler(value)
                    else
                        values[#values + 1] = value
                        positions[#positions + 1] = scaler(value)
                    end
                end
            end
            iters = iters + 1
            position = major * (math.floor(min / major) + iters)
        end
    end
    return values, positions, values, positions, minorPositions
end

function LinearAxis:getScaler(rect)
    local min, max = self:getRange(self:getLength(rect))
    local rectMin, rectSize
    if self:getOrientation() == Graph.horizontalOrientation then
        rectMin, rectSize = rect:minx(), rect:width()
    else
        rectMin, rectSize = rect:miny(), rect:height()
    end
    return LinearScaler(min, max, rectMin, rectSize)
end

function LinearAxis:scaled(rect, position)
    local min, max = self:getRange(self:getLength(rect))
    local rectMin, rectSize
    if self:getOrientation() == Graph.horizontalOrientation then
        rectMin, rectSize = rect:minx(), rect:width()
    else
        rectMin, rectSize = rect:miny(), rect:height()
    end
    local fraction = ((position - rectMin) / rectSize)
    return min * (1 - fraction) + max * fraction
end

return LinearAxis
