package glyphs

import (
	"errors"
	"image"
	"image/color"
	"image/draw"
	"image/png"
	"os"
	"unicode"

	"github.com/bloeys/nterm/assert"
	"github.com/bloeys/nterm/consts"
	"github.com/golang/freetype/truetype"
	"golang.org/x/image/font"
	"golang.org/x/image/math/fixed"
)

type FontAtlas struct {
	Font   *truetype.Font
	Face   font.Face
	Img    *image.RGBA
	Glyphs map[rune]FontAtlasGlyph

	//SpaceAdvance is the advance of a space char
	SpaceAdvance float32
	//LineHeight is the height of metrics.Height
	LineHeight float32
}

type FontAtlasGlyph struct {
	Rune  rune
	U     float32
	V     float32
	SizeU float32
	SizeV float32

	Ascent   float32
	Descent  float32
	BearingX float32
	Advance  float32
}

//NewFontAtlasFromFile reads a TTF or TTC file and produces a font texture atlas containing
//all its characters using the specified options. The atlas uses equally sized tiles
//such that all characters use an equal horizontal/vertical on the atlas.
//If the character is smaller than the tile then the rest of the tile is empty.
//
//Only monospaced fonts are supported
func NewFontAtlasFromFile(fontFile string, fontOptions *truetype.Options) (*FontAtlas, error) {

	fBytes, err := os.ReadFile(fontFile)
	if err != nil {
		return nil, err
	}

	f, err := truetype.Parse(fBytes)
	if err != nil {
		return nil, err
	}

	face := truetype.NewFace(f, fontOptions)
	return NewFontAtlasFromFont(f, face, uint(fontOptions.Size))
}

func calcNeededAtlasSize(glyphs []rune, face font.Face, charPaddingXFixed, charPaddingYFixed fixed.Int26_6) (atlasSizeX, atlasSizeY int) {

	//Calculate needed atlas size
	atlasSizeX = 512
	atlasSizeY = 512
	largestLineDescent := fixed.I(0)
	spaceAdv, _ := face.GlyphAdvance(' ')
	lineHeight := face.Metrics().Height
	foundAtlasSize := false
	for !foundAtlasSize {

		foundAtlasSize = true
		dotX := spaceAdv + charPaddingXFixed
		dotY := lineHeight
		atlasSizeXFixed := fixed.I(atlasSizeX)
		atlasSizeYFixed := fixed.I(atlasSizeY)
		for i := 0; i < len(glyphs); i++ {

			//Prepare all glyph metrics
			g := glyphs[i]
			gBounds, _, _ := face.GlyphBounds(g)
			bearingXFixed := gBounds.Min.X
			gWidthFixed := gBounds.Max.X - gBounds.Min.X
			descentAbsFixed := absI26_6(gBounds.Max.Y)

			if descentAbsFixed > largestLineDescent {
				largestLineDescent = descentAbsFixed
			}

			//If bearing is negative this char might overlap with the previous one.
			//So we need to move the dot so the drawer won't overlap even after a negative offset
			if bearingXFixed < 0 {
				dotX += absI26_6(bearingXFixed)
			}

			// Calculate distance dot will move after drawing. Advance normally if line has space,
			// otherwise go to next line and reset X position.
			distToMoveX := bearingXFixed + gWidthFixed + charPaddingXFixed

			//If we have no more space go to next line
			if dotX+distToMoveX >= atlasSizeXFixed {

				dotX = charPaddingXFixed
				if bearingXFixed < 0 {
					dotX += absI26_6(bearingXFixed)
				}

				dotY += lineHeight + fixed.I(largestLineDescent.Ceil()) + charPaddingYFixed
				largestLineDescent = 0

				//If we have only one more empty line then resize to be safe against descents being clipped
				if dotY+lineHeight+charPaddingYFixed >= atlasSizeYFixed {
					atlasSizeX *= 2
					atlasSizeY *= 2
					foundAtlasSize = false
					break
				}
			}

			dotX += distToMoveX
		}
	}

	return atlasSizeX, atlasSizeY
}

//NewFontAtlasFromFile uses the passed font to produce a font texture atlas containing
//all its characters using the specified options. The atlas uses equally sized tiles
//such that all characters use an equal horizontal/vertical on the atlas.
//If the character is smaller than the tile then the rest of the tile is empty.
//
//Only monospaced fonts are supported.
func NewFontAtlasFromFont(f *truetype.Font, face font.Face, pointSize uint) (*FontAtlas, error) {

	const maxAtlasSize = 8192
	const charPaddingXFixed = 4 << 6
	const charPaddingYFixed = 4 << 6

	glyphs := getGlyphsFromRuneRanges(getGlyphRangesFromFont(f))
	assert.T(len(glyphs) > 0, "no glyphs")

	atlasSizeX, atlasSizeY := calcNeededAtlasSize(glyphs, face, charPaddingXFixed, charPaddingYFixed)
	if atlasSizeX > maxAtlasSize {
		return nil, errors.New("atlas size went beyond the maximum of 8192*8192")
	}

	//Create atlas
	lineHeight := face.Metrics().Height
	spaceAdv, _ := face.GlyphAdvance(' ')
	atlas := &FontAtlas{
		Font:   f,
		Face:   face,
		Img:    image.NewRGBA(image.Rect(0, 0, atlasSizeX, atlasSizeY)),
		Glyphs: make(map[rune]FontAtlasGlyph, len(glyphs)),

		SpaceAdvance: I26_6ToF32(spaceAdv),
		LineHeight:   I26_6ToF32(lineHeight),
	}

	//Clear background to black
	draw.Draw(atlas.Img, atlas.Img.Bounds(), image.Black, image.Point{}, draw.Src)
	drawer := &font.Drawer{
		Dst:  atlas.Img,
		Src:  image.White,
		Face: face,
	}

	//Put glyphs on atlas
	drawer.Dot.X = spaceAdv + charPaddingXFixed
	drawer.Dot.Y = lineHeight

	const drawBoundingBoxes bool = false
	largestLineDescent := fixed.I(0)
	atlasSizeXFixed := fixed.I(atlasSizeX)
	atlasSizeYFixed := fixed.I(atlasSizeY)
	for _, g := range glyphs {

		//Glyph metrics
		gBounds, gAdvanceFixed, _ := face.GlyphBounds(g)
		bearingXFixed := gBounds.Min.X
		ascentAbsFixed := absI26_6(gBounds.Min.Y)
		descentAbsFixed := absI26_6(gBounds.Max.Y)
		gWidthFixed := gBounds.Max.X - gBounds.Min.X

		if descentAbsFixed > largestLineDescent {
			largestLineDescent = descentAbsFixed
		}

		//If bearing is negative this char might overlap with the previous one.
		//So we need to move the dot so the drawer won't overlap even after a negative offset
		if bearingXFixed < 0 {
			drawer.Dot.X += absI26_6(bearingXFixed)
		}

		// Position dot by calculating how much it will move after drawing, and if there isn't enough space
		// move to next line then draw
		nextDotPosDeltaX := bearingXFixed + gWidthFixed + charPaddingXFixed
		if drawer.Dot.X+nextDotPosDeltaX >= atlasSizeXFixed {

			assert.T(drawer.Dot.Y+lineHeight < atlasSizeYFixed, "Failed to create atlas because it did not fit all glyphs. Is calcNeededAtlasSize wrong?")

			drawer.Dot.X = charPaddingXFixed
			if bearingXFixed < 0 {
				drawer.Dot.X += absI26_6(bearingXFixed)
			}

			drawer.Dot.Y += lineHeight + fixed.I(largestLineDescent.Ceil()) + charPaddingYFixed
			largestLineDescent = 0
		}

		drawer.Dot = fixed.P(drawer.Dot.X.Floor(), drawer.Dot.Y.Floor())

		//Build and insert glyph struct
		gTopLeft := image.Point{
			X: (drawer.Dot.X + bearingXFixed).Floor(),
			Y: (drawer.Dot.Y - ascentAbsFixed).Floor(),
		}

		gBotRight := image.Point{
			X: (drawer.Dot.X + bearingXFixed + gWidthFixed).Ceil(),
			Y: (drawer.Dot.Y + descentAbsFixed).Ceil(),
		}

		atlas.Glyphs[g] = FontAtlasGlyph{
			Rune:  g,
			U:     float32(gTopLeft.X),
			V:     float32(atlasSizeY - gBotRight.Y),
			SizeU: float32(gBotRight.X - gTopLeft.X),
			SizeV: float32(gBotRight.Y - gTopLeft.Y),

			Ascent:   I26_6ToF32(ascentAbsFixed),
			Descent:  I26_6ToF32(descentAbsFixed),
			BearingX: I26_6ToF32(bearingXFixed),
			Advance:  I26_6ToF32(gAdvanceFixed),
		}

		if consts.Mode_Debug && drawBoundingBoxes {
			rect := image.Rectangle{
				Min: gTopLeft,
				Max: gBotRight,
			}
			drawRectOutline(atlas.Img, rect, color.NRGBA{B: 255, A: 128})
		}

		//Draw glyph
		imgRect, mask, maskp, _, _ := face.Glyph(drawer.Dot, g)
		draw.DrawMask(drawer.Dst, imgRect, drawer.Src, image.Point{}, mask, maskp, draw.Over)
		drawer.Dot.X += nextDotPosDeltaX
	}

	// // This is a test section that uses the drawer to draw an Arabic
	// // string at the bottom of the atlas. Useful to compare glyph renderer against a 'correct' implementation
	// str := "السلام عليكم"
	// rs := []rune(str)
	// finalR := make([]rune, 0)
	// prevRune := invalidRune
	// for i := len(rs) - 1; i >= 0; i-- {
	// 	var g FontAtlasGlyph
	// 	if i > 0 {
	// 		//start or middle of sentence
	// 		g = GlyphFromRunes(atlas.Glyphs, rs[i], rs[i-1], prevRune)
	// 	} else {
	// 		//Last character
	// 		g = GlyphFromRunes(atlas.Glyphs, rs[i], invalidRune, prevRune)
	// 	}
	// 	prevRune = rs[i]
	// 	finalR = append(finalR, g.R)
	// }
	// drawer.Dot.Y += lineHeightFixed + charPaddingYFixed
	// drawer.DrawString(string(finalR))

	return atlas, nil
}

func drawRectOutline(img *image.RGBA, rect image.Rectangle, color color.NRGBA) {

	rowPixCount := img.Stride / 4

	topLeft := img.PixOffset(rect.Min.X, rect.Min.Y)
	botRight := img.PixOffset(rect.Max.X, rect.Max.Y)

	for i := topLeft; i <= botRight; i += 4 {

		pixel := i / 4
		y := pixel / rowPixCount
		x := pixel - y*rowPixCount

		if x >= rect.Min.X && x <= rect.Max.X && y >= rect.Min.Y && y <= rect.Max.Y {
			if x == rect.Min.X || x == rect.Max.X || y == rect.Min.Y || y == rect.Max.Y {
				img.Pix[i+3] = color.A
				img.Pix[i+2] = color.B
				img.Pix[i+1] = color.G
				img.Pix[i+0] = color.R
			}
		}
	}
}

func DrawRect(img *image.RGBA, rect image.Rectangle, color color.NRGBA) {

	rowPixCount := img.Stride / 4

	topLeft := img.PixOffset(rect.Min.X, rect.Min.Y)
	botRight := img.PixOffset(rect.Max.X, rect.Max.Y)

	//Draw top line
	for i := topLeft; i <= botRight; i += 4 {

		pixel := i / 4
		y := pixel / rowPixCount
		x := pixel - y*rowPixCount

		if x >= rect.Min.X && x <= rect.Max.X && y >= rect.Min.Y && y <= rect.Max.Y {
			img.Pix[i+3] = color.A
			img.Pix[i+2] = color.B
			img.Pix[i+1] = color.G
			img.Pix[i+0] = color.R
		}
	}
}

func I26_6ToF32(x fixed.Int26_6) float32 {
	const lower6BitMask = 1<<6 - 1

	if x > 0 {
		return float32(x.Floor()) + float32(x&lower6BitMask)/64
	} else {
		return float32(x.Floor()) - float32(x&lower6BitMask)/64
	}
}

func DrawVerticalLine(img *image.RGBA, posX int, color color.NRGBA) {

	rowLength := img.Stride
	start := img.PixOffset(posX, 0)

	for i := start; i < len(img.Pix); i += rowLength {

		img.Pix[i+3] = color.A
		img.Pix[i+2] = color.B
		img.Pix[i+1] = color.G
		img.Pix[i+0] = color.R
	}
}

func DrawHorizontalLine(img *image.RGBA, posY int, color color.NRGBA) {

	rowLength := img.Stride
	start := img.PixOffset(0, posY)

	//Horizontal line
	for i := start; i < start+rowLength; i += 4 {

		img.Pix[i+3] = color.A
		img.Pix[i+2] = color.B
		img.Pix[i+1] = color.G
		img.Pix[i+0] = color.R
	}
}

func SaveImgToPNG(img image.Image, file string) error {

	outFile, err := os.Create(file)
	if err != nil {
		return err
	}
	defer outFile.Close()

	err = png.Encode(outFile, img)
	if err != nil {
		return err
	}

	return nil
}

//getGlyphRangesFromFont returns a list of ranges, each range is: [i][0]<=range<[i][1]
func getGlyphRangesFromFont(f *truetype.Font) (ret [][2]rune) {

	isRuneInPrivateUseArea := func(r rune) bool {
		return 0xe000 <= r && r <= 0xf8ff ||
			0xf0000 <= r && r <= 0xffffd ||
			0x100000 <= r && r <= 0x10fffd
	}

	rr := [2]rune{-1, -1}
	for r := rune(0); r <= unicode.MaxRune; r++ {
		if isRuneInPrivateUseArea(r) {
			continue
		}
		if f.Index(r) == 0 {
			continue
		}
		if rr[1] == r {
			rr[1] = r + 1
			continue
		}
		if rr[0] != -1 {
			ret = append(ret, rr)
		}
		rr = [2]rune{r, r + 1}
	}
	if rr[0] != -1 {
		ret = append(ret, rr)
	}
	return ret
}

//getGlyphsFromRuneRanges takes ranges of runes and produces an array of all the runes in these ranges
func getGlyphsFromRuneRanges(ranges [][2]rune) []rune {

	out := make([]rune, 0)
	for _, rr := range ranges {

		temp := make([]rune, 0, rr[1]-rr[0])
		for r := rr[0]; r < rr[1]; r++ {
			temp = append(temp, r)
		}

		out = append(out, temp...)
	}

	return out
}

func absI26_6(x fixed.Int26_6) fixed.Int26_6 {
	if x < 0 {
		return -x
	}

	return x
}
