package core

import (
	"encoding/base64"
	"encoding/json"
	"fmt"
	"strings"
	"unicode"

	"github.com/pion/sdp/v3"
)

type Codec struct {
	Name        string // H264, PCMU, PCMA, opus...
	ClockRate   uint32 // 90000, 8000, 16000...
	Channels    uint8  // 0, 1, 2
	FmtpLine    string
	PayloadType uint8
}

// MarshalJSON - return FFprobe compatible output
func (c *Codec) MarshalJSON() ([]byte, error) {
	info := map[string]any{}
	if name := FFmpegCodecName(c.Name); name != "" {
		info["codec_name"] = name
		info["codec_type"] = c.Kind()
	}
	if c.Name == CodecH264 {
		profile, level := DecodeH264(c.FmtpLine)
		if profile != "" {
			info["profile"] = profile
			info["level"] = level
		}
	}
	if c.ClockRate != 0 && c.ClockRate != 90000 {
		info["sample_rate"] = c.ClockRate
	}
	if c.Channels > 0 {
		info["channels"] = c.Channels
	}
	return json.Marshal(info)
}

func FFmpegCodecName(name string) string {
	switch name {
	case CodecH264:
		return "h264"
	case CodecH265:
		return "hevc"
	case CodecJPEG:
		return "mjpeg"
	case CodecRAW:
		return "rawvideo"
	case CodecPCMA:
		return "pcm_alaw"
	case CodecPCMU:
		return "pcm_mulaw"
	case CodecPCM:
		return "pcm_s16be"
	case CodecPCML:
		return "pcm_s16le"
	case CodecAAC:
		return "aac"
	case CodecOpus:
		return "opus"
	case CodecVP8:
		return "vp8"
	case CodecVP9:
		return "vp9"
	case CodecAV1:
		return "av1"
	case CodecELD:
		return "aac/eld"
	case CodecFLAC:
		return "flac"
	case CodecMP3:
		return "mp3"
	}
	return name
}

func (c *Codec) String() (s string) {
	s = c.Name
	if c.ClockRate != 0 && c.ClockRate != 90000 {
		s += fmt.Sprintf("/%d", c.ClockRate)
	}
	if c.Channels > 0 {
		s += fmt.Sprintf("/%d", c.Channels)
	}
	return
}

func (c *Codec) IsRTP() bool {
	return c.PayloadType != PayloadTypeRAW
}

func (c *Codec) IsVideo() bool {
	return c.Kind() == KindVideo
}

func (c *Codec) IsAudio() bool {
	return c.Kind() == KindAudio
}

func (c *Codec) Kind() string {
	return GetKind(c.Name)
}

func (c *Codec) PrintName() string {
	switch c.Name {
	case CodecAAC:
		return "AAC"
	case CodecPCM:
		return "S16B"
	case CodecPCML:
		return "S16L"
	}
	return c.Name
}

func (c *Codec) Clone() *Codec {
	clone := *c
	return &clone
}

func (c *Codec) Match(remote *Codec) bool {
	switch remote.Name {
	case CodecAll, CodecAny:
		return true
	}

	return c.Name == remote.Name &&
		(c.ClockRate == remote.ClockRate || remote.ClockRate == 0) &&
		(c.Channels == remote.Channels || remote.Channels == 0)
}

func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
	c := &Codec{PayloadType: byte(Atoi(payloadType))}

	for _, attr := range md.Attributes {
		switch {
		case c.Name == "" && attr.Key == "rtpmap" && strings.HasPrefix(attr.Value, payloadType):
			i := strings.IndexByte(attr.Value, ' ')
			ss := strings.Split(attr.Value[i+1:], "/")

			c.Name = strings.ToUpper(ss[0])
			// fix tailing space: `a=rtpmap:96 H264/90000 `
			c.ClockRate = uint32(Atoi(strings.TrimRightFunc(ss[1], unicode.IsSpace)))

			if len(ss) == 3 && ss[2] == "2" {
				c.Channels = 2
			}
		case c.FmtpLine == "" && attr.Key == "fmtp" && strings.HasPrefix(attr.Value, payloadType):
			if i := strings.IndexByte(attr.Value, ' '); i > 0 {
				c.FmtpLine = attr.Value[i+1:]
			}
		}
	}

	switch c.Name {
	case "PCM":
		// https://www.reddit.com/r/Hikvision/comments/17elxex/comment/k642g2r/
		// check pkg/rtsp/rtsp_test.go TestHikvisionPCM
		c.Name = CodecPCML
	case "":
		// https://en.wikipedia.org/wiki/RTP_payload_formats
		switch payloadType {
		case "0":
			c.Name = CodecPCMU
			c.ClockRate = 8000
		case "8":
			c.Name = CodecPCMA
			c.ClockRate = 8000
		case "10":
			c.Name = CodecPCM
			c.ClockRate = 44100
			c.Channels = 2
		case "11":
			c.Name = CodecPCM
			c.ClockRate = 44100
		case "14":
			c.Name = CodecMP3
			c.ClockRate = 90000 // it's not real sample rate
		case "26":
			c.Name = CodecJPEG
			c.ClockRate = 90000
		case "96", "97", "98":
			if len(md.Bandwidth) == 0 {
				c.Name = payloadType
				break
			}

			// FFmpeg + RTSP + pcm_s16le = doesn't pass info about codec name and params
			// so try to guess the codec based on bitrate
			// https://github.com/AlexxIT/go2rtc/issues/523
			switch md.Bandwidth[0].Bandwidth {
			case 128:
				c.ClockRate = 8000
			case 256:
				c.ClockRate = 16000
			case 384:
				c.ClockRate = 24000
			case 512:
				c.ClockRate = 32000
			case 705:
				c.ClockRate = 44100
			case 768:
				c.ClockRate = 48000
			case 1411:
				// default Windows DShow
				c.ClockRate = 44100
				c.Channels = 2
			case 1536:
				// default Linux ALSA
				c.ClockRate = 48000
				c.Channels = 2
			default:
				c.Name = payloadType
				break
			}

			c.Name = CodecPCML
		default:
			c.Name = payloadType
		}
	}

	return c
}

func DecodeH264(fmtp string) (profile string, level byte) {
	if ps := Between(fmtp, "sprop-parameter-sets=", ","); ps != "" {
		if sps, _ := base64.StdEncoding.DecodeString(ps); len(sps) >= 4 {
			switch sps[1] {
			case 0x42:
				profile = "Baseline"
			case 0x4D:
				profile = "Main"
			case 0x58:
				profile = "Extended"
			case 0x64:
				profile = "High"
			default:
				profile = fmt.Sprintf("0x%02X", sps[1])
			}

			level = sps[3]
		}
	}
	return
}

func ParseCodecString(s string) *Codec {
	var codec Codec

	ss := strings.Split(s, "/")
	switch strings.ToLower(ss[0]) {
	case "pcm_s16be", "s16be", "pcm":
		codec.Name = CodecPCM
	case "pcm_s16le", "s16le", "pcml":
		codec.Name = CodecPCML
	case "pcm_alaw", "alaw", "pcma":
		codec.Name = CodecPCMA
	case "pcm_mulaw", "mulaw", "pcmu":
		codec.Name = CodecPCMU
	case "aac", "mpeg4-generic":
		codec.Name = CodecAAC
	case "opus":
		codec.Name = CodecOpus
	case "flac":
		codec.Name = CodecFLAC
	default:
		return nil
	}

	if len(ss) >= 2 {
		codec.ClockRate = uint32(Atoi(ss[1]))
	}
	if len(ss) >= 3 {
		codec.Channels = uint8(Atoi(ss[1]))
	}

	return &codec
}
