// Copyright 2015 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package binres

import (
	"bytes"
	"compress/gzip"
	"errors"
	"fmt"
	"io"
	"strings"
	"unicode/utf16"
)

// NoEntry marks a key with no value in the table
const NoEntry = 0xFFFFFFFF

// TableRef uniquely identifies entries within a resource table.
type TableRef uint32

// Resolve returns the Entry of TableRef in the given table.
//
// A TableRef is structured as follows:
//
//	0xpptteeee
//	pp: package index
//	tt: type spec index in package
//	eeee: entry index in type spec
//
// The package and type spec values start at 1 for the first item,
// to help catch cases where they have not been supplied.
func (ref TableRef) Resolve(tbl *Table) (*Entry, error) {
	pkg := tbl.pkgs[uint8(ref>>24)-1]
	spec := pkg.specs[uint8(ref>>16)-1]
	idx := uint16(ref)

	for _, typ := range spec.types {
		if idx < uint16(len(typ.entries)) {
			nt := typ.entries[idx]
			if nt == nil {
				return nil, errors.New("nil entry match")
			}
			return nt, nil
		}
	}

	return nil, errors.New("failed to resolve table reference")
}

// Table is a container for packaged resources. Resource values within a package
// are obtained through pool while resource names and identifiers are obtained
// through each package's type and key pools respectively.
type Table struct {
	chunkHeader
	pool *Pool
	pkgs []*Package
}

// NewMipmapTable returns a resource table initialized for a single xxxhdpi mipmap resource
// and the path to write resource data to.
func NewMipmapTable(pkgname string) (*Table, string) {
	pkg := &Package{id: 127, name: pkgname, typePool: &Pool{}, keyPool: &Pool{}}

	attr := pkg.typePool.ref("attr")
	mipmap := pkg.typePool.ref("mipmap")
	icon := pkg.keyPool.ref("icon")

	nt := &Entry{values: []*Value{{data: &Data{Type: DataString}}}}
	typ := &Type{id: 2, indices: []uint32{0}, entries: []*Entry{nt}}
	typ.config.screenType.density = 640
	typ.config.version.sdk = 4

	pkg.specs = append(pkg.specs,
		&TypeSpec{
			id: uint8(attr) + 1, //1,
		},
		&TypeSpec{
			id:         uint8(mipmap) + 1, //2,
			entryCount: 1,
			entries:    []uint32{uint32(icon)}, // {0}
			types:      []*Type{typ},
		})

	pkg.lastPublicType = uint32(len(pkg.typePool.strings)) // 2
	pkg.lastPublicKey = uint32(len(pkg.keyPool.strings))   // 1

	name := "res/mipmap-xxxhdpi-v4/icon.png"
	tbl := &Table{pool: &Pool{}, pkgs: []*Package{pkg}}
	tbl.pool.ref(name)
	return tbl, name
}

// OpenSDKTable decodes resources.arsc from sdk platform jar.
func OpenSDKTable() (*Table, error) {
	bin, err := apiResources()
	if err != nil {
		return nil, err
	}
	tbl := new(Table)
	if err := tbl.UnmarshalBinary(bin); err != nil {
		return nil, err
	}
	return tbl, nil
}

// OpenTable decodes the prepacked resources.arsc for the supported sdk platform.
func OpenTable() (*Table, error) {
	zr, err := gzip.NewReader(bytes.NewReader(arsc))
	if err != nil {
		return nil, fmt.Errorf("gzip: %v", err)
	}
	defer zr.Close()

	var buf bytes.Buffer
	if _, err := io.Copy(&buf, zr); err != nil {
		return nil, fmt.Errorf("io: %v", err)
	}
	tbl := new(Table)
	if err := tbl.UnmarshalBinary(buf.Bytes()); err != nil {
		return nil, err
	}
	return tbl, nil
}

// SpecByName parses the spec name from an entry string if necessary and returns
// the Package and TypeSpec associated with that name along with their respective
// indices.
//
// For example:
//
//	tbl.SpecByName("@android:style/Theme.NoTitleBar")
//	tbl.SpecByName("style")
//
// Both locate the spec by name "style".
func (tbl *Table) SpecByName(name string) (int, *Package, int, *TypeSpec, error) {
	n := strings.TrimPrefix(name, "@android:")
	n = strings.Split(n, "/")[0]
	for pp, pkg := range tbl.pkgs {
		for tt, spec := range pkg.specs {
			if n == pkg.typePool.strings[spec.id-1] {
				return pp, pkg, tt, spec, nil
			}
		}
	}
	return 0, nil, 0, nil, fmt.Errorf("spec by name not found: %q", name)
}

// RefByName returns the TableRef by a given name. The ref may be used to resolve the
// associated Entry and is used for the generation of binary manifest files.
func (tbl *Table) RefByName(name string) (TableRef, error) {
	pp, pkg, tt, spec, err := tbl.SpecByName(name)
	if err != nil {
		return 0, err
	}

	q := strings.Split(name, "/")
	if len(q) != 2 {
		return 0, fmt.Errorf("invalid entry format, missing forward-slash: %q", name)
	}
	n := q[1]

	for _, t := range spec.types {
		for eeee, nt := range t.entries {
			if nt == nil { // NoEntry
				continue
			}
			if n == pkg.keyPool.strings[nt.key] {
				return TableRef(uint32(eeee) | uint32(tt+1)<<16 | uint32(pp+1)<<24), nil
			}
		}
	}
	return 0, fmt.Errorf("failed to find table ref by %q", name)
}

// UnmarshalBinary creates the table from binary data
func (tbl *Table) UnmarshalBinary(bin []byte) error {
	if err := (&tbl.chunkHeader).UnmarshalBinary(bin); err != nil {
		return err
	}
	if tbl.typ != ResTable {
		return fmt.Errorf("unexpected resource type %s, want %s", tbl.typ, ResTable)
	}

	npkgs := btou32(bin[8:])
	tbl.pkgs = make([]*Package, npkgs)

	buf := bin[tbl.headerByteSize:]

	tbl.pool = new(Pool)
	if err := tbl.pool.UnmarshalBinary(buf); err != nil {
		return err
	}
	buf = buf[tbl.pool.size():]
	for i := range tbl.pkgs {
		pkg := new(Package)
		if err := pkg.UnmarshalBinary(buf); err != nil {
			return err
		}
		tbl.pkgs[i] = pkg
		buf = buf[pkg.byteSize:]
	}

	return nil
}

// MarshalBinary outputs the binary format from a given table
func (tbl *Table) MarshalBinary() ([]byte, error) {
	bin := make([]byte, 12)
	putu16(bin, uint16(ResTable))
	putu16(bin[2:], 12)
	putu32(bin[8:], uint32(len(tbl.pkgs)))

	if tbl.pool.IsUTF8() {
		tbl.pool.flags ^= UTF8Flag
		defer func() {
			tbl.pool.flags |= UTF8Flag
		}()
	}

	b, err := tbl.pool.MarshalBinary()
	if err != nil {
		return nil, err
	}
	bin = append(bin, b...)

	for _, pkg := range tbl.pkgs {
		b, err = pkg.MarshalBinary()
		if err != nil {
			return nil, err
		}
		bin = append(bin, b...)
	}

	putu32(bin[4:], uint32(len(bin)))

	return bin, nil
}

// Package contains a collection of resource data types.
type Package struct {
	chunkHeader

	id   uint32
	name string

	lastPublicType uint32 // last index into typePool that is for public use
	lastPublicKey  uint32 // last index into keyPool that is for public use

	typePool *Pool // type names; e.g. theme
	keyPool  *Pool // resource names; e.g. Theme.NoTitleBar

	specs []*TypeSpec
}

// UnmarshalBinary creates a pakage from binary data
func (pkg *Package) UnmarshalBinary(bin []byte) error {
	if err := (&pkg.chunkHeader).UnmarshalBinary(bin); err != nil {
		return err
	}
	if pkg.typ != ResTablePackage {
		return errWrongType(pkg.typ, ResTablePackage)
	}

	pkg.id = btou32(bin[8:])

	var name []uint16
	for i := 0; i < 128; i++ {
		x := btou16(bin[12+i*2:])
		if x == 0 {
			break
		}
		name = append(name, x)
	}
	pkg.name = string(utf16.Decode(name))

	typeOffset := btou32(bin[268:]) // 0 if inheriting from another package
	pkg.lastPublicType = btou32(bin[272:])
	keyOffset := btou32(bin[276:]) // 0 if inheriting from another package
	pkg.lastPublicKey = btou32(bin[280:])

	var idOffset uint32 // value determined by either typePool or keyPool below

	if typeOffset != 0 {
		pkg.typePool = new(Pool)
		if err := pkg.typePool.UnmarshalBinary(bin[typeOffset:]); err != nil {
			return err
		}
		idOffset = typeOffset + pkg.typePool.byteSize
	}

	if keyOffset != 0 {
		pkg.keyPool = new(Pool)
		if err := pkg.keyPool.UnmarshalBinary(bin[keyOffset:]); err != nil {
			return err
		}
		idOffset = keyOffset + pkg.keyPool.byteSize
	}

	if idOffset == 0 {
		return nil
	}

	buf := bin[idOffset:]
	for len(buf) > 0 {
		t := ResType(btou16(buf))
		switch t {
		case ResTableTypeSpec:
			spec := new(TypeSpec)
			if err := spec.UnmarshalBinary(buf); err != nil {
				return err
			}
			pkg.specs = append(pkg.specs, spec)
			buf = buf[spec.byteSize:]
		case ResTableType:
			typ := new(Type)
			if err := typ.UnmarshalBinary(buf); err != nil {
				return err
			}
			last := pkg.specs[len(pkg.specs)-1]
			last.types = append(last.types, typ)
			buf = buf[typ.byteSize:]
		default:
			return errWrongType(t, ResTableTypeSpec, ResTableType)
		}
	}

	return nil
}

// MarshalBinary outputs the binary format from a given package
func (pkg *Package) MarshalBinary() ([]byte, error) {
	// Package header size is determined by C++ struct ResTable_package
	// see frameworks/base/include/ResourceTypes.h
	bin := make([]byte, 288)
	putu16(bin, uint16(ResTablePackage))
	putu16(bin[2:], 288)

	putu32(bin[8:], pkg.id)
	p := utf16.Encode([]rune(pkg.name))
	for i, x := range p {
		putu16(bin[12+i*2:], x)
	}

	if pkg.typePool != nil {
		if pkg.typePool.IsUTF8() {
			pkg.typePool.flags ^= UTF8Flag
			defer func() {
				pkg.typePool.flags |= UTF8Flag
			}()
		}

		b, err := pkg.typePool.MarshalBinary()
		if err != nil {
			return nil, err
		}
		putu32(bin[268:], uint32(len(bin)))
		putu32(bin[272:], pkg.lastPublicType)
		bin = append(bin, b...)
	}

	if pkg.keyPool != nil {
		if pkg.keyPool.IsUTF8() {
			pkg.keyPool.flags ^= UTF8Flag
			defer func() {
				pkg.keyPool.flags |= UTF8Flag
			}()
		}
		b, err := pkg.keyPool.MarshalBinary()
		if err != nil {
			return nil, err
		}
		putu32(bin[276:], uint32(len(bin)))
		putu32(bin[280:], pkg.lastPublicKey)
		bin = append(bin, b...)
	}

	for _, spec := range pkg.specs {
		b, err := spec.MarshalBinary()
		if err != nil {
			return nil, err
		}
		bin = append(bin, b...)
	}

	putu32(bin[4:], uint32(len(bin)))
	return bin, nil
}

// TypeSpec provides a specification for the resources defined by a particular type.
type TypeSpec struct {
	chunkHeader
	id         uint8  // id-1 is name index in Package.typePool
	res0       uint8  // must be 0
	res1       uint16 // must be 0
	entryCount uint32 // number of uint32 entry configuration masks that follow

	entries []uint32 // entry configuration masks
	types   []*Type
}

// UnmarshalBinary creates the type spec from binary data
func (spec *TypeSpec) UnmarshalBinary(bin []byte) error {
	if err := (&spec.chunkHeader).UnmarshalBinary(bin); err != nil {
		return err
	}
	if spec.typ != ResTableTypeSpec {
		return errWrongType(spec.typ, ResTableTypeSpec)
	}
	spec.id = bin[8]
	spec.res0 = bin[9]
	spec.res1 = btou16(bin[10:])
	spec.entryCount = btou32(bin[12:])

	spec.entries = make([]uint32, spec.entryCount)
	for i := range spec.entries {
		spec.entries[i] = btou32(bin[16+i*4:])
	}

	return nil
}

// MarshalBinary outputs the binary format from a given type spec
func (spec *TypeSpec) MarshalBinary() ([]byte, error) {
	bin := make([]byte, 16+len(spec.entries)*4)
	putu16(bin, uint16(ResTableTypeSpec))
	putu16(bin[2:], 16)
	putu32(bin[4:], uint32(len(bin)))

	bin[8] = spec.id
	// [9] = 0
	// [10:12] = 0
	putu32(bin[12:], uint32(len(spec.entries)))
	for i, x := range spec.entries {
		putu32(bin[16+i*4:], x)
	}

	for _, typ := range spec.types {
		b, err := typ.MarshalBinary()
		if err != nil {
			return nil, err
		}
		bin = append(bin, b...)
	}

	return bin, nil
}

// Type provides a collection of entries for a specific device configuration.
type Type struct {
	chunkHeader
	id           uint8
	res0         uint8  // must be 0
	res1         uint16 // must be 0
	entryCount   uint32 // number of uint32 entry configuration masks that follow
	entriesStart uint32 // offset from header where Entry data starts

	// configuration this collection of entries is designed for
	config struct {
		size uint32
		imsi struct {
			mcc uint16 // mobile country code
			mnc uint16 // mobile network code
		}
		locale struct {
			language uint16
			country  uint16
		}
		screenType struct {
			orientation uint8
			touchscreen uint8
			density     uint16
		}
		input struct {
			keyboard   uint8
			navigation uint8
			inputFlags uint8
			inputPad0  uint8
		}
		screenSize struct {
			width  uint16
			height uint16
		}
		version struct {
			sdk   uint16
			minor uint16 // always 0
		}
		screenConfig struct {
			layout          uint8
			uiMode          uint8
			smallestWidthDP uint16
		}
		screenSizeDP struct {
			width  uint16
			height uint16
		}
	}

	indices []uint32 // values that map to typePool
	entries []*Entry
}

// UnmarshalBinary creates the type from binary data
func (typ *Type) UnmarshalBinary(bin []byte) error {
	if err := (&typ.chunkHeader).UnmarshalBinary(bin); err != nil {
		return err
	}
	if typ.typ != ResTableType {
		return errWrongType(typ.typ, ResTableType)
	}

	typ.id = bin[8]
	typ.res0 = bin[9]
	typ.res1 = btou16(bin[10:])
	typ.entryCount = btou32(bin[12:])
	typ.entriesStart = btou32(bin[16:])

	if typ.res0 != 0 || typ.res1 != 0 {
		return fmt.Errorf("res0 res1 not zero")
	}

	typ.config.size = btou32(bin[20:])
	typ.config.imsi.mcc = btou16(bin[24:])
	typ.config.imsi.mnc = btou16(bin[26:])
	typ.config.locale.language = btou16(bin[28:])
	typ.config.locale.country = btou16(bin[30:])
	typ.config.screenType.orientation = bin[32]
	typ.config.screenType.touchscreen = bin[33]
	typ.config.screenType.density = btou16(bin[34:])
	typ.config.input.keyboard = bin[36]
	typ.config.input.navigation = bin[37]
	typ.config.input.inputFlags = bin[38]
	typ.config.input.inputPad0 = bin[39]
	typ.config.screenSize.width = btou16(bin[40:])
	typ.config.screenSize.height = btou16(bin[42:])
	typ.config.version.sdk = btou16(bin[44:])
	typ.config.version.minor = btou16(bin[46:])
	typ.config.screenConfig.layout = bin[48]
	typ.config.screenConfig.uiMode = bin[49]
	typ.config.screenConfig.smallestWidthDP = btou16(bin[50:])
	typ.config.screenSizeDP.width = btou16(bin[52:])
	typ.config.screenSizeDP.height = btou16(bin[54:])

	// fmt.Println("language/country:", u16tos(typ.config.locale.language), u16tos(typ.config.locale.country))

	buf := bin[typ.headerByteSize:typ.entriesStart]
	for len(buf) > 0 {
		typ.indices = append(typ.indices, btou32(buf))
		buf = buf[4:]
	}

	if len(typ.indices) != int(typ.entryCount) {
		return fmt.Errorf("indices len[%v] doesn't match entryCount[%v]", len(typ.indices), typ.entryCount)
	}
	typ.entries = make([]*Entry, typ.entryCount)

	for i, x := range typ.indices {
		if x == NoEntry {
			continue
		}
		nt := &Entry{}
		if err := nt.UnmarshalBinary(bin[typ.entriesStart+x:]); err != nil {
			return err
		}
		typ.entries[i] = nt
	}

	return nil
}

// MarshalBinary outputs the binary format from a given type
func (typ *Type) MarshalBinary() ([]byte, error) {
	bin := make([]byte, 56+len(typ.entries)*4)
	putu16(bin, uint16(ResTableType))
	putu16(bin[2:], 56)

	bin[8] = typ.id
	// [9] = 0
	// [10:12] = 0
	putu32(bin[12:], uint32(len(typ.entries)))
	putu32(bin[16:], uint32(56+len(typ.entries)*4))

	// assure typ.config.size is always written as 52; extended configuration beyond supported
	// API level is not supported by this marshal implementation but will be forward-compatible.
	putu32(bin[20:], 52)

	putu16(bin[24:], typ.config.imsi.mcc)
	putu16(bin[26:], typ.config.imsi.mnc)
	putu16(bin[28:], typ.config.locale.language)
	putu16(bin[30:], typ.config.locale.country)
	bin[32] = typ.config.screenType.orientation
	bin[33] = typ.config.screenType.touchscreen
	putu16(bin[34:], typ.config.screenType.density)
	bin[36] = typ.config.input.keyboard
	bin[37] = typ.config.input.navigation
	bin[38] = typ.config.input.inputFlags
	bin[39] = typ.config.input.inputPad0
	putu16(bin[40:], typ.config.screenSize.width)
	putu16(bin[42:], typ.config.screenSize.height)
	putu16(bin[44:], typ.config.version.sdk)
	putu16(bin[46:], typ.config.version.minor)
	bin[48] = typ.config.screenConfig.layout
	bin[49] = typ.config.screenConfig.uiMode
	putu16(bin[50:], typ.config.screenConfig.smallestWidthDP)
	putu16(bin[52:], typ.config.screenSizeDP.width)
	putu16(bin[54:], typ.config.screenSizeDP.height)

	var ntbin []byte
	for i, nt := range typ.entries {
		if nt == nil { // NoEntry
			putu32(bin[56+i*4:], NoEntry)
			continue
		}
		putu32(bin[56+i*4:], uint32(len(ntbin)))
		b, err := nt.MarshalBinary()
		if err != nil {
			return nil, err
		}
		ntbin = append(ntbin, b...)
	}
	bin = append(bin, ntbin...)

	putu32(bin[4:], uint32(len(bin)))
	return bin, nil
}

// Entry is a resource key typically followed by a value or resource map.
type Entry struct {
	size  uint16
	flags uint16
	key   PoolRef // ref into key pool

	// only filled if this is a map entry; when size is 16
	parent TableRef // id of parent mapping or zero if none
	count  uint32   // name and value pairs that follow for FlagComplex

	values []*Value
}

// UnmarshalBinary creates an entry from binary data
func (nt *Entry) UnmarshalBinary(bin []byte) error {
	nt.size = btou16(bin)
	nt.flags = btou16(bin[2:])
	nt.key = PoolRef(btou32(bin[4:]))

	if nt.size == 16 {
		nt.parent = TableRef(btou32(bin[8:]))
		nt.count = btou32(bin[12:])
		nt.values = make([]*Value, nt.count)
		for i := range nt.values {
			val := &Value{}
			if err := val.UnmarshalBinary(bin[16+i*12:]); err != nil {
				return err
			}
			nt.values[i] = val
		}
	} else {
		data := &Data{}
		if err := data.UnmarshalBinary(bin[8:]); err != nil {
			return err
		}
		// TODO boxing data not strictly correct as binary repr isn't of Value.
		nt.values = append(nt.values, &Value{0, data})
	}

	return nil
}

// MarshalBinary outputs the binary format from a given entry
func (nt *Entry) MarshalBinary() ([]byte, error) {
	bin := make([]byte, 8)
	sz := nt.size
	if sz == 0 {
		sz = 8
	}
	putu16(bin, sz)
	putu16(bin[2:], nt.flags)
	putu32(bin[4:], uint32(nt.key))

	if sz == 16 {
		bin = append(bin, make([]byte, 8+len(nt.values)*12)...)
		putu32(bin[8:], uint32(nt.parent))
		putu32(bin[12:], uint32(len(nt.values)))
		for i, val := range nt.values {
			b, err := val.MarshalBinary()
			if err != nil {
				return nil, err
			}
			copy(bin[16+i*12:], b)
		}
	} else {
		b, err := nt.values[0].data.MarshalBinary()
		if err != nil {
			return nil, err
		}
		bin = append(bin, b...)
	}

	return bin, nil
}

// Value is resource value associated with a key
type Value struct {
	name TableRef
	data *Data
}

// UnmarshalBinary creates the pool from binary data
func (val *Value) UnmarshalBinary(bin []byte) error {
	val.name = TableRef(btou32(bin))
	val.data = &Data{}
	return val.data.UnmarshalBinary(bin[4:])
}

// MarshalBinary outputs the binary format from a given value
func (val *Value) MarshalBinary() ([]byte, error) {
	bin := make([]byte, 12)
	putu32(bin, uint32(val.name))
	b, err := val.data.MarshalBinary()
	if err != nil {
		return nil, err
	}
	copy(bin[4:], b)
	return bin, nil
}

// DataType marks the type of a data item in a value
type DataType uint8

// explicitly defined for clarity and resolvability with apt source
const (
	DataNull             DataType = 0x00 // either 0 or 1 for resource undefined or empty
	DataReference        DataType = 0x01 // ResTable_ref, a reference to another resource table entry
	DataAttribute        DataType = 0x02 // attribute resource identifier
	DataString           DataType = 0x03 // index into the containing resource table's global value string pool
	DataFloat            DataType = 0x04 // single-precision floating point number
	DataDimension        DataType = 0x05 // complex number encoding a dimension value, such as "100in"
	DataFraction         DataType = 0x06 // complex number encoding a fraction of a container
	DataDynamicReference DataType = 0x07 // dynamic ResTable_ref, which needs to be resolved before it can be used like a TYPE_REFERENCE.
	DataIntDec           DataType = 0x10 // raw integer value of the form n..n
	DataIntHex           DataType = 0x11 // raw integer value of the form 0xn..n
	DataIntBool          DataType = 0x12 // either 0 or 1, for input "false" or "true"
	DataIntColorARGB8    DataType = 0x1c // raw integer value of the form #aarrggbb
	DataIntColorRGB8     DataType = 0x1d // raw integer value of the form #rrggbb
	DataIntColorARGB4    DataType = 0x1e // raw integer value of the form #argb
	DataIntColorRGB4     DataType = 0x1f // raw integer value of the form #rgb
)

// Data is the raw data stored in a value
type Data struct {
	ByteSize uint16
	Res0     uint8 // always 0, useful for debugging bad read offsets
	Type     DataType
	Value    uint32
}

// UnmarshalBinary creates the data item from binary data
func (d *Data) UnmarshalBinary(bin []byte) error {
	d.ByteSize = btou16(bin)
	d.Res0 = bin[2]
	d.Type = DataType(bin[3])
	d.Value = btou32(bin[4:])
	return nil
}

// MarshalBinary outputs the binary format from a given data item
func (d *Data) MarshalBinary() ([]byte, error) {
	bin := make([]byte, 8)
	putu16(bin, 8)
	bin[2] = d.Res0
	bin[3] = byte(d.Type)
	putu32(bin[4:], d.Value)
	return bin, nil
}
