package covergen

import (
	"embed"
	"fmt"
	"strings"

	"github.com/go-pdf/fpdf"
	"github.com/rs/zerolog/log"
)

const margin = 20
const miwebbLogoName = "miwebbLogo"
const miwebbLogoPath = "miwebb.white.png"
const logoScale = 0.6

const gridXTicks = 10
const gridYTicks = 6
const gridMargin = 5
const lineHeight = 1.2
const maxFontSize = 100

var bgColor = Color{25, 25, 25}
var textColor = Color{255, 255, 255}
var gridColor = Color{80, 80, 80}
var gridNoduleColor = Color{255, 255, 255}

//go:embed miwebb.white.png font_embed
var fs embed.FS

const fontsEmbedPrefix = "font_embed/"

// Copied from fPDF's source, can't get it dynamically
// constant for mm is just to ensure you don't change it without noting the scale factor that depends on it
const scaleUnit = "mm"
const scaleFactor = 72.0 / 25.4

type CoverSettings struct {
	Number       string
	NumberPrefix string

	CustomerName string
	HLColor      Color
}

func GenerateInvoice(settings CoverSettings) (*fpdf.Fpdf, error) {
	pdf := fpdf.New("P", scaleUnit, "A4", "")

	if err := addEmbeddedFont(pdf, "Mark Medium", "", "Mark-Medium"); err != nil {
		log.Fatal().Err(err).Msg("Error adding font")
	}
	if err := addEmbeddedFont(pdf, "Mark Light", "", "Mark-Light"); err != nil {
		log.Fatal().Err(err).Msg("Error adding font")
	}
	miwebbLogo, err := addEmbeddedLogo(pdf, miwebbLogoName, miwebbLogoPath)
	if err != nil {
		log.Fatal().Err(err).Msg("Error adding logo")
	}

	pdf.SetMargins(margin, margin, margin)
	pageWidth, pageHeight := pdf.GetPageSize()
	pdf.SetAutoPageBreak(false, 0)

	pdf.SetHeaderFunc(func() {
		bgColor.Fill(pdf)
		pdf.Rect(0, 0, pageWidth, pageHeight, "F")
		pdf.Image(miwebbLogoName, margin-5, margin, miwebbLogo.Width()*logoScale, miwebbLogo.Height()*logoScale, false, "", 0, "")
	})

	pdf.AddPage()

	drawCustomerName(pdf, settings)
	drawInvoiceNumber(pdf, settings)
	drawMiwebbLink(pdf)

	pdf.AddPage()
	drawCustomerName(pdf, settings)

	if pdf.Err() {
		return nil, pdf.Error()
	}

	return pdf, nil
}
func drawInvoiceNumber(pdf *fpdf.Fpdf, settings CoverSettings) {
	_, pageHeight := pdf.GetPageSize()

	pdf.MoveTo(margin, pageHeight-margin)
	pdf.SetFont("Mark Light", "", 9)
	textColor.Text(pdf)

	pdf.TransformBegin()
	pdf.TransformRotate(90, margin, pageHeight-margin)
	invoiceText := strings.ToUpper(fmt.Sprintf("%s %s", settings.NumberPrefix, settings.Number))
	pdf.CellFormat(0, 1, invoiceText, "", 0, "LT", false, 0, "")
	pdf.TransformEnd()

	_, h := pdf.GetFontSize()
	// X is 'margin + half text height'
	x := float64(margin) + h/2
	// Y is 'where the text ended' + some gap
	y := pageHeight - margin - pdf.GetStringWidth(invoiceText) - 5

	textColor.Draw(pdf)
	pdf.SetLineWidth(0.01)
	pdf.Line(x, y, x, y-10)
}

func drawMiwebbLink(pdf *fpdf.Fpdf) {
	pageWidth, pageHeight := pdf.GetPageSize()
	pdf.MoveTo(pageWidth-margin, pageHeight-margin)

	pdf.SetFont("Mark Light", "", 9)
	textColor.Text(pdf)
	pdf.CellFormat(0, 0, "WWW.MIWEBB.COM", "", 0, "RB", false, 0, "https://www.miwebb.com")
}

func drawCustomerName(pdf *fpdf.Fpdf, settings CoverSettings) {
	ml, _, mr, _ := pdf.GetMargins()
	pw, ph := pdf.GetPageSize()

	width := pw - ml - mr
	height := (width/gridXTicks - 1) * gridYTicks
	text := strings.ToUpper(settings.CustomerName)

	// Draw the grid with a weird gray color
	gridColor.Draw(pdf)
	pdf.SetLineWidth(0.01)
	gridX, gridY := ml, (ph/2)-(height/2)
	drawGrid(pdf, gridX, gridY, width, height, gridXTicks, gridYTicks)

	// Draw the highlight on the right
	drawNoduleLine(
		pdf,
		gridX+width, gridY,
		gridX+width, gridY+height,
		1.2, gridNoduleColor,
		0.25, settings.HLColor)

	pdf.SetFont("Mark Medium", "", 10)
	// Estimate font size, and calculate line height immediately after
	pdf.SetFontSize(estimateFontSize(pdf, text, width, height, gridMargin))
	_, sz := pdf.GetFontSize()
	sz *= lineHeight

	_, textHeight := stringSize(pdf, text, sz, 0)
	// Centered vertically:
	//  x = <gridX> + <margin>
	//  y = <gridY> + <gridH>/2 - textHeight/2

	pdf.MoveTo(ml+gridMargin, ph/2-(textHeight/2))
	textColor.Text(pdf)
	pdf.MultiCell(width-2*gridMargin, sz, text, "", "ML", false)
}

func drawGrid(pdf *fpdf.Fpdf, x, y, w, h float64, xs, ys int) {
	// Horizontal lines
	for i := 0; i < ys; i++ {
		yOff := y + (h/float64(ys-1))*float64(i)
		pdf.Line(x, yOff, x+w, yOff)
	}

	// Vertical line(pw/2)-(width/2)s
	for i := 0; i < xs; i++ {
		xOff := x + (w/float64(xs-1))*float64(i)
		pdf.Line(xOff, y, xOff, y+h)
	}
}

func drawNoduleLine(pdf *fpdf.Fpdf, x1, y1, x2, y2, squareSize float64, squareColor Color, lineWidth float64, lineColor Color) {
	lineColor.Draw(pdf)
	pdf.SetLineWidth(lineWidth)
	pdf.Line(x1, y1, x2, y2)

	squareColor.Fill(pdf)
	pdf.Rect(x1-(squareSize/2), y1-(squareSize/2), squareSize, squareSize, "F")
	pdf.Rect(x2-(squareSize/2), y2-(squareSize/2), squareSize, squareSize, "F")
}

func estimateFontSize(pdf *fpdf.Fpdf, text string, containerWidth, containerHeight, margin float64) float64 {
	// Margin is on both sides haha, but I add an extra just for 'safety'
	maxWidth := containerWidth - 3*margin
	maxHeight := containerHeight - 3*margin

	// Probably smallest we can start with
	for fontSize := 10.; fontSize <= maxFontSize; fontSize += 1 {
		width, height := stringSize(pdf, text, (fontSize/scaleFactor)*lineHeight, fontSize)
		if width >= maxWidth || height >= maxHeight {
			return fontSize - 1
		}
	}

	return maxFontSize
}

func stringSize(pdf *fpdf.Fpdf, text string, lineHeight, fontSizePt float64) (width, height float64) {
	if fontSizePt == 0 {
		fontSizePt, _ = pdf.GetFontSize()
	}

	lines := strings.Split(text, "\n")
	height = float64(len(lines)) * lineHeight
	for _, line := range lines {
		lineWidth := float64(pdf.GetStringSymbolWidth(line)) * fontSizePt / scaleFactor / 1000
		if lineWidth > width {
			width = lineWidth
		}
	}

	return
}

func addEmbeddedFont(pdf *fpdf.Fpdf, family, style, path string) error {
	jsonData, err := fs.ReadFile(fontsEmbedPrefix + path + ".json")
	if err != nil {
		return err
	}
	zData, err := fs.ReadFile(fontsEmbedPrefix + path + ".z")
	if err != nil {
		return err
	}

	pdf.AddFontFromBytes(family, style, jsonData, zData)
	if pdf.Err() {
		return pdf.Error()
	}

	return nil
}

func addEmbeddedLogo(pdf *fpdf.Fpdf, name, path string) (*fpdf.ImageInfoType, error) {
	reader, err := fs.Open(path)
	if err != nil {
		return nil, err
	}

	handle := pdf.RegisterImageOptionsReader(name, fpdf.ImageOptions{
		ImageType: "PNG",
		ReadDpi:   true,
	}, reader)

	if pdf.Err() {
		return nil, pdf.Error()
	}

	return handle, nil
}
