// Sfnt2Woff is a native go port of the JS library
// https://github.com/fontello/ttf2woff
// that converts sfnt fonts (.ttf and .otf) to .woff fonts

package font

import (
	"bytes"
	"compress/zlib"
	"encoding/binary"
	"fmt"
	"math"
	"sort"
)

var (
	// sfnt2Woff offset
	SFNT_OFFSET_TAG      = 0
	SFNT_OFFSET_CHECKSUM = 4
	SFNT_OFFSET_OFFSET   = 8
	SFNT_OFFSET_LENGTH   = 12

	// sfnt2Woff entry offset
	SFNT_ENTRY_OFFSET_FLAVOR              = 0
	SFNT_ENTRY_OFFSET_VERSION_MAJ         = 4
	SFNT_ENTRY_OFFSET_VERSION_MIN         = 6
	SFNT_ENTRY_OFFSET_CHECKSUM_ADJUSTMENT = 8

	// woff offset
	WOFF_OFFSET_MAGIC            = 0
	WOFF_OFFSET_FLAVOR           = 4
	WOFF_OFFSET_SIZE             = 8
	WOFF_OFFSET_NUM_TABLES       = 12
	WOFF_OFFSET_RESERVED         = 14
	WOFF_OFFSET_SFNT_SIZE        = 16
	WOFF_OFFSET_VERSION_MAJ      = 20
	WOFF_OFFSET_VERSION_MIN      = 22
	WOFF_OFFSET_META_OFFSET      = 24
	WOFF_OFFSET_META_LENGTH      = 28
	WOFF_OFFSET_META_ORIG_LENGTH = 32
	WOFF_OFFSET_PRIV_OFFSET      = 36
	WOFF_OFFSET_PRIV_LENGTH      = 40

	// woff entry offset
	WOFF_ENTRY_OFFSET_TAG          = 0
	WOFF_ENTRY_OFFSET_OFFSET       = 4
	WOFF_ENTRY_OFFSET_COMPR_LENGTH = 8
	WOFF_ENTRY_OFFSET_LENGTH       = 12
	WOFF_ENTRY_OFFSET_CHECKSUM     = 16

	// magic
	MAGIC_WOFF                uint32 = 0x774f4646
	MAGIC_CHECKSUM_ADJUSTMENT uint32 = 0xb1b0afba

	// sizes
	SIZE_OF_WOFF_HEADER      = 44
	SIZE_OF_WOFF_ENTRY       = 20
	SIZE_OF_SFNT_HEADER      = 12
	SIZE_OF_SFNT_TABLE_ENTRY = 16
)

type TableEntry struct {
	Tag      []byte
	CheckSum uint32
	Offset   uint32
	Length   uint32
}

func longAlign(n uint32) uint32 {
	return (n + 3) & ^uint32(3)
}

func calcChecksum(buf []byte) uint32 {
	var sum uint32 = 0
	var nlongs = len(buf) / 4

	for i := 0; i < nlongs; i++ {
		var t = binary.BigEndian.Uint32(buf[i*4:])
		sum = sum + t
	}
	return sum
}

func Sfnt2Woff(fontBuf []byte) ([]byte, error) {
	numTables := binary.BigEndian.Uint16(fontBuf[4:])

	woffHeader := make([]byte, SIZE_OF_WOFF_HEADER)
	binary.BigEndian.PutUint32(woffHeader[WOFF_OFFSET_MAGIC:], MAGIC_WOFF)
	binary.BigEndian.PutUint16(woffHeader[WOFF_OFFSET_NUM_TABLES:], numTables)
	binary.BigEndian.PutUint16(woffHeader[WOFF_OFFSET_SFNT_SIZE:], 0)
	binary.BigEndian.PutUint32(woffHeader[WOFF_OFFSET_META_OFFSET:], 0)
	binary.BigEndian.PutUint32(woffHeader[WOFF_OFFSET_META_LENGTH:], 0)
	binary.BigEndian.PutUint32(woffHeader[WOFF_OFFSET_META_ORIG_LENGTH:], 0)
	binary.BigEndian.PutUint32(woffHeader[WOFF_OFFSET_PRIV_OFFSET:], 0)
	binary.BigEndian.PutUint32(woffHeader[WOFF_OFFSET_PRIV_LENGTH:], 0)

	var entries []TableEntry

	for i := 0; i < int(numTables); i++ {
		table := fontBuf[SIZE_OF_SFNT_HEADER+i*SIZE_OF_SFNT_TABLE_ENTRY:]

		entry := TableEntry{
			Tag:      table[SFNT_OFFSET_TAG : SFNT_OFFSET_TAG+4],
			CheckSum: binary.BigEndian.Uint32(table[SFNT_OFFSET_CHECKSUM:]),
			Offset:   binary.BigEndian.Uint32(table[SFNT_OFFSET_OFFSET:]),
			Length:   binary.BigEndian.Uint32(table[SFNT_OFFSET_LENGTH:]),
		}

		entries = append(entries, entry)
	}
	sort.Slice(entries, func(i, j int) bool {
		return string(entries[i].Tag) < string(entries[j].Tag)
	})

	sfntSize := uint32(SIZE_OF_SFNT_HEADER + int(numTables)*SIZE_OF_SFNT_TABLE_ENTRY)
	tableInfo := make([]byte, int(numTables)*SIZE_OF_WOFF_ENTRY)

	for i := 0; i < len(entries); i++ {
		tableEntry := entries[i]
		if string(tableEntry.Tag) != "head" {
			alignTable := fontBuf[tableEntry.Offset : tableEntry.Offset+longAlign(tableEntry.Length)]

			if calcChecksum(alignTable) != tableEntry.CheckSum {
				return nil, fmt.Errorf("checksum error in table: %v", string(tableEntry.Tag))
			}
		}

		binary.BigEndian.PutUint32(tableInfo[i*SIZE_OF_WOFF_ENTRY+WOFF_ENTRY_OFFSET_TAG:], binary.BigEndian.Uint32(tableEntry.Tag))
		binary.BigEndian.PutUint32(tableInfo[i*SIZE_OF_WOFF_ENTRY+WOFF_ENTRY_OFFSET_LENGTH:], tableEntry.Length)
		binary.BigEndian.PutUint32(tableInfo[i*SIZE_OF_WOFF_ENTRY+WOFF_ENTRY_OFFSET_CHECKSUM:], tableEntry.CheckSum)

		sfntSize += longAlign(tableEntry.Length)
	}

	sfntOffset := uint32(SIZE_OF_SFNT_HEADER + len(entries)*SIZE_OF_SFNT_TABLE_ENTRY)
	csum := calcChecksum(fontBuf[:SIZE_OF_SFNT_HEADER])
	for i := 0; i < len(entries); i++ {
		tableEntry := entries[i]

		b := make([]byte, SIZE_OF_SFNT_TABLE_ENTRY)
		binary.BigEndian.PutUint32(b[SFNT_OFFSET_TAG:], binary.BigEndian.Uint32(tableEntry.Tag))
		binary.BigEndian.PutUint32(b[SFNT_OFFSET_CHECKSUM:], tableEntry.CheckSum)
		binary.BigEndian.PutUint32(b[SFNT_OFFSET_OFFSET:], sfntOffset)
		binary.BigEndian.PutUint32(b[SFNT_OFFSET_LENGTH:], tableEntry.Length)

		sfntOffset += longAlign(tableEntry.Length)
		csum += calcChecksum(b)
		csum += tableEntry.CheckSum
	}

	var checksumAdjustment = MAGIC_CHECKSUM_ADJUSTMENT - csum

	majorVersion := uint16(0)
	minVersion := uint16(1)
	flavor := uint32(0)
	offset := SIZE_OF_WOFF_HEADER + int(numTables)*SIZE_OF_WOFF_ENTRY
	var tableBytes []byte

	for i := 0; i < len(entries); i++ {
		tableEntry := entries[i]

		sfntData := fontBuf[tableEntry.Offset : tableEntry.Offset+tableEntry.Length]
		if string(tableEntry.Tag) == "head" {
			majorVersion = binary.BigEndian.Uint16(sfntData[SFNT_ENTRY_OFFSET_VERSION_MAJ:])
			minVersion = binary.BigEndian.Uint16(sfntData[SFNT_ENTRY_OFFSET_VERSION_MIN:])
			flavor = binary.BigEndian.Uint32(sfntData[SFNT_ENTRY_OFFSET_FLAVOR:])
			binary.BigEndian.PutUint32(sfntData[SFNT_ENTRY_OFFSET_CHECKSUM_ADJUSTMENT:], uint32(checksumAdjustment))
		}

		var res bytes.Buffer
		w := zlib.NewWriter(&res)
		w.Write(sfntData)
		w.Flush()
		w.Close()

		compLength := math.Min(float64(len(res.Bytes())), float64(len(sfntData)))
		length := longAlign(uint32(compLength))

		table := make([]byte, length)
		// only deflate data if the deflated data is actually smaller
		if len(res.Bytes()) >= len(sfntData) {
			copy(table, sfntData)
		} else {
			copy(table, res.Bytes())
		}

		binary.BigEndian.PutUint32(tableInfo[i*SIZE_OF_WOFF_ENTRY+WOFF_ENTRY_OFFSET_OFFSET:], uint32(offset))

		offset += len(table)

		binary.BigEndian.PutUint32(tableInfo[i*SIZE_OF_WOFF_ENTRY+WOFF_ENTRY_OFFSET_COMPR_LENGTH:], uint32(compLength))

		tableBytes = append(tableBytes, table...)

	}

	woffSize := uint32(len(woffHeader) + len(tableInfo) + len(tableBytes))
	binary.BigEndian.PutUint32(woffHeader[WOFF_OFFSET_SIZE:], woffSize)
	binary.BigEndian.PutUint32(woffHeader[WOFF_OFFSET_SFNT_SIZE:], sfntSize)
	binary.BigEndian.PutUint16(woffHeader[WOFF_OFFSET_VERSION_MAJ:], majorVersion)
	binary.BigEndian.PutUint16(woffHeader[WOFF_OFFSET_VERSION_MIN:], minVersion)
	binary.BigEndian.PutUint32(woffHeader[WOFF_OFFSET_FLAVOR:], flavor)

	var out []byte
	out = append(out, woffHeader...)
	out = append(out, tableInfo...)
	out = append(out, tableBytes...)

	return out, nil
}
