local class  = require('class')
local Region = require('ui.region')
local Util   = require('util')

local _rep = string.rep
local _sub = string.sub
local _gsub = string.gsub
local colors = _G.colors

local Canvas = class()

Canvas.colorPalette = { }
Canvas.darkPalette = { }
Canvas.grayscalePalette = { }

for n = 1, 16 do
	Canvas.colorPalette[2 ^ (n - 1)]     = _sub("0123456789abcdef", n, n)
	Canvas.grayscalePalette[2 ^ (n - 1)] = _sub("088888878877787f", n, n)
	Canvas.darkPalette[2 ^ (n - 1)]      = _sub("8777777f77fff77f", n, n)
end

function Canvas:init(args)
	self.x = 1
	self.y = 1
	self.layers = { }

	Util.merge(self, args)

	self.ex = self.x + self.width - 1
	self.ey = self.y + self.height - 1

	if not self.palette then
		if self.isColor then
			self.palette = Canvas.colorPalette
		else
			self.palette = Canvas.grayscalePalette
		end
	end

	self.lines = { }
	for i = 1, self.height do
		self.lines[i] = { }
	end
end

function Canvas:move(x, y)
	self.x, self.y = x, y
	self.ex = self.x + self.width - 1
	self.ey = self.y + self.height - 1
end

function Canvas:resize(w, h)
	for i = self.height, h do
		self.lines[i] = { }
	end

	while #self.lines > h do
		table.remove(self.lines, #self.lines)
	end

	if w ~= self.width then
		for i = 1, self.height do
			self.lines[i] = { dirty = true }
		end
	end

	self.ex = self.x + w - 1
	self.ey = self.y + h - 1
	self.width = w
	self.height = h
end

function Canvas:copy()
	local b = Canvas({
		x       = self.x,
		y       = self.y,
		width   = self.width,
		height  = self.height,
		isColor = self.isColor,
	})
	for i = 1, self.height do
		b.lines[i].text = self.lines[i].text
		b.lines[i].fg = self.lines[i].fg
		b.lines[i].bg = self.lines[i].bg
	end
	return b
end

function Canvas:addLayer(layer)
	local canvas = Canvas({
		x       = layer.x,
		y       = layer.y,
		width   = layer.width,
		height  = layer.height,
		isColor = self.isColor,
	})
	canvas.parent = self
	table.insert(self.layers, canvas)
	return canvas
end

function Canvas:removeLayer()
	for k, layer in pairs(self.parent.layers) do
		if layer == self then
			self:setVisible(false)
			table.remove(self.parent.layers, k)
			break
		end
	end
end

function Canvas:setVisible(visible)
	self.visible = visible
	if not visible then
		self.parent:dirty()
		-- set parent's lines to dirty for each line in self
	end
end

function Canvas:write(x, y, text, bg, fg)
	if bg then
		bg = _rep(self.palette[bg], #text)
	end
	if fg then
		fg = _rep(self.palette[fg], #text)
	end
	self:writeBlit(x, y, text, bg, fg)
end

function Canvas:writeBlit(x, y, text, bg, fg)
	if y > 0 and y <= #self.lines and x <= self.width then
		local width = #text

		-- fix ffs
		if x < 1 then
			text = _sub(text, 2 - x)
			if bg then
				bg = _sub(bg, 2 - x)
			end
			if fg then
				fg = _sub(fg, 2 - x)
			end
			width = width + x - 1
			x = 1
		end

		if x + width - 1 > self.width then
			text = _sub(text, 1, self.width - x + 1)
			if bg then
				bg = _sub(bg, 1, self.width - x + 1)
			end
			if fg then
				fg = _sub(fg, 1, self.width - x + 1)
			end
			width = #text
		end

		if width > 0 then

			local function replace(sstr, pos, rstr, width)
				if pos == 1 and width == self.width then
					return rstr
				elseif pos == 1 then
					return rstr .. _sub(sstr, pos+width)
				elseif pos + width > self.width then
					return _sub(sstr, 1, pos-1) .. rstr
				end
				return _sub(sstr, 1, pos-1) .. rstr .. _sub(sstr, pos+width)
			end

			local line = self.lines[y]
			line.dirty = true
			line.text = replace(line.text, x, text, width)
			if fg then
				line.fg = replace(line.fg, x, fg, width)
			end
			if bg then
				line.bg = replace(line.bg, x, bg, width)
			end
		end
	end
end

function Canvas:writeLine(y, text, fg, bg)
	self.lines[y].dirty = true
	self.lines[y].text = text
	self.lines[y].fg = fg
	self.lines[y].bg = bg
end

function Canvas:reset()
	self.regions = nil
end

function Canvas:clear(bg, fg)
	local text = _rep(' ', self.width)
	fg = _rep(self.palette[fg or colors.white], self.width)
	bg = _rep(self.palette[bg or colors.black], self.width)
	for i = 1, self.height do
		self:writeLine(i, text, fg, bg)
	end
end

function Canvas:punch(rect)
	if not self.regions then
		self.regions = Region.new(self.x, self.y, self.ex, self.ey)
	end
	self.regions:subRect(rect.x, rect.y, rect.ex, rect.ey)
end

function Canvas:blitClipped(device)
	for _,region in ipairs(self.regions.region) do
		self:blit(device,
			{ x = region[1] - self.x + 1,
				y = region[2] - self.y + 1,
				ex = region[3]- self.x + 1,
				ey = region[4] - self.y + 1 },
			{ x = region[1], y = region[2] })
	end
end

function Canvas:redraw(device)
	self:reset()
	if #self.layers > 0 then
		for _,layer in pairs(self.layers) do
			self:punch(layer)
		end
		self:blitClipped(device)
	else
		self:blit(device)
	end
	self:clean()
end

function Canvas:isDirty()
	for _, line in pairs(self.lines) do
		if line.dirty then
			return true
		end
	end
end

function Canvas:dirty()
	for _, line in pairs(self.lines) do
		line.dirty = true
	end
end

function Canvas:clean()
	for _, line in pairs(self.lines) do
		line.dirty = false
	end
end

function Canvas:render(device, layers) --- redrawAll ?
	layers = layers or self.layers
	if #layers > 0 then
		self.regions = Region.new(self.x, self.y, self.ex, self.ey)
		local l = Util.shallowCopy(layers)
		for _, canvas in ipairs(layers) do
			table.remove(l, 1)
			if canvas.visible then
				self:punch(canvas)
				canvas:render(device, l)
			end
		end
		self:blitClipped(device)
		self:reset()
	else
		self:blit(device)
	end
	self:clean()
end

function Canvas:blit(device, src, tgt)
	src = src or { x = 1, y = 1, ex = self.ex - self.x + 1, ey = self.ey - self.y + 1 }
	tgt = tgt or self

	for i = 0, src.ey - src.y do
		local line = self.lines[src.y + i]
		if line and line.dirty then
			local t, fg, bg = line.text, line.fg, line.bg
			if src.x > 1 or src.ex < self.ex then
				t  = _sub(t, src.x, src.ex)
				fg = _sub(fg, src.x, src.ex)
				bg = _sub(bg, src.x, src.ex)
			end
			--if tgt.y + i > self.ey then -- wrong place to do clipping ??
			--  break
			--end
			device.setCursorPos(tgt.x, tgt.y + i)
			device.blit(t, fg, bg)
		end
	end
end

function Canvas:applyPalette(palette)

	local lookup = { }
	for n = 1, 16 do
		lookup[self.palette[2 ^ (n - 1)]] = palette[2 ^ (n - 1)]
	end

	for _, l in pairs(self.lines) do
		l.fg = _gsub(l.fg, '%w', lookup)
		l.bg = _gsub(l.bg, '%w', lookup)
		l.dirty = true
	end

	self.palette = palette
end

function Canvas.convertWindow(win, parent, wx, wy)
	local w, h = win.getSize()

	win.canvas = Canvas({
		x       = wx,
		y       = wy,
		width   = w,
		height  = h,
		isColor = win.isColor(),
	})

	function win.clear()
		win.canvas:clear(win.getBackgroundColor(), win.getTextColor())
	end

	function win.clearLine()
		local _, y = win.getCursorPos()
		win.canvas:write(1,
			y,
			_rep(' ', win.canvas.width),
			win.getBackgroundColor(),
			win.getTextColor())
	end

	function win.write(str)
		local x, y = win.getCursorPos()
		win.canvas:write(x,
			y,
			str,
			win.getBackgroundColor(),
			win.getTextColor())
		win.setCursorPos(x + #str, y)
	end

	function win.blit(text, fg, bg)
		local x, y = win.getCursorPos()
		win.canvas:writeBlit(x, y, text, bg, fg)
	end

	function win.redraw()
		win.canvas:redraw(parent)
	end

	function win.scroll(n)
		table.insert(win.canvas.lines, table.remove(win.canvas.lines, 1))
		win.canvas.lines[#win.canvas.lines].text = _rep(' ', win.canvas.width)
		win.canvas:dirty()
	end

	function win.reposition(x, y, width, height)
		win.canvas.x, win.canvas.y = x, y
		win.canvas:resize(width or win.canvas.width, height or win.canvas.height)
	end

	win.clear()
end

function Canvas.scrollingWindow(win, wx, wy)
	local w, h = win.getSize()
	local scrollPos = 0
	local maxScroll = h

	-- canvas lines are are a sliding window within the local lines table
	local lines = { }

	local parent
	local canvas = Canvas({
		x       = wx,
		y       = wy,
		width   = w,
		height  = h,
		isColor = win.isColor(),
	})
	win.canvas = canvas

	local function scrollTo(p, forceRedraw)
		local ms = #lines - canvas.height -- max scroll
		p = math.min(math.max(p, 0), ms)  -- normalize

		if p ~= scrollPos or forceRedraw then
			scrollPos = p
			for i = 1, canvas.height do
				canvas.lines[i] = lines[i + scrollPos]
			end
			canvas:dirty()
		end
	end

	function win.blit(text, fg, bg)
		local x, y = win.getCursorPos()
		win.canvas:writeBlit(x, y, text, bg, fg)
		win.redraw()
	end

	function win.clear()
		lines = { }
		for i = 1, canvas.height do
			lines[i] = canvas.lines[i]
		end
		scrollPos = 0
		canvas:clear(win.getBackgroundColor(), win.getTextColor())
		win.redraw()
	end

	function win.clearLine()
		local _, y = win.getCursorPos()

		scrollTo(#lines - canvas.height)
		win.canvas:write(1,
			y,
			_rep(' ', win.canvas.width),
			win.getBackgroundColor(),
			win.getTextColor())
		win.redraw()
	end

	function win.redraw()
		if parent and canvas.visible then
			local x, y = win.getCursorPos()
			for i = 1, canvas.height do
				local line = canvas.lines[i]
				if line and line.dirty then
					parent.setCursorPos(canvas.x, canvas.y + i - 1)
					parent.blit(line.text, line.fg, line.bg)
					line.dirty = false
				end
			end
			win.setCursorPos(x, y)
		end
	end

	-- doesn't support negative scrolling...
	function win.scroll(n)
		for _ = 1, n do
			lines[#lines + 1] = {
				text = _rep(' ', canvas.width),
				fg = _rep(canvas.palette[win.getTextColor()], canvas.width),
				bg = _rep(canvas.palette[win.getBackgroundColor()], canvas.width),
			}
		end

		while #lines > maxScroll do
			table.remove(lines, 1)
		end

		scrollTo(maxScroll, true)
		win.redraw()
	end

	function win.scrollDown()
		scrollTo(scrollPos + 1)
		win.redraw()
	end

	function win.scrollUp()
		scrollTo(scrollPos - 1)
		win.redraw()
	end

	function win.setMaxScroll(ms)
		maxScroll = ms
	end

	function win.setParent(p)
		parent = p
	end

	function win.write(str)
		str = tostring(str) or ''

		local x, y = win.getCursorPos()
		scrollTo(#lines - canvas.height)
		win.blit(str,
			_rep(canvas.palette[win.getTextColor()], #str),
			_rep(canvas.palette[win.getBackgroundColor()], #str))
		win.setCursorPos(x + #str, y)
	end

	function win.reposition(x, y, width, height)
		win.canvas.x, win.canvas.y = x, y
		win.canvas:resize(width or win.canvas.width, height or win.canvas.height)
	end

	win.clear()
end
return Canvas