package mp4

import (
	"errors"
	"fmt"
	"reflect"
	"strings"
)

var ErrBoxInfoNotFound = errors.New("box info not found")

// BoxType is mpeg box type
type BoxType [4]byte

func StrToBoxType(code string) BoxType {
	if len(code) != 4 {
		panic(fmt.Errorf("invalid box type id length: [%s]", code))
	}
	return BoxType{code[0], code[1], code[2], code[3]}
}

func (boxType BoxType) String() string {
	if isPrintable(boxType[0]) && isPrintable(boxType[1]) && isPrintable(boxType[2]) && isPrintable(boxType[3]) {
		s := string([]byte{boxType[0], boxType[1], boxType[2], boxType[3]})
		s = strings.ReplaceAll(s, string([]byte{0xa9}), "(c)")
		return s
	}
	return fmt.Sprintf("0x%02x%02x%02x%02x", boxType[0], boxType[1], boxType[2], boxType[3])
}

func isASCII(c byte) bool {
	return c >= 0x20 && c <= 0x7e
}

func isPrintable(c byte) bool {
	return isASCII(c) || c == 0xa9
}

func (lhs BoxType) MatchWith(rhs BoxType) bool {
	if lhs == boxTypeAny || rhs == boxTypeAny {
		return true
	}
	return lhs == rhs
}

var boxTypeAny = BoxType{0x00, 0x00, 0x00, 0x00}

func BoxTypeAny() BoxType {
	return boxTypeAny
}

type boxDef struct {
	dataType reflect.Type
	versions []uint8
	isTarget func(Context) bool
	fields   []*field
}

var boxMap = make(map[BoxType][]boxDef, 64)

func AddBoxDef(payload IBox, versions ...uint8) {
	boxMap[payload.GetType()] = append(boxMap[payload.GetType()], boxDef{
		dataType: reflect.TypeOf(payload).Elem(),
		versions: versions,
		fields:   buildFields(payload),
	})
}

func AddBoxDefEx(payload IBox, isTarget func(Context) bool, versions ...uint8) {
	boxMap[payload.GetType()] = append(boxMap[payload.GetType()], boxDef{
		dataType: reflect.TypeOf(payload).Elem(),
		versions: versions,
		isTarget: isTarget,
		fields:   buildFields(payload),
	})
}

func AddAnyTypeBoxDef(payload IAnyType, boxType BoxType, versions ...uint8) {
	boxMap[boxType] = append(boxMap[boxType], boxDef{
		dataType: reflect.TypeOf(payload).Elem(),
		versions: versions,
		fields:   buildFields(payload),
	})
}

func AddAnyTypeBoxDefEx(payload IAnyType, boxType BoxType, isTarget func(Context) bool, versions ...uint8) {
	boxMap[boxType] = append(boxMap[boxType], boxDef{
		dataType: reflect.TypeOf(payload).Elem(),
		versions: versions,
		isTarget: isTarget,
		fields:   buildFields(payload),
	})
}

func (boxType BoxType) getBoxDef(ctx Context) *boxDef {
	boxDefs := boxMap[boxType]
	for i := len(boxDefs) - 1; i >= 0; i-- {
		boxDef := &boxDefs[i]
		if boxDef.isTarget == nil || boxDef.isTarget(ctx) {
			return boxDef
		}
	}
	return nil
}

func (boxType BoxType) IsSupported(ctx Context) bool {
	return boxType.getBoxDef(ctx) != nil
}

func (boxType BoxType) New(ctx Context) (IBox, error) {
	boxDef := boxType.getBoxDef(ctx)
	if boxDef == nil {
		return nil, ErrBoxInfoNotFound
	}

	box, ok := reflect.New(boxDef.dataType).Interface().(IBox)
	if !ok {
		return nil, fmt.Errorf("box type not implements IBox interface: %s", boxType.String())
	}

	anyTypeBox, ok := box.(IAnyType)
	if ok {
		anyTypeBox.SetType(boxType)
	}

	return box, nil
}

func (boxType BoxType) GetSupportedVersions(ctx Context) ([]uint8, error) {
	boxDef := boxType.getBoxDef(ctx)
	if boxDef == nil {
		return nil, ErrBoxInfoNotFound
	}
	return boxDef.versions, nil
}

func (boxType BoxType) IsSupportedVersion(ver uint8, ctx Context) bool {
	boxDef := boxType.getBoxDef(ctx)
	if boxDef == nil {
		return false
	}
	if len(boxDef.versions) == 0 {
		return true
	}
	for _, sver := range boxDef.versions {
		if ver == sver {
			return true
		}
	}
	return false
}