package png

// http://www.libpng.org/pub/png/spec/1.2/PNG-Contents.html
// https://ftp-osl.osuosl.org/pub/libpng/documents/pngext-1.5.0.html
// https://wiki.mozilla.org/APNG_Specification

import (
	"compress/zlib"
	"hash/crc32"

	"github.com/wader/fq/format"
	"github.com/wader/fq/pkg/bitio"
	"github.com/wader/fq/pkg/decode"
	"github.com/wader/fq/pkg/interp"
	"github.com/wader/fq/pkg/scalar"
)

var iccProfileFormat decode.Group
var exifFormat decode.Group

func init() {
	interp.RegisterFormat(decode.Format{
		Name:        format.PNG,
		Description: "Portable Network Graphics file",
		Groups:      []string{format.PROBE, format.IMAGE},
		DecodeFn:    pngDecode,
		Dependencies: []decode.Dependency{
			{Names: []string{format.ICC_PROFILE}, Group: &iccProfileFormat},
			{Names: []string{format.EXIF}, Group: &exifFormat},
		},
	})
}

const (
	compressionDeflate = 0
)

var compressionNames = scalar.UintMapSymStr{
	compressionDeflate: "deflate",
}

const (
	disposeOpNone       = 0
	disposeOpBackground = 1
	disposeOpPrevious   = 2
)

var disposeOpNames = scalar.UintMapSymStr{
	disposeOpNone:       "none",
	disposeOpBackground: "background",
	disposeOpPrevious:   "previous",
}

const (
	blendOpNone       = 0
	blendOpBackground = 1
)

var blendOpNames = scalar.UintMapSymStr{
	blendOpNone:       "source",
	blendOpBackground: "over",
}

const (
	colorTypeGrayscale          = 0
	colorTypeRGB                = 2
	colorTypePalette            = 3
	colorTypeGrayscaleWithAlpha = 4
	colorTypeRGBA               = 6
)

var colorTypeMap = scalar.UintMapSymStr{
	colorTypeGrayscale:          "grayscale",
	colorTypeRGB:                "rgb",
	colorTypePalette:            "palette",
	colorTypeGrayscaleWithAlpha: "grayscale_alpha",
	colorTypeRGBA:               "rgba",
}

func pngDecode(d *decode.D, _ any) any {
	iEndFound := false
	var colorType uint64

	d.FieldRawLen("signature", 8*8, d.AssertBitBuf([]byte("\x89PNG\r\n\x1a\n")))
	d.FieldStructArrayLoop("chunks", "chunk", func() bool { return d.NotEnd() && !iEndFound }, func(d *decode.D) {
		chunkLength := d.FieldU32("length")
		crcStartPos := d.Pos()
		chunkType := d.FieldUTF8("type", 4)
		// upper/lower case in chunk type is used for flags
		d.SeekRel(-4 * 8)
		d.SeekRel(3)
		d.FieldBool("ancillary")
		d.SeekRel(7)
		d.FieldBool("private")
		d.SeekRel(7)
		d.FieldBool("reserved")
		d.SeekRel(7)
		d.FieldBool("safe_to_copy")
		d.SeekRel(4)

		d.FramedFn(int64(chunkLength)*8, func(d *decode.D) {
			switch chunkType {
			case "IHDR":
				d.FieldU32("width")
				d.FieldU32("height")
				d.FieldU8("bit_depth")
				colorType = d.FieldU8("color_type", colorTypeMap)
				d.FieldU8("compression_method", compressionNames)
				d.FieldU8("filter_method", scalar.UintMapSymStr{
					0: "adaptive_filtering",
				})
				d.FieldU8("interlace_method", scalar.UintMapSymStr{
					0: "none",
					1: "adam7",
				})
			case "tEXt":
				d.FieldUTF8Null("keyword")
				d.FieldUTF8("text", int(d.BitsLeft())/8)
			case "zTXt":
				d.FieldUTF8Null("keyword")
				compressionMethod := d.FieldU8("compression_method", compressionNames)
				dataLen := d.BitsLeft()

				// TODO: make nicer
				d.FieldRawLen("compressed", dataLen)
				d.SeekRel(-dataLen)

				switch compressionMethod {
				case compressionDeflate:
					d.FieldFormatReaderLen("uncompressed", dataLen, zlib.NewReader, decode.FormatFn(func(d *decode.D, _ any) any {
						d.FieldUTF8("text", int(d.BitsLeft()/8))
						return nil
					}))
				default:
					d.FieldRawLen("data", dataLen)
				}
			case "iCCP":
				d.FieldUTF8Null("profile_name")
				compressionMethod := d.FieldU8("compression_method", compressionNames)
				dataLen := d.BitsLeft()

				d.FieldRawLen("compressed", dataLen)
				d.SeekRel(-dataLen)

				switch compressionMethod {
				case compressionDeflate:
					d.FieldFormatReaderLen("uncompressed", dataLen, zlib.NewReader, iccProfileFormat)
				default:
					d.FieldRawLen("data", dataLen)
				}
			case "pHYs":
				d.FieldU32("x_pixels_per_unit")
				d.FieldU32("y_pixels_per_unit")
				d.FieldU8("unit")
			case "bKGD":
				switch colorType {
				case colorTypePalette:
					d.FieldU8("index")
				case colorTypeGrayscale, colorTypeGrayscaleWithAlpha:
					d.FieldU16("gray")
				case colorTypeRGB, colorTypeRGBA:
					d.FieldU16("r")
					d.FieldU16("g")
					d.FieldU16("b")
				}
			case "gAMA":
				d.FieldU32("value")
			case "cHRM":
				df := func(d *decode.D) float64 { return float64(d.U32()) / 1000.0 }
				d.FieldFltFn("white_point_x", df)
				d.FieldFltFn("white_point_y", df)
				d.FieldFltFn("red_x", df)
				d.FieldFltFn("red_y", df)
				d.FieldFltFn("green_x", df)
				d.FieldFltFn("green_y", df)
				d.FieldFltFn("blue_x", df)
				d.FieldFltFn("blue_y", df)
			case "eXIf":
				d.FieldFormatLen("exif", d.BitsLeft(), exifFormat, nil)
			case "acTL":
				d.FieldU32("num_frames")
				d.FieldU32("num_plays")
			case "fcTL":
				d.FieldU32("sequence_number")
				d.FieldU32("width")
				d.FieldU32("height")
				d.FieldU32("x_offset")
				d.FieldU32("y_offset")
				d.FieldU16("delay_num")
				d.FieldU16("delay_sep")
				d.FieldU8("dispose_op", disposeOpNames)
				d.FieldU8("blend_op", blendOpNames)
			case "fdAT":
				d.FieldU32("sequence_number")
				d.FieldRawLen("data", d.BitsLeft()-32)
			case "PLTE":
				d.FieldArray("palette", func(d *decode.D) {
					for !d.End() {
						d.FieldStruct("color", func(d *decode.D) {
							d.FieldU8("r")
							d.FieldU8("g")
							d.FieldU8("b")
						})
					}
				})
			case "tRNS":
				switch colorType {
				case colorTypeGrayscale:
					d.FieldU16("alpha")
				case colorTypeRGB:
					d.FieldU16("r")
					d.FieldU16("g")
					d.FieldU16("b")
				case colorTypePalette:
					d.FieldArray("alphas", func(d *decode.D) {
						for !d.End() {
							d.FieldU8("alpha")
						}
					})
				}
			default:
				if chunkType == "IEND" {
					iEndFound = true
				} else {
					d.FieldRawLen("data", d.BitsLeft())
				}
			}
		})

		chunkCRC := crc32.NewIEEE()
		d.Copy(chunkCRC, bitio.NewIOReader(d.BitBufRange(crcStartPos, d.Pos()-crcStartPos)))
		d.FieldU32("crc", d.UintValidateBytes(chunkCRC.Sum(nil)), scalar.UintHex)
	})

	return nil
}
