// Package x264 implements H264 encoder.
// This package requires libx264 headers and libraries to be built.
// Reference: https://code.videolan.org/videolan/x264/blob/master/example.c
package x264

// #cgo pkg-config: x264
// #include "bridge.h"
import "C"
import (
	"fmt"
	"image"
	"io"
	"sync"
	"unsafe"

	"github.com/pion/mediadevices/pkg/codec"
	"github.com/pion/mediadevices/pkg/io/video"
	"github.com/pion/mediadevices/pkg/prop"
)

type encoder struct {
	engine *C.Encoder
	r      video.Reader
	mu     sync.Mutex
	closed bool
}

type cerror int

func (e cerror) Error() string {
	switch e {
	case C.ERR_DEFAULT_PRESET:
		return errDefaultPreset.Error()
	case C.ERR_APPLY_PROFILE:
		return errApplyProfile.Error()
	case C.ERR_ALLOC_PICTURE:
		return errAllocPicture.Error()
	case C.ERR_OPEN_ENGINE:
		return errOpenEngine.Error()
	case C.ERR_ENCODE:
		return errEncode.Error()
	case C.ERR_BITRATE_RECONFIG:
		return errSetBitrate.Error()
	default:
		return "unknown error"
	}
}

func errFromC(rc C.int) error {
	if rc == 0 {
		return nil
	}
	return cerror(rc)
}

var (
	errInitEngine    = fmt.Errorf("failed to initialize x264")
	errDefaultPreset = fmt.Errorf("failed to set default preset")
	errApplyProfile  = fmt.Errorf("failed to apply profile")
	errAllocPicture  = fmt.Errorf("failed to alloc picture")
	errOpenEngine    = fmt.Errorf("failed to open x264")
	errEncode        = fmt.Errorf("failed to encode")
	errSetBitrate    = fmt.Errorf("failed to change x264 encoder bitrate")
)

func newEncoder(r video.Reader, p prop.Media, params Params) (codec.ReadCloser, error) {
	if params.KeyFrameInterval == 0 {
		params.KeyFrameInterval = 60
	}

	// Convert from bit/s to kbit/s because x264 uses kbit/s instead.
	// Reference: https://code.videolan.org/videolan/x264/-/blob/7923c5818b50a3d8816eed222a7c43b418a73b36/encoder/ratecontrol.c#L657
	params.BitRate /= 1000

	// Map Go LogLevel to x264 log level constants.
	// x264 uses: X264_LOG_NONE(-1), X264_LOG_ERROR(0), X264_LOG_WARNING(1),
	// X264_LOG_INFO(2), X264_LOG_DEBUG(3).
	x264LogLevel := C.int(C.X264_LOG_INFO)
	switch params.LogLevel {
	case LogNone:
		x264LogLevel = C.X264_LOG_NONE
	case LogError:
		x264LogLevel = C.X264_LOG_ERROR
	case LogWarning:
		x264LogLevel = C.X264_LOG_WARNING
	case LogInfo:
		x264LogLevel = C.X264_LOG_INFO
	case LogDebug:
		x264LogLevel = C.X264_LOG_DEBUG
	}

	param := C.x264_param_t{
		i_csp:        C.X264_CSP_I420,
		i_width:      C.int(p.Width),
		i_height:     C.int(p.Height),
		i_keyint_max: C.int(params.KeyFrameInterval),
		i_log_level:  x264LogLevel,
	}
	param.rc.i_bitrate = C.int(params.BitRate)
	param.rc.i_vbv_max_bitrate = param.rc.i_bitrate
	param.rc.i_vbv_buffer_size = param.rc.i_vbv_max_bitrate * 2

	var rc C.int
	// cPreset will be freed in C.enc_new
	cPreset := C.CString(fmt.Sprint(params.Preset))
	engine := C.enc_new(param, cPreset, &rc)
	if err := errFromC(rc); err != nil {
		return nil, err
	}

	e := encoder{
		engine: engine,
		r:      video.ToI420(r),
	}
	return &e, nil
}

func (e *encoder) Read() ([]byte, func(), error) {
	e.mu.Lock()
	defer e.mu.Unlock()

	if e.closed {
		return nil, func() {}, io.EOF
	}

	img, release, err := e.r.Read()
	if err != nil {
		return nil, func() {}, err
	}
	defer release()
	yuvImg := img.(*image.YCbCr)

	var rc C.int
	s := C.enc_encode(
		e.engine,
		(*C.uchar)(&yuvImg.Y[0]),
		(*C.uchar)(&yuvImg.Cb[0]),
		(*C.uchar)(&yuvImg.Cr[0]),
		&rc,
	)
	if err := errFromC(rc); err != nil {
		return nil, func() {}, err
	}

	encoded := C.GoBytes(unsafe.Pointer(s.data), s.data_len)
	return encoded, func() {}, err
}

// TODO: Implement bit rate control
//var _ codec.BitRateController = (*encoder)(nil)

func (e *encoder) ForceKeyFrame() error {
	e.engine.force_key_frame = C.int(1)
	return nil
}

func (e *encoder) SetBitRate(bitrate int) error {
	e.mu.Lock()
	defer e.mu.Unlock()
	errNum := C.apply_target_bitrate(e.engine, C.int(bitrate))
	if err := errFromC(errNum); err != nil {
		return err
	}
	return nil
}

func (e *encoder) Controller() codec.EncoderController {
	return e
}

func (e *encoder) Close() error {
	e.mu.Lock()
	defer e.mu.Unlock()

	if e.closed {
		return nil
	}

	var rc C.int
	C.enc_close(e.engine, &rc)
	e.closed = true
	return nil
}
