package gui

import (
	"image"
	"math"

	"github.com/go-gl/gl/all-core/gl"
	"github.com/liamg/aminal/buffer"
	"github.com/liamg/aminal/config"
	"github.com/liamg/aminal/glfont"
)

type OpenGLRenderer struct {
	font          *glfont.Font
	boldFont      *glfont.Font
	areaWidth     int
	areaHeight    int
	areaX         int
	areaY         int
	cellWidth     float32
	cellHeight    float32
	termCols      uint
	termRows      uint
	cellPositions map[[2]uint][2]float32
	rectangles    map[[2]uint]*rectangle
	config        *config.Config
	colourAttr    uint32
	program       uint32
	textureMap    map[*image.RGBA]uint32
}

type rectangle struct {
	vao        uint32
	vbo        uint32
	cv         uint32
	colourAttr uint32
	colour     [3]float32
	points     []float32
	prog       uint32
}

func (r *OpenGLRenderer) CellWidth() float32 {
	return r.cellWidth
}

func (r *OpenGLRenderer) CellHeight() float32 {
	return r.cellHeight
}

func (r *OpenGLRenderer) newRectangle(x float32, y float32, colourAttr uint32) *rectangle {

	halfAreaWidth := float32(r.areaWidth / 2)
	halfAreaHeight := float32(r.areaHeight / 2)

	x = (x - halfAreaWidth) / halfAreaWidth
	y = -(y - halfAreaHeight) / halfAreaHeight
	w := r.cellWidth / halfAreaWidth
	h := r.cellHeight / halfAreaHeight

	rect := &rectangle{
		points: []float32{
			x, y, 0,
			x, y + h, 0,
			x + w, y + h, 0,

			x + w, y, 0,
			x, y, 0,
			x + w, y + h, 0,
		},
		colourAttr: colourAttr,
		prog:       r.program,
	}

	gl.UseProgram(rect.prog)

	// SHAPE
	gl.GenBuffers(1, &rect.vbo)
	gl.BindBuffer(gl.ARRAY_BUFFER, rect.vbo)
	gl.BufferData(gl.ARRAY_BUFFER, 4*len(rect.points), gl.Ptr(rect.points), gl.STATIC_DRAW)

	gl.GenVertexArrays(1, &rect.vao)
	gl.BindVertexArray(rect.vao)
	gl.EnableVertexAttribArray(0)

	gl.BindBuffer(gl.ARRAY_BUFFER, rect.vbo)
	gl.VertexAttribPointer(0, 3, gl.FLOAT, false, 0, nil)

	// colour
	gl.GenBuffers(1, &rect.cv)

	rect.setColour([3]float32{0, 1, 0})

	return rect
}

func (rect *rectangle) Draw() {
	gl.UseProgram(rect.prog)
	gl.BindVertexArray(rect.vao)
	gl.DrawArrays(gl.TRIANGLES, 0, 6)
}

func (rect *rectangle) setColour(colour [3]float32) {
	if rect.colour == colour {
		return
	}

	c := []float32{
		colour[0], colour[1], colour[2],
		colour[0], colour[1], colour[2],
		colour[0], colour[1], colour[2],
		colour[0], colour[1], colour[2],
		colour[0], colour[1], colour[2],
		colour[0], colour[1], colour[2],
	}

	gl.UseProgram(rect.prog)
	gl.BindBuffer(gl.ARRAY_BUFFER, rect.cv)
	gl.BufferData(gl.ARRAY_BUFFER, len(c)*4, gl.Ptr(c), gl.STATIC_DRAW)
	gl.EnableVertexAttribArray(rect.colourAttr)
	gl.VertexAttribPointer(rect.colourAttr, 3, gl.FLOAT, false, 0, gl.PtrOffset(0))

	rect.colour = colour
}

func (rect *rectangle) Free() {
	gl.UseProgram(rect.prog)
	gl.DeleteVertexArrays(1, &rect.vao)
	gl.DeleteBuffers(1, &rect.vbo)
	gl.DeleteBuffers(1, &rect.cv)
}

func NewOpenGLRenderer(config *config.Config, font *glfont.Font, boldFont *glfont.Font, areaX int, areaY int, areaWidth int, areaHeight int, colourAttr uint32, program uint32) *OpenGLRenderer {
	r := &OpenGLRenderer{
		areaWidth:     areaWidth,
		areaHeight:    areaHeight,
		areaX:         areaX,
		areaY:         areaY,
		cellPositions: map[[2]uint][2]float32{},
		rectangles:    map[[2]uint]*rectangle{},
		config:        config,
		colourAttr:    colourAttr,
		program:       program,
		textureMap:    map[*image.RGBA]uint32{},
	}
	r.SetFont(font, boldFont)
	return r
}

func (r *OpenGLRenderer) GetTermSize() (uint, uint) {
	return r.termCols, r.termRows
}

func (r *OpenGLRenderer) SetArea(areaX int, areaY int, areaWidth int, areaHeight int) {
	r.areaWidth = areaWidth
	r.areaHeight = areaHeight
	r.areaX = areaX
	r.areaY = areaY
	r.SetFont(r.font, r.boldFont)
}

func (r *OpenGLRenderer) SetFont(font *glfont.Font, bold *glfont.Font) { // @todo check for monospace and return error if not?
	r.font = font
	r.boldFont = bold
	r.cellWidth, _ = font.Size("X")
	r.cellHeight = font.LineHeight() // vertical padding
	r.termCols = uint(math.Floor(float64(float32(r.areaWidth) / r.cellWidth)))
	r.termRows = uint(math.Floor(float64(float32(r.areaHeight) / r.cellHeight)))
	r.rectangles = map[[2]uint]*rectangle{}
}

func (r *OpenGLRenderer) getRectangle(col uint, row uint) *rectangle {
	if rect, ok := r.rectangles[[2]uint{col, row}]; ok {
		return rect
	}
	return r.generateRectangle(col, row)
}

func (r *OpenGLRenderer) generateRectangle(col uint, line uint) *rectangle {

	rect, ok := r.rectangles[[2]uint{col, line}]
	if ok {
		rect.Free()
	}

	// rounding to whole pixels makes everything nice
	x := float32(float32(col) * r.cellWidth)
	y := float32((float32(line) * r.cellHeight)) + r.cellHeight
	r.rectangles[[2]uint{col, line}] = r.newRectangle(x, y, r.colourAttr)
	return r.rectangles[[2]uint{col, line}]
}

func (r *OpenGLRenderer) DrawCursor(col uint, row uint, colour config.Colour) {
	rect := r.getRectangle(col, row)
	rect.setColour(colour)
	rect.Draw()
}

func (r *OpenGLRenderer) DrawCellBg(cell buffer.Cell, col uint, row uint, cursor bool, colour *config.Colour) {

	var bg [3]float32

	if colour != nil {
		bg = *colour
	} else {

		if cursor {
			bg = r.config.ColourScheme.Cursor
		} else if cell.Attr().Reverse {
			bg = cell.Fg()
		} else {
			bg = cell.Bg()
		}
	}

	if bg != r.config.ColourScheme.Background {
		rect := r.getRectangle(col, row)
		rect.setColour(bg)
		rect.Draw()
	}

}

func (r *OpenGLRenderer) DrawCellText(cell buffer.Cell, col uint, row uint, colour *config.Colour) {

	var fg [3]float32

	if colour != nil {
		fg = *colour
	} else {
		if cell.Attr().Reverse {
			fg = cell.Bg()
		} else {
			fg = cell.Fg()
		}
	}

	var alpha float32 = 1
	if cell.Attr().Dim {
		alpha = 0.5
	}
	r.font.SetColor(fg[0], fg[1], fg[2], alpha)

	x := float32(r.areaX) + float32(col)*r.cellWidth
	y := float32(r.areaY) + (float32(row+1) * r.cellHeight) - (r.font.LinePadding())

	if cell.Attr().Bold { // bold means draw text again one pixel to right, so it's fatter
		if r.boldFont != nil {
			y := float32(r.areaY) + (float32(row+1) * r.cellHeight) - (r.boldFont.LinePadding())
			r.boldFont.SetColor(fg[0], fg[1], fg[2], alpha)
			r.boldFont.Print(x, y, string(cell.Rune()))
			return
		}
		r.font.Print(x+1, y, string(cell.Rune()))
	}

	r.font.Print(x, y, string(cell.Rune()))

}

func (r *OpenGLRenderer) DrawCellImage(cell buffer.Cell, col uint, row uint) {

	img := cell.Image()

	if img == nil {
		return
	}

	ix := float32(col) * r.cellWidth
	iy := float32(r.areaHeight) - (float32(row+1) * r.cellHeight)
	iy -= float32(cell.Image().Bounds().Size().Y)
	gl.UseProgram(r.program)

	var tex uint32

	tex, ok := r.textureMap[img]
	if !ok {
		gl.Enable(gl.TEXTURE_2D)
		gl.GenTextures(1, &tex)
		gl.BindTexture(gl.TEXTURE_2D, tex)
		gl.TexParameterf(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST)
		gl.TexParameterf(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST)
		gl.TexParameterf(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
		gl.TexParameterf(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)

		gl.TexImage2D(
			gl.TEXTURE_2D,
			0,
			gl.RGBA,
			int32(img.Bounds().Size().X),
			int32(img.Bounds().Size().Y),
			0,
			gl.RGBA,
			gl.UNSIGNED_BYTE,
			gl.Ptr(img.Pix),
		)
		gl.BindTexture(gl.TEXTURE_2D, 0)
		gl.Disable(gl.TEXTURE_2D)

		gl.Disable(gl.BLEND)

		r.textureMap[img] = tex
	}

	var w float32 = float32(img.Bounds().Size().X)
	var h float32 = float32(img.Bounds().Size().Y)

	var readFboId uint32
	gl.GenFramebuffers(1, &readFboId)
	gl.BindFramebuffer(gl.READ_FRAMEBUFFER, readFboId)

	gl.FramebufferTexture2D(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0,
		gl.TEXTURE_2D, tex, 0)
	gl.BlitFramebuffer(0, 0, int32(w), int32(h),
		int32(ix), int32(iy), int32(ix+w), int32(iy+h),
		gl.COLOR_BUFFER_BIT, gl.LINEAR)
	gl.BindFramebuffer(gl.READ_FRAMEBUFFER, 0)
	gl.DeleteFramebuffers(1, &readFboId)

}
