local textgpu = {}

local mapping = {
  ["0"] = 0x000000,
  ["1"] = 0xFF0000,
  ["2"] = 0x00FF00,
  ["3"] = 0xFFFF00,
  ["4"] = 0x0000FF,
  ["5"] = 0xFF00FF,
  ["6"] = 0x00FFFF,
  ["7"] = 0xFFFFFF,
}

--[[local nw = io.write
io.write = function(...)
  nw(...)
  io.flush()
  native.sleep(20000)
end]]--

local write = io.write
local flush = io.flush

local background = "0"
local foreground = "0"

local tbuffer = {}
local bbuffer = {}
local fbuffer = {}

local function prepareBuffers(w, h)
  local tbline = (" "):rep(w)
  local bbline = ("0"):rep(w)
  local fbline = ("7"):rep(w)
  for i=1, h do
    tbuffer[i] = tbline
    bbuffer[i] = bbline
    fbuffer[i] = fbline
  end
end

local usub
local function insertString(main, sub, at)
  checkArg(1, main, "string")
  checkArg(2, sub, "string")
  checkArg(3, at, "number")

  return usub(main, 1, at - 1)
    .. sub .. usub(main, at + (utf8.len(sub) or 0))
end

function textgpu.start()
  usub = modules.sandbox.unicode.sub
  local _height = 0
  local gpu = {}
  function gpu.bind() return false, "This is static bound gpu" end
  function gpu.setBackground(color, isPaletteIndex)
    checkArg(1, color, "number")
    checkArg(2, isPaletteIndex, "boolean", "nil")
    if isPaletteIndex then
      return --TODO: Maybe?
    end
    local old = background
    background = tostring(math.floor(modules.color.nearest(color, mapping)))
    write("\x1b[4" .. background .. "m")
    flush()
    return mapping[old]
  end
  function gpu.setForeground(color, isPaletteIndex)
    checkArg(1, color, "number")
    checkArg(2, isPaletteIndex, "boolean", "nil")
    if isPaletteIndex then
      return --TODO: Maybe?
    end
    local old = foreground
    foreground = tostring(math.floor(modules.color.nearest(color, mapping)))
    write("\x1b[3" .. foreground .. "m")
    flush()
    return mapping[old]
  end
  function gpu.getBackground()
    return mapping[background], false
  end
  function gpu.getForeground()
    return mapping[foreground], false
  end
  function gpu.getPaletteColor()
    return nil
  end
  function gpu.setPaletteColor()
    return nil
  end
  function gpu.maxDepth()
    return 3
  end
  function gpu.setDepth()
    return false
  end
  function gpu.getDepth()
    return 3
  end
  function gpu.maxResolution()
    return termutils.getSize()
  end
  function gpu.getResolution()
    return termutils.getSize()
  end
  function gpu.getViewport()
    return termutils.getSize()
  end
  function gpu.setViewport(w, h)
    checkArg(1, w, "number")
    checkArg(2, h, "number")
    return false, "Viewport not supported for this gpu"
  end
  function gpu.setResolution(w, h)
    checkArg(1, w, "number")
    checkArg(2, h, "number")
    return false, "Non resizeable gpu"
  end
  function gpu.get(x, y)
    checkArg(1, x, "number")
    checkArg(2, y, "number")
    
    return tbuffer[y]:sub(x,x), mapping[fbuffer[y]:sub(x,x)], mapping[bbuffer[y]:sub(x,x)]
  end
  function gpu.set(x, y, value, vertical)
    checkArg(1, x, "number")
    checkArg(2, y, "number")
    checkArg(3, value, "string")
    checkArg(4, vertical, "boolean", "nil")
    x = math.floor(x)
    y = math.floor(y)
    if not vertical then
      if not tbuffer[y] then
        native.log("GPU Set failed: under buffer")
        return false
      end
      tbuffer[y] = insertString(tbuffer[y], value, x)
      bbuffer[y] = insertString(bbuffer[y], background:rep(utf8.len(value)), x)
      fbuffer[y] = insertString(fbuffer[y], foreground:rep(utf8.len(value)), x)
      write("\x1b[" .. y .. ";" .. x .. "H" .. value)
    else
      --TODO: Buffers!
      write("\x1b[" .. y .. ";" .. x .. "H")
      value:gsub("([%z\1-\127\194-\244][\128-\191]*)", function(c)
        write(c .. "\x1b[D\x1b[B")
      end)
    end
    flush()
    return true
  end
  function gpu.copy(x, y, w, h, tx, ty) --TODO: Check(check X multiple times)
    checkArg(1, x, "number")
    checkArg(2, y, "number")
    checkArg(3, w, "number")
    checkArg(4, h, "number")
    checkArg(5, tx, "number")
    checkArg(6, ty, "number")
    local ttbuf = {}
    local btbuf = {}
    local ftbuf = {}
    for i=1, h do
      if i + y - 2 <= _height and i + y > 1 then
        ttbuf[i] = tbuffer[y + i - 1] and usub(tbuffer[y + i - 1], x, x + w - 1) or (" "):rep(w)
        btbuf[i] = bbuffer[y + i - 1] and bbuffer[y + i - 1]:sub(x, x + w - 1) or background:rep(w)
        ftbuf[i] = fbuffer[y + i - 1] and fbuffer[y + i - 1]:sub(x, x + w - 1) or foreground:rep(w)
      else
        ttbuf[i] = (" "):rep(w)
        btbuf[i] = background:rep(w)
        ftbuf[i] = foreground:rep(w)
      end
    end
    local bg = background
    local fg = foreground

    for i=1, h do
      local line, linex
      local lwrite = false
      for j=1, w do
        if btbuf[i]:sub(j,j) ~= bg then
          lwrite = true
        end
        if ftbuf[i]:sub(j,j) ~= fg then
          lwrite = true
        end
        if not line then linex = j end
        line = (line or "")
        if lwrite then
          local wx = (tx + x + linex - 1)|0
          local wy = (ty + y + i - 1)|0
          if tbuffer[wy] then
            write("\x1b[4" .. bg .. "m\x1b[3" .. fg .. "m\x1b[" .. wy .. ";" .. wx .. "H" .. line)
            tbuffer[wy] = insertString(tbuffer[wy], line, wx)
            bbuffer[wy] = insertString(bbuffer[wy], bg:rep(utf8.len(line)), wx)
            fbuffer[wy] = insertString(fbuffer[wy], fg:rep(utf8.len(line)), wx)
          end
        
          bg = btbuf[i]:sub(j,j)
          fg = ftbuf[i]:sub(j,j)
          line = nil
          linex = nil
          lwrite = false
        end
        if not line then linex = j end
        line = (line or "") .. usub(ttbuf[i], j,j)
      end
      if line then
        local wx = (tx + x + linex - 1)|0
        local wy = (ty + y + i - 1)|0
        if tbuffer[wy] then
          write("\x1b[4" .. bg .. "m\x1b[3" .. fg .. "m\x1b[" .. wy .. ";" .. wx .. "H" .. line)
          tbuffer[wy] = insertString(tbuffer[wy], line, wx)
          bbuffer[wy] = insertString(bbuffer[wy], bg:rep(utf8.len(line)), wx)
          fbuffer[wy] = insertString(fbuffer[wy], fg:rep(utf8.len(line)), wx)
        end
        line = nil
        linex = nil
        lwrite = false
      end
    end
    write("\x1b[4" .. background .. "m")
    write("\x1b[3" .. foreground .. "m")
    flush()
    return true
  end
  function gpu.fill(x, y, w, h, ch)
    checkArg(1, x, "number")
    checkArg(2, y, "number")
    checkArg(3, w, "number")
    checkArg(4, h, "number")
    checkArg(5, ch, "string")
    ch = usub(ch, 1, 1):rep(math.floor(w))
    for i=1, h do
      if i + y - 1 <= _height and i + y > 1 then
        gpu.set(x, y + i - 1, ch)
      end
    end
    return true
  end

  local screenAddr

  function gpu.getScreen()
    return screenAddr
  end

  if not termutils.init() then
    return nil, "Cannot initialize terminal based gpu"
  end
  write("\x1b[?25l") --Disable cursor
  local w, h = gpu.getResolution()
  _height = h
  prepareBuffers(w, h)
  gpu.setForeground(0xFFFFFF)
  gpu.setBackground(0x000000)

  local gpuaddr = modules.component.api.register(nil, "gpu", gpu)
  screenAddr = modules.component.api.register(nil, "screen", {getKeyboards = function() return {"TODO:SetThisUuid"} end}) --verry dummy screen, TODO: make it better, kbd uuid also in epoll.c
  modules.component.api.register("TODO:SetThisUuid", "keyboard", {})

  deadhooks[#deadhooks + 1] = function()
    write("\x1b[?25h\x1b[" .. ((h-1)|0) .. ";1H") --Enable cursor on quit
    io.flush()
    termutils.restore()
  end

  return gpuaddr
end

return textgpu