local super = require "NumericAxis"

DateAxis = super:new()

local defaults = {
}

local nilDefaults = {
    'major',
}

local _max = math.max

local function _isdate(date)
    return (date and type(date) == 'userdata' and date.year) and true
end

local function _isduration(duration)
    return (duration and type(duration) == 'userdata' and duration.years) and true
end

local dateValidator = function(value)
    return value == nil or _isdate(value)
end

local durationValidator = function(value)
    return value == nil or _isduration(value)
end

local function stepDate(date, duration, up)
    local durationFields = { 'years', 'months', 'days', 'hours', 'minutes', 'seconds' }
    local dateFields = { 'year', 'month', 'day', 'hour', 'minute', 'second' }
    local dateMaxes = { 0, 12, false, 24, 60, 60 }
    local dateOffsets = { 0, 1, 1, 0, 0, 0 }
    local dateValues = {}
    local hasHadValue
    for index = #durationFields, 1, -1 do
        local durationValue = duration[durationFields[index]](duration)
        local hasValue = (durationValue > 0)
        hasHadValue = hasHadValue or hasValue
        if hasHadValue then
            local stepSize = 1
            local offset = 0
            if dateMaxes[index] and dateMaxes[index] % durationValue == 0 then
                stepSize = _max(durationValue, 1)
                offset = dateOffsets[index]
            end
            local dateValue = date[dateFields[index]](date)
            dateValue = math.floor((dateValue - offset) / stepSize + (hasValue and up and 1 or 0)) * stepSize + offset
            dateValues[index] = dateValue
        end
    end
    return Date:get(unpack(dateValues))
end
local function stepDateUp(date, duration) return stepDate(date, duration, true) end
local function stepDateDown(date, duration) return stepDate(date, duration, false) end

local stepLimit = 50
local function countSteps(min, max, step)
    local steps = 1
    local value = stepDateDown(min, step)
    while value < max and steps <= stepLimit do
        steps = steps + 1
        value = value + step
    end
    return steps
end

function DateAxis: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(dateValidator)
    self:getPropertyHook('max'):setValidator(dateValidator)
    self:getPropertyHook('major'):setValidator(durationValidator)
    
    self.valueInspectorInfo = { min = 'min', max = 'max', step = 'major' }
    self:setAutoRange(Date:get(2000), Date:get(2020))
    self._autoStepOptions = {
        Duration:get(1),
        Duration:get(2),
        Duration:get(5),
    }
    
    return self
end

function DateAxis:getPreferredType()
    return 'Date'
end

function DateAxis:allowsDateValues()
    return true
end

function DateAxis:requiresDateValues()
    return true
end

function DateAxis:setRange(min, max)
    local major = self:getProperty('major') or self._autoStepOptions[1]
    local numericMin, numericMax = min:numeric(), max:numeric()
    local snapMin, snapMax = stepDateDown(min, major), stepDateDown(max, major)
    local numericSnapMin, numericSnapMax = snapMin:numeric(), snapMax:numeric()
    local highMin, highMax = stepDateUp(min, major), stepDateUp(max, major)
    local numericHighMin, numericHighMax = highMin:numeric(), highMax:numeric()
    if numericHighMin - numericMin < numericMin - numericSnapMin then
        snapMin = highMin
        numericSnapMin = numericHighMin
    end
    if numericHighMax - numericMax < numericMax - numericSnapMax then
        snapMax = highMax
        numericSnapMax = numericHighMax
    end
    local tolerance = (numericMax - numericMin) / 64
    if math.abs(numericMin - numericSnapMin) < tolerance then
        min = snapMin
    end
    if math.abs(numericMax - numericSnapMax) < tolerance then
        max = snapMax
    end
    super.setRange(self, min, max)
end

function DateAxis: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() * 1.5)
        end)
    end
    
    if not fixedMin then
        min = stepDateDown(min, step)
    end
    if not fixedMax then
        max = stepDateUp(max, step)
    end
    return min, max, step
end

local function getAutoStepOptions(min, max)
    local filter = function(options)
        return Array.filter(options, function(step)
            return countSteps(min, max, step) <= stepLimit
        end)
    end
    local durationFields = {
        { 0, 0, 0, 0, 0, 1 },
        { 0, 0, 0, 0, 0, 5 },
        { 0, 0, 0, 0, 0, 15 },
        { 0, 0, 0, 0, 1 },
        { 0, 0, 0, 0, 5 },
        { 0, 0, 0, 0, 15 },
        { 0, 0, 0, 1 },
        { 0, 0, 0, 4 },
        { 0, 0, 1 },
        { 0, 0, 3 },
        { 0, 0, 7 },
        { 0, 1 },
        { 0, 3 },
        { 1 },
        { 2 },
        { 5 },
        { 10 },
        { 100 },
        { 1000 },
    }
    if Meta.isThumbnail then
        durationFields = { { 0, 0, 1 } }
    end
    local durations = Array.map(durationFields, function(fields) return Duration:get(unpack(fields)) end)
    return filter(durations)
end

function DateAxis:setAutoRangeForValueRange(min, max)
    local fixedMin, fixedMax = self:getExplicitRange()
    min = (fixedMin and fixedMin:numeric()) or min
    max = (fixedMax and fixedMax:numeric()) or max
    if min < max then
        min, max = Date:numeric(min), Date:numeric(max)
    elseif min == max then
        min, max = Date:numeric(min), Date:numeric(max)
        min = Date:get(min:year(), min:month(), min:day())
        max = min + Duration:get(0, 0, 1)
    else
        min, max = Date:numeric(min), Date:numeric(max)
        if fixedMin then
            max = min + Duration:get(0, 0, 1)
        elseif fixedMax then
            min = max - Duration:get(0, 0, 1)
        else
            min = Date:get(2000)
            max = Date:get(2020) - Duration:get(0, 0, 0, 0, 0, 1)
        end
    end
    self:setAutoRange(min, max)
    self._autoStepOptions = getAutoStepOptions(min, max)
end

function DateAxis:getValueConverters()
    return function(value)
        if _isdate(value) then
            return value:numeric()
        end
    end, function(value) return Date:numeric(value) end
end

function DateAxis:origin()
    return Date:get(0)
end

function DateAxis:distribute(rect, crossing)
    local scaler = self:getScaler(rect)
    local length = self:getLength(rect)
    if length == 0 then
        length = math.huge
    end
    local min, max, major = self:getRange(length)
    local formatter = self:getFormatter()
    local formatterInterval = formatter and formatter:getInterval()
    local tickValues = {}
    local tickPositions = {}
    local labelValues = {}
    local labelPositions = {}
    if _isdate(min) and _isdate(max) and _isduration(major) then
        value = stepDateDown(min, major)
        if value < min then
            value = stepDateUp(min, major)
        end
        while value <= max do
            if value >= min then
                tickValues[#tickValues + 1] = value
                tickPositions[#tickPositions + 1] = scaler(value)
                if formatterInterval == major then
                    if #tickValues > 1 then
                        labelValues[#labelValues + 1] = tickValues[#tickValues - 1]
                        labelPositions[#labelPositions + 1] = (tickPositions[#tickPositions - 1] + tickPositions[#tickPositions]) / 2
                    end
                else
                    labelValues[#labelValues + 1] = tickValues[#tickValues]
                    labelPositions[#labelPositions + 1] = tickPositions[#tickPositions]
                end
            end
            value = value + major
        end
    end
    return tickValues, tickPositions, labelValues, labelPositions, nil
end

function DateAxis:getFormatter()
    return super.getFormatter(self) or AutoDateFormatter:new()
end

function DateAxis:getScaler(rect)
    local min, max = self:getRange(self:getLength(rect))
    min, max = min:numeric(), max:numeric()
    local rectMin, rectSize
    if self:getOrientation() == Graph.horizontalOrientation then
        rectMin, rectSize = rect:minx(), rect:width()
    else
        rectMin, rectSize = rect:miny(), rect:height()
    end
    -- rectMin + rectSize * (value - min) / (max - min)
    local A = rectMin - rectSize * min / (max - min)
    local M = rectSize / (max - min)
    return function(value, size)
        if _isdate(value) then
            local numberSize = tonumber(size)
            if numberSize then
                size = Duration:get(0, 0, size)
            end
            if _isduration(size) then
                local value2 = (value + size):numeric()
                value = value:numeric()
                return A + M * (3 * value - value2) / 2, A + M * (value + value2) / 2
            else
                value = value:numeric()
                return A + M * value
            end
        end
    end
end

function DateAxis:scaled(rect, position)
    local min, max = self:getRange(self:getLength(rect))
    min, max = min:numeric(), max:numeric()
    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 Date:numeric(min * (1 - fraction) + max * fraction)
end

return DateAxis
