package ingest

import (
	"encoding/binary"
	"fmt"
)

type liveChunk struct {
	FrameType byte
	Payload   []byte
}

type fmp4Assembler struct {
	pending []byte

	initReady bool
	initBuf   []byte

	mediaPrefix []byte
	inFragment  bool
	fragmentBuf []byte
}

func newFMP4Assembler() *fmp4Assembler {
	return &fmp4Assembler{}
}

func (a *fmp4Assembler) Push(data []byte) ([]liveChunk, error) {
	if len(data) == 0 {
		return nil, nil
	}
	a.pending = append(a.pending, data...)

	result := make([]liveChunk, 0, 2)
	for {
		box, boxType, ok, err := a.nextBox()
		if err != nil {
			return nil, err
		}
		if !ok {
			break
		}
		chunks, err := a.consumeBox(boxType, box)
		if err != nil {
			return nil, err
		}
		result = append(result, chunks...)
	}
	return result, nil
}

func (a *fmp4Assembler) nextBox() ([]byte, string, bool, error) {
	if len(a.pending) < 8 {
		return nil, "", false, nil
	}

	headerLen := 8
	size := uint64(binary.BigEndian.Uint32(a.pending[:4]))
	if size == 1 {
		if len(a.pending) < 16 {
			return nil, "", false, nil
		}
		headerLen = 16
		size = binary.BigEndian.Uint64(a.pending[8:16])
	}
	if size == 0 {
		// 0 means box runs to stream end; wait for more data.
		return nil, "", false, nil
	}
	if size < uint64(headerLen) {
		return nil, "", false, fmt.Errorf("invalid MP4 box size %d", size)
	}
	if size > uint64(len(a.pending)) {
		return nil, "", false, nil
	}

	boxLen := int(size)
	boxType := string(a.pending[4:8])
	box := make([]byte, boxLen)
	copy(box, a.pending[:boxLen])
	a.pending = a.pending[boxLen:]
	return box, boxType, true, nil
}

func (a *fmp4Assembler) consumeBox(boxType string, box []byte) ([]liveChunk, error) {
	if !a.initReady {
		a.initBuf = append(a.initBuf, box...)
		if boxType != "moov" {
			return nil, nil
		}

		a.initReady = true
		initPayload := cloneBytes(a.initBuf)
		a.initBuf = nil
		return []liveChunk{{FrameType: liveFrameTypeInit, Payload: initPayload}}, nil
	}

	switch boxType {
	case "styp", "sidx", "prft":
		if a.inFragment {
			a.fragmentBuf = append(a.fragmentBuf, box...)
		} else {
			a.mediaPrefix = append(a.mediaPrefix, box...)
		}
		return nil, nil
	case "moof":
		a.fragmentBuf = a.fragmentBuf[:0]
		if len(a.mediaPrefix) > 0 {
			a.fragmentBuf = append(a.fragmentBuf, a.mediaPrefix...)
			a.mediaPrefix = a.mediaPrefix[:0]
		}
		a.fragmentBuf = append(a.fragmentBuf, box...)
		a.inFragment = true
		return nil, nil
	case "mdat":
		if !a.inFragment {
			return nil, nil
		}
		a.fragmentBuf = append(a.fragmentBuf, box...)
		mediaPayload := cloneBytes(a.fragmentBuf)
		a.fragmentBuf = a.fragmentBuf[:0]
		a.inFragment = false
		return []liveChunk{{FrameType: liveFrameTypeMedia, Payload: mediaPayload}}, nil
	default:
		if a.inFragment {
			a.fragmentBuf = append(a.fragmentBuf, box...)
		}
		return nil, nil
	}
}

func cloneBytes(data []byte) []byte {
	if len(data) == 0 {
		return nil
	}
	out := make([]byte, len(data))
	copy(out, data)
	return out
}
