// SPDX-License-Identifier: Unlicense OR BSD-3-Clause

package opentype

import (
	
	
	
	
	
	
)

var (
	// TrueType is the first four bytes of an OpenType file containing a TrueType font
	TrueType = Tag(0x00010000)
	// AppleTrueType is the first four bytes of an OpenType file containing a TrueType font
	// (specifically one designed for Apple products, it's recommended to use TrueType instead)
	AppleTrueType = MustNewTag("true")
	// PostScript1 is the first four bytes of an OpenType file containing a PostScript 1 font
	PostScript1 = MustNewTag("typ1")
	// OpenType is the first four bytes of an OpenType file containing a PostScript Type 2 font
	// as specified by OpenType
	OpenType = MustNewTag("OTTO")

	// signatureWOFF is the magic number at the start of a WOFF file.
	signatureWOFF = MustNewTag("wOFF")

	ttcTag = MustNewTag("ttcf")

	errInvalidDfont = errors.New("invalid dfont")
)

// dfontResourceDataOffset is the assumed value of a dfont file's resource data
// offset.
//
// https://github.com/kreativekorp/ksfl/wiki/Macintosh-Resource-File-Format
// says that "A Mac OS resource file... [starts with an] offset from start of
// file to start of resource data section... [usually] 0x0100". In theory,
// 0x00000100 isn't always a magic number for identifying dfont files. In
// practice, it seems to work.
const dfontResourceDataOffset = 0x00000100

type Resource interface {
	Read([]byte) (int, error)
	ReadAt([]byte, int64) (int, error)
	Seek(int64, int) (int64, error)
}

// tableSection represents a table within the font file.
type tableSection struct {
	offset  uint32 // Offset into the file this table starts.
	length  uint32 // Length of this table within the file.
	zLength uint32 // Uncompressed length of this table.
}

// Loader is the low level font reader, providing
// full control over table loading.
type Loader struct {
	file   Resource             // source, needed to parse each table
	tables map[Tag]tableSection // header only, contents is processed on demand

	// Type represents the kind of this font being loaded.
	// It is one of TrueType, TrueTypeApple, PostScript1, OpenType
	Type Tag
}

// NewLoader reads the `file` header and returns
// a new lazy ot.
// `file` will be used to parse tables, and should not be close.
func ( Resource) (*Loader, error) {
	return parseOneFont(, 0, false)
}

// NewLoaders is the same as `NewLoader`, but supports collections.
func ( Resource) ([]*Loader, error) {
	,  := .Seek(0, io.SeekStart) // file might have been used before
	if  != nil {
		return nil, 
	}

	var  [4]byte
	_,  = .Read([:])
	if  != nil {
		return nil, 
	}
	 := NewTag([0], [1], [2], [3])

	.Seek(0, io.SeekStart)

	var (
		             *Loader
		        []uint32
		 bool
	)
	switch  {
	case signatureWOFF, TrueType, OpenType, PostScript1, AppleTrueType:
		,  = parseOneFont(, 0, false)
	case ttcTag:
		,  = parseTTCHeader()
	case dfontResourceDataOffset:
		,  = parseDfont()
		 = true
	default:
		return nil, fmt.Errorf("unsupported font format %v", )
	}
	if  != nil {
		return nil, 
	}

	// only one font
	if  != nil {
		return []*Loader{}, nil
	}

	// collection
	 := make([]*Loader, len())
	for ,  := range  {
		[],  = parseOneFont(, , )
		if  != nil {
			return nil, 
		}
	}
	return , nil
}

// dst is an optional storage which may be provided to reduce allocations.
func ( *Loader) ( tableSection,  []byte) ([]byte, error) {
	if .length != 0 && .length < .zLength {
		 := io.NewSectionReader(.file, int64(.offset), int64(.length))
		,  := zlib.NewReader()
		if  != nil {
			return nil, 
		}
		defer .Close()

		if cap() < int(.zLength) {
			 = make([]byte, .zLength)
		}
		 = [0:.zLength]
		if ,  := io.ReadFull(, );  != nil {
			return nil, 
		}
	} else {
		if cap() < int(.length) {
			 = make([]byte, .length)
		}
		 = [0:.length]
		if ,  := .file.ReadAt(, int64(.offset));  != nil {
			return nil, 
		}
	}
	return , nil
}

// HasTable returns true if [table] is present.
func ( *Loader) ( Tag) bool {
	,  := .tables[]
	return 
}

// Tables returns all the tables found in the file,
// as a sorted slice.
func ( *Loader) () []Tag {
	 := make([]Tag, 0, len(.tables))
	for  := range .tables {
		 = append(, )
	}
	sort.Slice(, func(,  int) bool { return [] < [] })
	return 
}

// RawTable returns the binary content of the given table,
// or an error if not found.
func ( *Loader) ( Tag) ([]byte, error) {
	return .RawTableTo(, nil)
}

// RawTable writes the binary content of the given table to [dst], returning it,
// or an error if not found.
func ( *Loader) ( Tag,  []byte) ([]byte, error) {
	,  := .tables[]
	if ! {
		return nil, fmt.Errorf("missing table %s", )
	}

	return .findTableBuffer(, )
}

func parseOneFont( Resource,  uint32,  bool) ( *Loader,  error) {
	_,  = .Seek(int64(), io.SeekStart)
	if  != nil {
		return nil, fmt.Errorf("invalid offset: %s", )
	}

	var  [4]byte
	_,  = .Read([:])
	if  != nil {
		return nil, 
	}
	 := NewTag([0], [1], [2], [3])

	switch  {
	case signatureWOFF:
		,  = parseWOFF(, , )
	case TrueType, OpenType, PostScript1, AppleTrueType:
		,  = parseOTF(, , )
	case ttcTag, dfontResourceDataOffset: // no more collections allowed here
		return nil, errors.New("collections not allowed")
	default:
		return nil, fmt.Errorf("unknown font format tag %v", )
	}

	if  != nil {
		return nil, 
	}

	return , nil
}

// support for collections

const maxNumFonts = 2048 // security implementation limit

// returns the offsets of each font
func parseTTCHeader( io.Reader) ([]uint32, error) {
	// The https://www.microsoft.com/typography/otspec/otff.htm "Font
	// Collections" section describes the TTC header.
	var  [12]byte
	if ,  := .Read([:]);  != nil {
		return nil, 
	}
	// skip versions
	 := binary.BigEndian.Uint32([8:])
	if  == 0 {
		return nil, errors.New("empty font collection")
	}
	if  > maxNumFonts {
		return nil, fmt.Errorf("number of fonts (%d) in collection exceed implementation limit (%d)",
			, maxNumFonts)
	}

	 := make([]byte, *4)
	,  := io.ReadFull(, )
	if  != nil {
		return nil, 
	}
	return parseUint32s(, int()), nil
}

// parseDfont parses a dfont resource map, as per
// https://github.com/kreativekorp/ksfl/wiki/Macintosh-Resource-File-Format
//
// That unofficial wiki page lists all of its fields as *signed* integers,
// which looks unusual. The actual file format might use *unsigned* integers in
// various places, but until we have either an official specification or an
// actual dfont file where this matters, we'll use signed integers and treat
// negative values as invalid.
func parseDfont( Resource) ([]uint32, error) {
	var  [16]byte
	if ,  := .Read([:]);  != nil {
		return nil, 
	}
	 := binary.BigEndian.Uint32([4:])
	 := binary.BigEndian.Uint32([12:])

	const (
		// (maxTableOffset + maxTableLength) will not overflow an int32.
		 = 1 << 29
		 = 1 << 29
	)
	if  >  ||  >  {
		return nil, errors.New("unsupported table offset or length")
	}

	const  = 28
	if  <  {
		return nil, errInvalidDfont
	}
	,  := .ReadAt([:2], int64(+24))
	if  != nil {
		return nil, 
	}
	 := int64(int16(binary.BigEndian.Uint16([:])))
	if  <  ||  < uint32()+2 {
		return nil, errInvalidDfont
	}
	_,  = .ReadAt([:2], int64()+)
	if  != nil {
		return nil, 
	}
	 := int(binary.BigEndian.Uint16([:])) // The number of types, minus one.
	if  == 0xFFFF {
		return nil, errInvalidDfont
	}
	 += 1

	const  = 8
	if *uint32() > -uint32()-2 {
		return nil, errInvalidDfont
	}

	 := make([]byte, *)
	_,  = .ReadAt(, int64()++2)
	if  != nil {
		return nil, 
	}
	,  := 0, 0
	for  := 0;  < ; ++ {
		if binary.BigEndian.Uint32([*:]) != 0x73666e74 { // "sfnt".
			continue
		}

		 = int(int16(binary.BigEndian.Uint16([*+4:])))
		if  < 0 {
			return nil, errInvalidDfont
		}
		// https://github.com/kreativekorp/ksfl/wiki/Macintosh-Resource-File-Format
		// says that the value in the wire format is "the number of
		// resources of this type, minus one."
		++

		 = int(int16(binary.BigEndian.Uint16(([*+6:]))))
		if  < 0 {
			return nil, errInvalidDfont
		}
	}
	if  == 0 {
		return nil, errInvalidDfont
	}
	if  > maxNumFonts {
		return nil, fmt.Errorf("number of fonts (%d) in collection exceed implementation limit (%d)",
			, maxNumFonts)
	}

	const  = 12
	,  := uint32(int()+), *uint32()
	if  >  ||  > - {
		return nil, errInvalidDfont
	}

	 := make([]byte, )
	_,  = .ReadAt(, int64(+))
	if  != nil {
		return nil, 
	}

	 := make([]uint32, )
	for  := range  {
		 := 0xffffff & binary.BigEndian.Uint32([*+4:])
		// Offsets are relative to the resource data start, not the file start.
		// A particular resource's data also starts with a 4-byte length, which
		// we skip.
		 += dfontResourceDataOffset + 4
		if  >  {
			return nil, errors.New("unsupported table offset or length")
		}
		[] = 
	}

	return , nil
}

// data length must have been checked
func parseUint32s( []byte,  int) []uint32 {
	 := make([]uint32, )
	for  := range  {
		[] = binary.BigEndian.Uint32([4*:])
	}
	return 
}