package mp4

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"io"
	"math"
)

type Context struct {
	// IsQuickTimeCompatible represents whether ftyp.compatible_brands contains "qt  ".
	IsQuickTimeCompatible bool

	// QuickTimeKeysMetaEntryCount the expected number of items under the ilst box as observed from the keys box
	QuickTimeKeysMetaEntryCount int

	// UnderWave represents whether current box is under the wave box.
	UnderWave bool

	// UnderIlst represents whether current box is under the ilst box.
	UnderIlst bool

	// UnderIlstMeta represents whether current box is under the metadata box under the ilst box.
	UnderIlstMeta bool

	// UnderIlstFreeMeta represents whether current box is under "----" box.
	UnderIlstFreeMeta bool

	// UnderUdta represents whether current box is under the udta box.
	UnderUdta bool
}

// BoxInfo has common infomations of box
type BoxInfo struct {
	// Offset specifies an offset of the box in a file.
	Offset uint64

	// Size specifies size(bytes) of box.
	Size uint64

	// HeaderSize specifies size(bytes) of common fields which are defined as "Box" class member at ISO/IEC 14496-12.
	HeaderSize uint64

	// Type specifies box type which is represented by 4 characters.
	Type BoxType

	// ExtendToEOF is set true when Box.size is zero. It means that end of box equals to end of file.
	ExtendToEOF bool

	// Context would be set by ReadBoxStructure, not ReadBoxInfo.
	Context
}

func (bi *BoxInfo) IsSupportedType() bool {
	return bi.Type.IsSupported(bi.Context)
}

const (
	SmallHeaderSize = 8
	LargeHeaderSize = 16
)

// WriteBoxInfo writes common fields which are defined as "Box" class member at ISO/IEC 14496-12.
// This function ignores bi.Offset and returns BoxInfo which contains real Offset and recalculated Size/HeaderSize.
func WriteBoxInfo(w io.WriteSeeker, bi *BoxInfo) (*BoxInfo, error) {
	offset, err := w.Seek(0, io.SeekCurrent)
	if err != nil {
		return nil, err
	}

	var data []byte
	if bi.ExtendToEOF {
		data = make([]byte, SmallHeaderSize)
	} else if bi.Size <= math.MaxUint32 && bi.HeaderSize != LargeHeaderSize {
		data = make([]byte, SmallHeaderSize)
		binary.BigEndian.PutUint32(data, uint32(bi.Size))
	} else {
		data = make([]byte, LargeHeaderSize)
		binary.BigEndian.PutUint32(data, 1)
		binary.BigEndian.PutUint64(data[SmallHeaderSize:], bi.Size)
	}
	data[4] = bi.Type[0]
	data[5] = bi.Type[1]
	data[6] = bi.Type[2]
	data[7] = bi.Type[3]

	if _, err := w.Write(data); err != nil {
		return nil, err
	}

	return &BoxInfo{
		Offset:      uint64(offset),
		Size:        bi.Size - bi.HeaderSize + uint64(len(data)),
		HeaderSize:  uint64(len(data)),
		Type:        bi.Type,
		ExtendToEOF: bi.ExtendToEOF,
	}, nil
}

// ReadBoxInfo reads common fields which are defined as "Box" class member at ISO/IEC 14496-12.
func ReadBoxInfo(r io.ReadSeeker) (*BoxInfo, error) {
	offset, err := r.Seek(0, io.SeekCurrent)
	if err != nil {
		return nil, err
	}

	bi := &BoxInfo{
		Offset: uint64(offset),
	}

	// read 8 bytes
	buf := bytes.NewBuffer(make([]byte, 0, SmallHeaderSize))
	if _, err := io.CopyN(buf, r, SmallHeaderSize); err != nil {
		return nil, err
	}
	bi.HeaderSize += SmallHeaderSize

	// pick size and type
	data := buf.Bytes()
	bi.Size = uint64(binary.BigEndian.Uint32(data))
	bi.Type = BoxType{data[4], data[5], data[6], data[7]}

	if bi.Size == 0 {
		// box extends to end of file
		offsetEOF, err := r.Seek(0, io.SeekEnd)
		if err != nil {
			return nil, err
		}
		bi.Size = uint64(offsetEOF) - bi.Offset
		bi.ExtendToEOF = true
		if _, err := bi.SeekToPayload(r); err != nil {
			return nil, err
		}
	} else if bi.Size == 1 {
		// read more 8 bytes
		buf.Reset()
		if _, err := io.CopyN(buf, r, LargeHeaderSize-SmallHeaderSize); err != nil {
			return nil, err
		}
		bi.HeaderSize += LargeHeaderSize - SmallHeaderSize
		bi.Size = binary.BigEndian.Uint64(buf.Bytes())
	}

	if bi.Size == 0 {
		return nil, fmt.Errorf("invalid size")
	}

	return bi, nil
}

func (bi *BoxInfo) SeekToStart(s io.Seeker) (int64, error) {
	return s.Seek(int64(bi.Offset), io.SeekStart)
}

func (bi *BoxInfo) SeekToPayload(s io.Seeker) (int64, error) {
	return s.Seek(int64(bi.Offset+bi.HeaderSize), io.SeekStart)
}

func (bi *BoxInfo) SeekToEnd(s io.Seeker) (int64, error) {
	return s.Seek(int64(bi.Offset+bi.Size), io.SeekStart)
}