package wpdf

import (
	"gogs.mikescher.com/BlackForestBytes/goext/exerr"
	"gogs.mikescher.com/BlackForestBytes/goext/langext"
	"gogs.mikescher.com/BlackForestBytes/goext/rext"
	"regexp"
	"strconv"
)

// Column specifier:
//
// - `{number}`:             Use this amount of space
// - `auto`:                 Use the needed space for the content
// - `*` / `fr`:             Use the remaining space, evenly distributed, shrink down to auto
// - `{num}fr` / `{num}*`:   Use the remaining space, evenly distributed (weighted), shrink down to auto
//
// # TableBuilder
//    - PadX/PadY:            Padding between cells
//    - DefaultStyle:         Default style for cells
//
// # TableCellStyleOpt
//    - MultiCell:            Use wpdf.MultiCell() instead of wpdf.Cell()  --> supports linebreaks
//    - Ellipsize:            Ellipsize text if too long
//    - PaddingHorz:          Additional horizontal padding inside of cell to space text around
//    - PDFCellOpt:           Normal styling options (evtl not all are supported, depending on MultiCell: true/false)

var regexTableColumnSpecFr = rext.W(regexp.MustCompile(`^(?P<num>[0-9]*)(fr|\*)$`))

type TableBuilder struct {
	builder *WPDFBuilder

	padx             float64
	pady             float64
	rows             []tableRow
	defaultCellStyle *TableCellStyleOpt
	columnWidths     *[]string
	debug            *bool
}

func (r tableRow) maxFontSize(defaultFontSize float64) float64 {
	mfs := defaultFontSize
	for _, cell := range r.cells {
		if cell.Style.fontSizeOverride != nil {
			mfs = max(mfs, *cell.Style.fontSizeOverride)
		}
	}
	return mfs
}

func (b *TableBuilder) Widths(v ...string) *TableBuilder {
	b.columnWidths = &v
	return b
}

func (b *TableBuilder) DefaultStyle(s *TableCellStyleOpt) *TableBuilder {
	b.defaultCellStyle = s
	return b
}

func (b *TableBuilder) PadX(v float64) *TableBuilder {
	b.padx = v
	return b
}

func (b *TableBuilder) PadY(v float64) *TableBuilder {
	b.pady = v
	return b
}

func (b *TableBuilder) AddRow(cells ...TableCell) *TableBuilder {
	b.rows = append(b.rows, tableRow{cells: cells})
	return b
}

func (b *TableBuilder) AddRowWithStyle(style *TableCellStyleOpt, cells ...string) *TableBuilder {
	tcels := make([]TableCell, 0, len(cells))
	for _, cell := range cells {
		tcels = append(tcels, TableCell{Content: cell, Style: *style})
	}

	b.rows = append(b.rows, tableRow{cells: tcels})

	return b
}

func (b *TableBuilder) AddRowDefaultStyle(cells ...string) *TableBuilder {
	tcels := make([]TableCell, 0, len(cells))
	for _, cell := range cells {
		tcels = append(tcels, TableCell{Content: cell, Style: langext.Coalesce(b.defaultCellStyle, TableCellStyleOpt{})})
	}

	b.rows = append(b.rows, tableRow{cells: tcels})

	return b
}

func (b *TableBuilder) BuildRow() *TableRowBuilder {
	return &TableRowBuilder{tabbuilder: b, cells: make([]TableCell, 0)}
}

func (b *TableBuilder) Build() {
	builder := b.builder

	debug := langext.Coalesce(b.debug, b.builder.debug)

	if len(b.rows) == 0 {
		return // nothing to do
	}

	_, pageHeight := builder.FPDF().GetPageSize()
	pbEnabled, pbMargin := builder.FPDF().GetAutoPageBreak()

	builder.FPDF().SetAutoPageBreak(false, 0) // manually handle pagebreak in tables
	defer func() { builder.FPDF().SetAutoPageBreak(pbEnabled, pbMargin) }()

	columnWidths := b.calculateColumns()

	columnCount := len(columnWidths)

	for i, dat := range b.rows {
		if len(dat.cells) != columnCount {
			builder.FPDF().SetError(exerr.New(exerr.TypeInternal, "data must have the same length as header").Int("idx", i).Build())
			return
		}
	}

	defaultFontSize, _ := builder.FPDF().GetFontSize()

	for rowIdx, row := range b.rows {
		nextY := builder.GetY()
		for cellIdx, cell := range row.cells {

			str := cell.Content
			style := cell.Style

			ellipsize := langext.Coalesce(style.ellipsize, true)
			cellPaddingHorz := langext.Coalesce(style.paddingHorz, 2)

			fillHeight := langext.Coalesce(style.fillHeight, false)

			bx := builder.GetX()
			by := builder.GetY()

			cellWidth := columnWidths[cellIdx]

			_ = fillHeight // TODO implement, but how?? ( cells with fillHeight=true should have a border of the full column height, even if another column is growing it, but we do not know teh height beforehand ... )

			if langext.Coalesce(style.multiCell, true) {

				builder.MultiCell(str, style.PDFCellOpt.Copy().ToMulti().Width(cellWidth).Debug(debug))

			} else {

				if ellipsize {
					if builder.GetStringWidth(str, style.PDFCellOpt) > (cellWidth - cellPaddingHorz) {
						for builder.GetStringWidth(str+"...", style.PDFCellOpt) > (cellWidth-cellPaddingHorz) && len(str) > 0 {
							str = str[:len(str)-1]
						}
						str += "..."
					}
				}

				builder.Cell(str, style.PDFCellOpt.Copy().Width(cellWidth).Debug(debug))

			}

			nextY = max(nextY, builder.GetY())
			builder.SetXY(bx+cellWidth+b.padx, by)
		}
		builder.SetY(nextY + b.pady)

		if rowIdx < len(b.rows)-1 && pbEnabled && (builder.GetY()+b.rows[rowIdx+1].maxFontSize(defaultFontSize)) > (pageHeight-pbMargin) {
			builder.FPDF().AddPage()
		}
	}

}

func (b *TableBuilder) calculateColumns() []float64 {
	pageWidthTotal, _ := b.builder.FPDF().GetPageSize()
	marginLeft, _, marginRight, _ := b.builder.FPDF().GetMargins()
	pageWidth := pageWidthTotal - marginLeft - marginRight

	columnDef := make([]string, 0)

	if b.columnWidths != nil {
		columnDef = *b.columnWidths
	} else if len(b.rows) > 0 {
		columnDef = make([]string, len(b.rows[0].cells))
		for i := range columnDef {
			columnDef[i] = "*"
		}
	} else {
		return []float64{}
	}

	columnWidths := make([]float64, len(columnDef))

	frColumnWidthCount := 0
	frColumnWeights := make([]float64, len(columnDef))
	remainingWidth := pageWidth - (float64(len(columnDef)-1) * b.padx)
	autoWidths := make([]float64, len(columnDef))

	for colIdx := range columnDef {
		w := float64(0)
		for _, row := range b.rows {
			if len(row.cells) > colIdx {
				w = max(w, b.builder.GetStringWidth(row.cells[colIdx].Content, row.cells[colIdx].Style.PDFCellOpt))
			}
		}
		autoWidths[colIdx] = w
	}

	for colIdx, col := range columnDef {

		maxPadHorz := float64(0)

		minWidth := float64(0)
		for _, row := range b.rows {
			if len(row.cells) > colIdx {

				ph := langext.Coalesce(row.cells[colIdx].Style.paddingHorz, 2)
				mw := langext.Coalesce(row.cells[colIdx].Style.minWidth, 0)

				minWidth = max(minWidth, ph+mw)

				maxPadHorz = max(maxPadHorz, ph)
			}
		}

		if col == "auto" {

			w := max(autoWidths[colIdx]+maxPadHorz, minWidth)

			columnWidths[colIdx] = w
			remainingWidth -= w

		} else if match, ok := regexTableColumnSpecFr.MatchFirst(col); ok {

			if match.GroupByName("num").Value() == "" {
				w := minWidth

				frColumnWidthCount += 1
				frColumnWeights[colIdx] = 1
				columnWidths[colIdx] = w
				remainingWidth -= w
			} else {
				w := minWidth

				n, _ := strconv.Atoi(match.GroupByName("num").Value())
				frColumnWidthCount += n
				frColumnWeights[colIdx] = float64(n)
				columnWidths[colIdx] = w
				remainingWidth -= w
			}

		} else {

			if w, err := strconv.ParseFloat(col, 64); err == nil {
				w = max(w, minWidth)

				columnWidths[colIdx] = w
				remainingWidth -= w
			} else {
				b.builder.FPDF().SetError(exerr.New(exerr.TypeInternal, "invalid column width").Str("width", col).Build())
				w = max(w, minWidth)

				columnWidths[colIdx] = w
				remainingWidth -= w
				return nil
			}

		}
	}

	if remainingWidth < 0 {
		// no remaining space to distribute
		return columnWidths
	}

	{
		rmSub := 0.0
		for i := range columnDef {
			if frColumnWeights[i] != 0 {
				w := min(autoWidths[i], (remainingWidth/float64(frColumnWidthCount))*frColumnWeights[i])
				rmSub += w - columnWidths[i]
				columnWidths[i] = w
			}
		}
		remainingWidth -= rmSub
	}

	if remainingWidth > 0.01 {
		rmSub := 0.0
		for i, _ := range columnDef {
			if frColumnWeights[i] != 0 {
				addW := (remainingWidth / float64(frColumnWidthCount)) * frColumnWeights[i]
				rmSub += addW
				columnWidths[i] += addW
			}
		}
		remainingWidth -= rmSub
	}

	return columnWidths
}

func (b *TableBuilder) RowCount() int {
	return len(b.rows)
}

func (b *TableBuilder) Debug(v bool) *TableBuilder {
	b.debug = &v
	return b
}

func (b *WPDFBuilder) Table() *TableBuilder {
	return &TableBuilder{
		builder:          b,
		rows:             make([]tableRow, 0),
		pady:             2,
		padx:             2,
		defaultCellStyle: defaultTableStyle(),
	}
}

func defaultTableStyle() *TableCellStyleOpt {
	return &TableCellStyleOpt{
		PDFCellOpt: *NewPDFCellOpt().
			FontSize(float64(8)).
			Border(BorderFull).
			BorderColorHex(uint32(0x666666)).
			FillColorHex(uint32(0xF0F0F0)).
			TextColorHex(uint32(0x000000)).
			FillBackground(true),
		minWidth:  langext.Ptr(float64(5)),
		ellipsize: langext.PTrue,
		multiCell: langext.PFalse,
	}
}