local super = require "Object"

SVGCanvas = super:new()

local function newState(oldState)
    return {
        paint = oldState and oldState.paint or Color.black,
        font = oldState and oldState.font or Font.get({ name = 'Helvetica', size = 14 }),
        thickness = oldState and oldState.thickness or 1,
        opacity = oldState and oldState.opacity or 0,
    }
end

function SVGCanvas:new(metrics)
    self = super.new(self)
    
    self._metrics = metrics
    
    local rect = Rect:new(metrics:rect())
    self._tag = SVGTag:new('svg', {
        version = '1.1',
        xmlns = 'http://www.w3.org/2000/svg',
        ['xmlns:xlink'] = 'http://www.w3.org/1999/xlink',
        width = rect:width(),
        height = rect:height(),
        viewBox = table.concat({ rect:minx(), -rect:maxy(), rect:width(), rect:height() }, ', '),
    }, {})
    self._defs = SVGTag:new('defs', {}, {})
    self._tag:addChild(self._defs)
    self._context = SVGTag:new('g', { transform = 'scale(1, -1)' }, {})
    self._topContext = self._context
    self._tag:addChild(self._context)
    
    self._defIndex = 0
    self._state = newState()
    
    return self
end

function SVGCanvas:toSVG()
    buffer = Buffer:new()
    
    Profiler.time('generate SVG', function()
        buffer:add('<?xml version="1.0" encoding="UTF-8"?>\n')
        self._tag:serialize(buffer, 0)
    end)

    return buffer
end

function SVGCanvas:stroke(path)
    if path then
        local state = self._state
        self._context:addChild(SVGTag:new('path', {
            stroke = state.paint:toSVG(),
            ['stroke-width'] = state.thickness,
            fill = 'none',
            d = path:toSVG(),
        }))
    end
    return self
end

function SVGCanvas:fill(path)
    if path then
        local state = self._state
        self._context:addChild(SVGTag:new('path', {
            stroke = 'none',
            fill = state.paint:toSVG(),
            d = path:toSVG(),
        }))
    end
    return self
end

function SVGCanvas:drawText(text, x, y)
    local state = self._state
    if type(text) == 'string' then
        self._context:addChild(SVGTag:new('text', {
            x = x,
            y = -y,
            fill = state.paint:toSVG(),
            transform = 'scale(1, -1)',
            ['font-family'] = state.font:getName(),
            ['font-size'] = state.font:getSize(),
            ['xml:space'] = 'preserve',
        }, text))
    else
        local components = text:getComponents()
        for index = 1, #components do
            local component = components[index]
            local text = component.string
            local font = component.font
            local offset = component.position
            offset.y = offset.y + (component.baseline or 0)
            self._context:addChild(SVGTag:new('text', {
                x = x + offset.x,
                y = -(y + offset.y),
                fill = state.paint:toSVG(),
                transform = 'scale(1, -1)',
                ['font-family'] = font:getName(),
                ['font-size'] = font:getSize(),
                ['xml:space'] = 'preserve',
            }, text))
        end
    end
    return self
end

function SVGCanvas:strokeText() return self end
function SVGCanvas:drawIcon() return self end
function SVGCanvas:clipIcon() return self end

function SVGCanvas:setOpacity(opacity)
    self._state.opacity = opacity or self._state.opacity
    return self
end

function SVGCanvas:setPaint(paint)
    self._state.paint = paint or self._state.paint
    return self
end

function SVGCanvas:setPaintMode() return self end

function SVGCanvas:setThickness(thickness)
    self._state.thickness = thickness or self._state.thickness
    return self
end

function SVGCanvas:setFont(font)
    self._state.font = font or self._state.font
    return self
end

function SVGCanvas:getFont()
    return self._state.font
end

function SVGCanvas:concatTransformation(transformation)
    if transformation and not transformation:isIdentity() then
        local g = SVGTag:new('g', { transform = transformation:toSVG() }, {})
        self._context:addChild(g)
        self._context = g
    end
    return self
end

function SVGCanvas:clipRect(rect)
    return self:clip(Path.rect(rect))
end

function SVGCanvas:clip(path)
    self._defIndex = self._defIndex + 1
    local def = SVGTag:new('clipPath', { id = tostring(self._defIndex) }, {})
        :addChild(SVGTag:new('path', { d = path:toSVG() }))
    self._defs:addChild(def)
    local g = SVGTag:new('g', { ['clip-path'] = 'url(#' .. self._defIndex .. ')' }, {})
    self._context:addChild(g)
    self._context = g
    return self
end

function SVGCanvas:preserve(func, newContext)
    if newContext and self._context == self._topContext and self._context:childCount() > 0 then
        self._context = SVGTag:new('g', { transform = 'scale(1, -1)' }, {})
        self._topContext = self._context
        self._tag:addChild(self._context)
    end
    local state = self._state
    local g = self._context
    self._state = newState(state)
    func(self)
    self._state = state
    self._context = g
    return self
end

function SVGCanvas:isHitTest() end
function SVGCanvas:test() end

function SVGCanvas:metrics()
    return self._metrics
end

function SVGCanvas:dirtyRect()
    return Rect:new(self._metrics:rect())
end

function SVGCanvas:pcall(func)
    local result
    self:preserve(function(_)
        -- NOTE: This ignores the passed-in canvas since the canvas is closed in func.
        result = ezpcall(func)
    end)
    return result
end

return SVGCanvas
