SudokuCanvas = {}
SudokuCanvas.__index = SudokuCanvas

function SudokuCanvas:pushHistory()
    if #self.history <= 1 then
        table.insert(self.history, sudoku.cloneBoard(self.board[2]))
        -- showHistory(self)
        -- print("pushHistory: ", #self.history)
    elseif not sudoku.checkBoardsEqual(self.board[2], self.history[#self.history]) then
        table.insert(self.history, sudoku.cloneBoard(self.board[2]))
        -- showHistory(self)
        -- print("pushHistory: ", #self.history)
    end
end

function showHistory(self)
    for i, v in ipairs(self.history) do
        print(string.format("-- [%03d] ----------", i))
        sudoku.showBoard(v)
    end
end

function undo(self)
    if #self.history > 1 then
        showHistory(self)
        table.remove(self.history)
        self.board[2] = sudoku.cloneBoard(self.history[#self.history])
        self.board[3] = sudoku.cloneBoard(self.board[2])

        -- self.board[2] = sudoku.cloneBoard(self.history[#self.history])
        -- self.history[#self.history] = nil

        self:checkBoard(true)
        print"-------------------------------------------"
        showHistory(self)
    end
end

function applyAllSingles(self)
    sudoku.applyFillSinglesRepeatedly(self.board[2])
    self.board[3] = sudoku.cloneBoard(self.board[2])
    self:checkBoard()
end

function showValidOptionsForCurrentCell(self, x, y)
    local s = sudoku.calCellValidOptions(self.board[2], y, x)
    for num = 1, 9 do
        self.smallNumbers[x][y][num].enabled = false
    end
    for _, num in ipairs(s) do
        self.smallNumbers[x][y][num].enabled = true
    end
end

function showValidOptionsForAllCells(self)
    for x = 1, 9 do
        for y = 1, 9 do
            showValidOptionsForCurrentCell(self, x, y)
        end
    end
end

local function selectSmallNumbersFont(cellSize)
    local fntSize = 65
    local margin = 0
    while fntSize >= 5 do
        fntSize = fntSize - 1
        local fnt = love.graphics.newFont(fntSize)
        -- margin = math.floor((cellSize - math.floor(fnt:getWidth("123") / 3) * 5) / 2)
        margin = math.floor((cellSize - (fnt:getHeight() * 3 + 6)) / 2)
        if margin >= 5 then return fnt, margin end
    end
end

local function createSNumTable()
    local smallNumbers = {}
    for x = 1, 9 do
        smallNumbers[x] = {}
        for y = 1, 9 do
            smallNumbers[x][y] = {}
            for num = 1, 9 do
                smallNumbers[x][y][num] = {x=0, y=0, enabled=false}
            end
        end
    end
    return smallNumbers
end

local function calSmallNumbersCoordinates(grid, smallNumbers)
    local smallNumbers = smallNumbers or createSNumTable()
    local cellSize = grid[1][1].x2 - grid[1][1].x1
    local font, my = selectSmallNumbersFont(cellSize)
    local mx = math.floor((cellSize - math.floor(font:getWidth("123") / 3) * 5) / 2)
    -- local my = math.floor(mx / 2)
    local dx = math.floor((cellSize - 2 * mx) / 3) + 2
    local dy = math.floor((cellSize - 2 * my) / 3) + 2
    for x = 1, 9 do
        for y = 1, 9 do
            local x1 = grid.x1 + (x - 1) * cellSize
            local y1 = grid.y1 + (y - 1) * cellSize
            local n = 1
            for j = 1, 3 do
                for i = 1, 3 do
                    local x2 = x1 + mx + (i-1) * dx
                    local y2 = y1 + my + (j-1) * dy
                    smallNumbers[x][y][n].x = x2
                    smallNumbers[x][y][n].y = y2
                    n = n + 1
                end
            end
        end
    end
    smallNumbers.font = font
    return smallNumbers
end

local function buildBigNumbers(grid, fonts)
    local cellSize = grid[1][1].x2 - grid[1][1].x1
    local fntSize = math.floor(cellSize * 0.9)
    local bigNumbers = {}

    if fonts.typedBig and fonts.typedBig ~= "" then
        bigNumbers.typedFont = love.graphics.newFont(fonts.typedBig, fntSize)
    else
        bigNumbers.typedFont = love.graphics.newFont(fntSize)
    end
    if fonts.handWrittenBig and fonts.handWrittenBig ~= "" then
        bigNumbers.handWrittenFont = love.graphics.newFont(fonts.handWrittenBig, fntSize)
    else
        bigNumbers.handWrittenFont = love.graphics.newFont(fntSize)
    end

    local typedHeight = bigNumbers.typedFont:getHeight()
    local handWrittenHeight = bigNumbers.handWrittenFont:getHeight()
    local typedWidth = {}
    local handWrittenWidth = {}
    for i = 1, 9 do
        local c = tostring(i)
        typedWidth[i] = bigNumbers.typedFont:getWidth(c)
        handWrittenWidth[i] = bigNumbers.handWrittenFont:getWidth(c)
    end

    for x = 1, 9 do
        bigNumbers[x] = {}
        for y = 1, 9 do
            bigNumbers[x][y] = {}
            local half = math.floor(cellSize / 2)
            for num = 1, 9 do
                bigNumbers[x][y][num] = {
                    typed = {
                        x = grid[x][y].x1 + half - math.floor(typedWidth[num] / 2),
                        y = grid[x][y].y1 + half - math.floor(typedHeight / 2),
                    },
                    handWritten = {
                        x = grid[x][y].x1 + half - math.floor(handWrittenWidth[num] / 2),
                        y = grid[x][y].y1 + half - math.floor(handWrittenHeight / 2),
                    },
                }
            end
        end
    end

    return bigNumbers
end

local function buildGrid(x, y, cellSize)
    local grid = {}
    grid.x1 = x
    grid.y1 = y
    grid.x2 = x + 9 * cellSize
    grid.y2 = y + 9 * cellSize
    for i = 1, 9 do
        grid[i] = {}
        for j = 1, 9 do
            local x = grid.x1 + (i - 1) * cellSize
            local y = grid.y1 + (j - 1) * cellSize
            grid[i][j] = {x1=x, y1=y, x2=x+cellSize, y2=y+cellSize}
        end
    end
    local n = 0
    local m = 0
    grid.vline = {}
    for x = grid.x1, grid.x2, cellSize do
        n = n + 1
        if n % 3 ~= 1 then
            m = m + 1
            grid.vline[m] = {x1=x, y1=grid.y1, x2=x, y2=grid.y1+9*cellSize}
        end
    end
    n = 0
    m = 0
    grid.hline = {}
    for y = grid.y1, grid.y2, cellSize do
        n = n + 1
        if n % 3 ~= 1 then
            m = m + 1
            grid.hline[m] = {x1=grid.x1, y1=y, x2=grid.x1+9*cellSize, y2=y}
        end
    end
    n = 0
    grid.vbline = {}
    for x = grid.x1, grid.x2, cellSize * 3 do
        n = n + 1
        grid.vbline[n] = {x1=x, y1=grid.y1, x2=x, y2=grid.y1+9*cellSize}
    end
    n = 0
    grid.hbline = {}
    for y = grid.y1, grid.y2, cellSize * 3 do
        n = n + 1
        grid.hbline[n] = {x1=grid.x1, y1=y, x2=grid.x1+9*cellSize, y2=y}
    end
    return grid
end

function SudokuCanvas:resize(size, x, y)
    x = x or self.x1
    y = y or self.y1
    self.cellSize = math.floor(size / 9)
    self.size = self.cellSize * 9
    self.x1 = x
    self.y1 = y
    self.x2 = x + size
    self.y2 = y + size
    self.grid = buildGrid(x, y, self.cellSize)
    self.smallNumbers = calSmallNumbersCoordinates(self.grid, self.smallNumbers)
    self.bigNumbers = buildBigNumbers(self.grid, self.fonts)
end

function SudokuCanvas:setBoard(board, board2)
    local board2 = board2 or board
    self.board = {}
    self.board[1] = sudoku.cloneBoard(board)
    self.board[2] = sudoku.cloneBoard(board2)
    self.board[3] = sudoku.cloneBoard(board2)
    -- self.board[4] = createEmptyBoard()
    self:checkBoard()
end

function SudokuCanvas:new(x, y, size, options)
    local options = options or {}
    local cellSize = math.floor(size / 9)
    local size = cellSize * 9
    local o = {
        x1 = x,
        y1 = y,
        x2 = x + size,
        y2 = y + size,
        size = size,
        cellSize = cellSize,
        cursor = {x=1, y=1, mode="big"},
        fonts = options.fonts,
        colors = options.colors,
        enabled = true,
        focused = true,
        history = {},
    }
    o.grid = buildGrid(x, y, cellSize)
    o.smallNumbers = calSmallNumbersCoordinates(o.grid)
    o.bigNumbers = buildBigNumbers(o.grid, o.fonts)
    if options.enabled == false then o.enabled = false end
    if options.focused == false then o.focused = false end
    if not o.enabled then o.cursorOn = false; o.focused = false end
    setmetatable(o, SudokuCanvas)
    return o
end

function SudokuCanvas:enable()
end

function SudokuCanvas:focus()
end

function SudokuCanvas:checkBoard(hist)
    if not hist then self:pushHistory() end
    local ok, isComplete, conflictList = sudoku.checkBoard(self.board[2])
    self.board[4] = sudoku.createEmptyBoard()
    for _, c in ipairs(conflictList) do
        self.board[4][c.x1][c.y1] = 1
        self.board[4][c.x2][c.y2] = 1
    end
end

function SudokuCanvas:draw()
    local grid = self.grid

    -- grid
    love.graphics.setLineWidth(1)
    uiUtils.setColor(self.colors.thinLines)
    for i = 1, 6 do
        love.graphics.line(grid.vline[i].x1, grid.vline[i].y1, grid.vline[i].x2, grid.vline[i].y2)
        love.graphics.line(grid.hline[i].x1, grid.hline[i].y1, grid.hline[i].x2, grid.hline[i].y2)
    end
    love.graphics.setLineWidth(2)
    uiUtils.setColor(self.colors.thickLines)
    for i = 1, 4 do
        love.graphics.line(grid.vbline[i].x1, grid.vbline[i].y1, grid.vbline[i].x2, grid.vbline[i].y2)
        love.graphics.line(grid.hbline[i].x1, grid.hbline[i].y1, grid.hbline[i].x2, grid.hbline[i].y2)
    end

    -- smallNumbers
    love.graphics.setFont(self.smallNumbers.font)
    for x = 1, 9 do
        for y = 1, 9 do
            for i = 1, 9 do
                if self.board[2][y][x] == 0 then
                    local x1 = self.smallNumbers[x][y][i].x
                    local y1 = self.smallNumbers[x][y][i].y
                    if self.smallNumbers[x][y][i].enabled then
                        uiUtils.setColor(self.colors.smallNumbersEnabled)
                    else
                        uiUtils.setColor(self.colors.smallNumbersDisabled)
                    end
                    love.graphics.print(tostring(i), x1, y1)
                end
            end
        end
    end

    -- bigNumbers
    for x = 1, 9 do
        for y = 1, 9 do
            if self.board[1][y][x] ~= 0 then
                -- print(y, x, self.board[1][y][x])
                local num = self.board[1][y][x]
                local px = self.bigNumbers[x][y][num].typed.x
                local py = self.bigNumbers[x][y][num].typed.y
                love.graphics.setFont(self.bigNumbers.typedFont)
                if self.board[4][y][x] ~= 0 then
                    uiUtils.setColor(self.colors.bigNumbersError)
                else
                    uiUtils.setColor(self.colors.bigNumbersTyped)
                end
                love.graphics.print(tostring(num), px, py)
            elseif self.board[2][y][x] ~= 0 then
                local num = self.board[2][y][x]
                local px = self.bigNumbers[x][y][num].handWritten.x
                local py = self.bigNumbers[x][y][num].handWritten.y
                love.graphics.setFont(self.bigNumbers.handWrittenFont)
                if self.board[4][y][x] ~= 0 then
                    uiUtils.setColor(self.colors.bigNumbersError)
                else
                    uiUtils.setColor(self.colors.bigNumbersHandWritten)
                end
                love.graphics.print(tostring(num), px, py)
            end
        end
    end

    -- cursor
    local x = grid[self.cursor.x][self.cursor.y].x1
    local y = grid[self.cursor.x][self.cursor.y].y1
    if self.board[1][self.cursor.y][self.cursor.x] == 0 then
        if self.cursor.mode == "big" then
            uiUtils.setColor(self.colors.cursorEditBig)
        else
            uiUtils.setColor(self.colors.cursorEditSmall)
        end
    else
        uiUtils.setColor(self.colors.cursorNoEdit)
    end
    love.graphics.setLineWidth(3)
    love.graphics.rectangle("line", x+3, y+3, self.cellSize-6, self.cellSize-6, 10, 10)
end

function SudokuCanvas:keypressed(key)
    local x = self.cursor.x
    local y = self.cursor.y
    local mode = self.cursor.mode
    if key == "-" then
        local size = self.size - math.floor(self.size * .05)
        if size >= 300 then
            self:resize(size)
        end
    elseif key == "=" then
        size = self.size + math.floor(self.size * .05)
        self:resize(size)
    elseif key:match("[1-9]") then
        local num = key:byte() - 48
        if self.board[1][y][x] == 0 then
            if mode == "big" then
                if self.board[2][y][x] == num then
                    self.board[2][y][x] = 0
                    self.board[3][y][x] = 0
                else
                    self.board[2][y][x] = num
                    self.board[3][y][x] = num
                end
                -- self:checkBoard()
            else
                self.smallNumbers[x][y][num].enabled = not self.smallNumbers[x][y][num].enabled
            end
            self:checkBoard()
        end
    elseif key == "delete" or key == "0" then
        if self.board[1][y][x] == 0 then
            if mode == "big" then
                self.board[2][y][x] = 0
                self.board[3][y][x] = 0
                self:checkBoard()
            else
                for num = 1, 9 do
                    self.smallNumbers[x][y][num].enabled = false
                end
            end
        end
    elseif key == "right" or key == "l" then
        x = x + 1
        if x > 9 then x = 1 end
    elseif key == "left" or key == "h" then
        x = x - 1
        if x < 1 then x = 9 end
    elseif key == "down" or key == "j" then
        y = y + 1
        if y > 9 then y = 1 end
    elseif key == "up" or key == "k" then
        y = y - 1
        if y < 1 then y = 9 end
    elseif key == "tab" then
        if mode == "big" then
            self.board[3][y][x] = self.board[2][y][x]
            self.board[2][y][x] = 0
            mode = "small"
        else
            self.board[2][y][x] = self.board[3][y][x]
            mode = "big"
        end
    elseif key == "s" then
        -- showValidOptionsForCurrentCell(self, x, y)
        showValidOptionsForAllCells(self)
    elseif key == "a" then
        applyAllSingles(self)
    elseif key == "u" then
        undo(self)
    elseif key == "f" then
        sudoku.saveBoard("lastBoard.txt", self.board[1], self.board[2])
    elseif key == "c" then
        self.board[2] = sudoku.solve(self.board[1])
        self:checkBoard()
    end
    self.cursor.x = x
    self.cursor.y = y
    self.cursor.mode = mode
end

function SudokuCanvas:mousepressed(x, y, button, istouch)
    local grid = self.grid
    local inBoard = false
    local canChange = false
    local isSameCell = false
    local dx = math.floor(self.cellSize / 3)
    local cx = 0
    local cy = 0
    local mode = self.cursor.mode
    local mx = 0
    local my = 0

    local n = 0
    if x > grid.x1 and x < grid.x2 and y > grid.y1 and y < grid.y2 then
        inBoard = true
        cx = math.floor((x - grid.x1) / self.cellSize) + 1
        cy = math.floor((y - grid.y1) / self.cellSize) + 1
        mx = math.floor((x - grid[cx][cy].x1) / dx) + 1
        my = math.floor((y - grid[cx][cy].y1) / dx) + 1
        num = mx + 3 * (my - 1)
        if self.board[1][cy][cx] == 0 then canChange = true end
        if self.cursor.x == cx and self.cursor.y == cy then isSameCell = true end
    end
    if canChange then
        if button == 1 then
            if isSameCell and self.cursor.mode == "small" then
                self.smallNumbers[cx][cy][num].enabled = not self.smallNumbers[cx][cy][num].enabled
            end
        elseif button == 2 then
            if isSameCell then
                if self.cursor.mode == "big" then
                    self.board[2][cy][cx] = 0
                    self.cursor.mode = "small"
                else
                    self.cursor.mode = "big"
                end
            end
        end

        self.cursor.x = cx
        self.cursor.y = cy
    end
end

setmetatable(SudokuCanvas, {__call=SudokuCanvas.new})
return SudokuCanvas
