package hls

import (
	"sync"
	"time"

	"github.com/bluenviron/gohlslib/v2"
	"github.com/bluenviron/gohlslib/v2/pkg/codecs"
	"github.com/bluenviron/gortsplib/v5/pkg/description"
	"github.com/bluenviron/gortsplib/v5/pkg/format"
	"github.com/bluenviron/mediamtx/internal/logger"
	"github.com/bluenviron/mediamtx/internal/stream"
	"github.com/bluenviron/mediamtx/internal/unit"
)

type ntpState int

const (
	ntpStateInitial ntpState = iota
	ntpStateUnavailable
	ntpStateAvailable
	ntpStateDegraded
)

func multiplyAndDivide(v, m, d int64) int64 {
	secs := v / d
	dec := v % d
	return (secs*m + dec*m/d)
}

// ToStream maps a HLS stream to a MediaMTX stream.
func ToStream(
	c *gohlslib.Client,
	tracks []*gohlslib.Track,
	stream **stream.Stream,
) ([]*description.Media, error) {
	var ntpStat ntpState
	var ntpStatMutex sync.Mutex

	handleNTP := func(track *gohlslib.Track) time.Time {
		ntpStatMutex.Lock()
		defer ntpStatMutex.Unlock()

		switch ntpStat {
		case ntpStateInitial:
			ntp, avail := c.AbsoluteTime(track)
			if !avail {
				ntpStat = ntpStateUnavailable
				return time.Now()
			}

			ntpStat = ntpStateAvailable
			return ntp

		case ntpStateAvailable:
			ntp, avail := c.AbsoluteTime(track)
			if !avail {
				panic("should not happen")
			}

			return ntp

		case ntpStateUnavailable:
			_, avail := c.AbsoluteTime(track)
			if avail {
				(*stream).Parent.Log(logger.Warn, "absolute timestamp appeared after stream started, we are not using it")
				ntpStat = ntpStateDegraded
			}

			return time.Now()

		default: // ntpStateDegraded
			return time.Now()
		}
	}

	var medias []*description.Media //nolint:prealloc

	for _, track := range tracks {
		var medi *description.Media
		clockRate := track.ClockRate

		switch tcodec := track.Codec.(type) {
		case *codecs.AV1:
			medi = &description.Media{
				Type: description.MediaTypeVideo,
				Formats: []format.Format{&format.AV1{
					PayloadTyp: 96,
				}},
			}
			newClockRate := medi.Formats[0].ClockRate()

			c.OnDataAV1(track, func(pts int64, tu [][]byte) {
				(*stream).WriteUnit(medi, medi.Formats[0], &unit.AV1{
					Base: unit.Base{
						NTP: handleNTP(track),
						PTS: multiplyAndDivide(pts, int64(newClockRate), int64(clockRate)),
					},
					TU: tu,
				})
			})

		case *codecs.VP9:
			medi = &description.Media{
				Type: description.MediaTypeVideo,
				Formats: []format.Format{&format.VP9{
					PayloadTyp: 96,
				}},
			}
			newClockRate := medi.Formats[0].ClockRate()

			c.OnDataVP9(track, func(pts int64, frame []byte) {
				(*stream).WriteUnit(medi, medi.Formats[0], &unit.VP9{
					Base: unit.Base{
						NTP: handleNTP(track),
						PTS: multiplyAndDivide(pts, int64(newClockRate), int64(clockRate)),
					},
					Frame: frame,
				})
			})

		case *codecs.H265:
			medi = &description.Media{
				Type: description.MediaTypeVideo,
				Formats: []format.Format{&format.H265{
					PayloadTyp: 96,
					VPS:        tcodec.VPS,
					SPS:        tcodec.SPS,
					PPS:        tcodec.PPS,
				}},
			}
			newClockRate := medi.Formats[0].ClockRate()

			c.OnDataH26x(track, func(pts int64, _ int64, au [][]byte) {
				(*stream).WriteUnit(medi, medi.Formats[0], &unit.H265{
					Base: unit.Base{
						NTP: handleNTP(track),
						PTS: multiplyAndDivide(pts, int64(newClockRate), int64(clockRate)),
					},
					AU: au,
				})
			})

		case *codecs.H264:
			medi = &description.Media{
				Type: description.MediaTypeVideo,
				Formats: []format.Format{&format.H264{
					PayloadTyp:        96,
					PacketizationMode: 1,
					SPS:               tcodec.SPS,
					PPS:               tcodec.PPS,
				}},
			}
			newClockRate := medi.Formats[0].ClockRate()

			c.OnDataH26x(track, func(pts int64, _ int64, au [][]byte) {
				(*stream).WriteUnit(medi, medi.Formats[0], &unit.H264{
					Base: unit.Base{
						NTP: handleNTP(track),
						PTS: multiplyAndDivide(pts, int64(newClockRate), int64(clockRate)),
					},
					AU: au,
				})
			})

		case *codecs.Opus:
			medi = &description.Media{
				Type: description.MediaTypeAudio,
				Formats: []format.Format{&format.Opus{
					PayloadTyp:   96,
					ChannelCount: tcodec.ChannelCount,
				}},
			}
			newClockRate := medi.Formats[0].ClockRate()

			c.OnDataOpus(track, func(pts int64, packets [][]byte) {
				(*stream).WriteUnit(medi, medi.Formats[0], &unit.Opus{
					Base: unit.Base{
						NTP: handleNTP(track),
						PTS: multiplyAndDivide(pts, int64(newClockRate), int64(clockRate)),
					},
					Packets: packets,
				})
			})

		case *codecs.MPEG4Audio:
			medi = &description.Media{
				Type: description.MediaTypeAudio,
				Formats: []format.Format{&format.MPEG4Audio{
					PayloadTyp:       96,
					SizeLength:       13,
					IndexLength:      3,
					IndexDeltaLength: 3,
					Config:           &tcodec.Config,
				}},
			}
			newClockRate := medi.Formats[0].ClockRate()

			c.OnDataMPEG4Audio(track, func(pts int64, aus [][]byte) {
				(*stream).WriteUnit(medi, medi.Formats[0], &unit.MPEG4Audio{
					Base: unit.Base{
						NTP: handleNTP(track),
						PTS: multiplyAndDivide(pts, int64(newClockRate), int64(clockRate)),
					},
					AUs: aus,
				})
			})

		default:
			panic("should not happen")
		}

		medias = append(medias, medi)
	}

	if len(medias) == 0 {
		return nil, ErrNoSupportedCodecs
	}

	return medias, nil
}
