package utils

import (
	"errors"
	"fmt"
	"io"
	"math"
	"strconv"
	"strings"

	"github.com/h2non/filetype"
)

const (
	// dlnaOrgFlagSenderPaced = 1 << 31
	// dlnaOrgFlagTimeBasedSeek = 1 << 30
	// dlnaOrgFlagByteBasedSeek = 1 << 29
	// dlnaOrgFlagPlayContainer = 1 << 28
	// dlnaOrgFlagS0Increase = 1 << 27
	// dlnaOrgFlagSnIncrease = 1 << 26
	// dlnaOrgFlagRtspPause = 1 << 25
	dlnaOrgFlagStreamingTransferMode = 1 << 24
	// dlnaOrgFlagInteractiveTransfertMode = 1 << 23
	dlnaOrgFlagBackgroundTransfertMode = 1 << 22
	dlnaOrgFlagConnectionStall         = 1 << 21
	dlnaOrgFlagDlnaV15                 = 1 << 20
)

var (
	dlnaprofiles = map[string]string{
		"video/x-mkv":             "DLNA.ORG_PN=MATROSKA",
		"video/x-matroska":        "DLNA.ORG_PN=MATROSKA",
		"video/x-msvideo":         "DLNA.ORG_PN=AVI",
		"video/mpeg":              "DLNA.ORG_PN=MPEG1",
		"video/vnd.dlna.mpeg-tts": "DLNA.ORG_PN=MPEG1",
		"video/mp4":               "DLNA.ORG_PN=AVC_MP4_MP_SD_AAC_MULT5",
		"video/quicktime":         "DLNA.ORG_PN=AVC_MP4_MP_SD_AAC_MULT5",
		"video/x-m4v":             "DLNA.ORG_PN=AVC_MP4_MP_SD_AAC_MULT5",
		"video/3gpp":              "DLNA.ORG_PN=AVC_MP4_MP_SD_AAC_MULT5",
		"video/x-flv":             "DLNA.ORG_PN=AVC_MP4_MP_SD_AAC_MULT5",
		"video/x-ms-wmv":          "DLNA.ORG_PN=WMVHIGH_FULL",
		"audio/mpeg":              "DLNA.ORG_PN=MP3",
		"image/jpeg":              "JPEG_LRG",
		"image/png":               "PNG_LRG",
	}

	ErrInvalidSeekFlag    = errors.New("invalid seek flag")
	ErrInvalidClockFormat = errors.New("invalid clock format")
)

func defaultStreamingFlags() string {
	return fmt.Sprintf("%.8x%.24x", dlnaOrgFlagStreamingTransferMode|
		dlnaOrgFlagBackgroundTransfertMode|
		dlnaOrgFlagConnectionStall|
		dlnaOrgFlagDlnaV15, 0)
}

// BuildContentFeatures builds the content features string
// for the "contentFeatures.dlna.org" header.
func BuildContentFeatures(mediaType string, seek string, transcode bool) (string, error) {
	var cf strings.Builder

	if mediaType != "" {
		dlnaProf, profExists := dlnaprofiles[mediaType]
		if profExists {
			cf.WriteString(dlnaProf + ";")
		}
	}

	// "00" neither time seek range nor range supported
	// "01" range supported
	// "10" time seek range supported
	// "11" both time seek range and range supported
	switch seek {
	case "00":
		cf.WriteString("DLNA.ORG_OP=00;")
	case "01":
		cf.WriteString("DLNA.ORG_OP=01;")
	case "10":
		cf.WriteString("DLNA.ORG_OP=10;")
	case "11":
		cf.WriteString("DLNA.ORG_OP=11;")
	default:
		return "", ErrInvalidSeekFlag
	}

	switch transcode {
	case true:
		cf.WriteString("DLNA.ORG_CI=1;")
	default:
		cf.WriteString("DLNA.ORG_CI=0;")
	}

	cf.WriteString("DLNA.ORG_FLAGS=")
	cf.WriteString(defaultStreamingFlags())

	return cf.String(), nil
}

// GetMimeDetailsFromFile returns the media file mime details.
func GetMimeDetailsFromFile(f io.ReadCloser) (string, error) {
	defer f.Close()
	head := make([]byte, 261)
	_, err := f.Read(head)
	if err != nil {
		return "", fmt.Errorf("getMimeDetailsFromFile error #2: %w", err)
	}

	kind, err := filetype.Match(head)
	if err != nil {
		return "", fmt.Errorf("getMimeDetailsFromFile error #3: %w", err)
	}

	return fmt.Sprintf("%s/%s", kind.MIME.Type, kind.MIME.Subtype), nil
}

// GetMimeDetailsFromStream returns the media URL mime details.
func GetMimeDetailsFromStream(s io.ReadCloser) (string, error) {
	defer s.Close()
	head := make([]byte, 261)
	_, err := s.Read(head)
	if err != nil {
		return "", fmt.Errorf("getMimeDetailsFromStream error: %w", err)
	}

	kind, err := filetype.Match(head)
	if err != nil {
		return "", fmt.Errorf("getMimeDetailsFromStream error  #2: %w", err)
	}

	return fmt.Sprintf("%s/%s", kind.MIME.Type, kind.MIME.Subtype), nil
}

// ClockTimeToSeconds converts relative time to seconds.
func ClockTimeToSeconds(strtime string) (int, error) {
	var out int
	v := make([]int, 0, 3)

	s := strings.Split(strtime, ":")
	if len(s) != 3 {
		return 0, ErrInvalidClockFormat
	}

	num, err := strconv.Atoi(s[0])
	if err != nil {
		return 0, ErrInvalidClockFormat
	}
	v = append(v, num)

	num, err = strconv.Atoi(s[1])
	if err != nil {
		return 0, ErrInvalidClockFormat
	}
	v = append(v, num)

	f, err := strconv.ParseFloat(s[2], 32)
	if err != nil {
		return 0, ErrInvalidClockFormat
	}
	f = math.Round(f)
	v = append(v, int(f))

	for n, i := range v {
		switch n {
		case 0:
			out += i * 3600
		case 1:
			out += i * 60
		case 2:
			out += i
		}
	}

	return out, nil
}

// SecondsToClockTime converts seconds to seconds relative time.
func SecondsToClockTime(secs int) (string, error) {
	hours := secs / 3600
	secs %= 3600
	minutes := secs / 60
	secs %= 60

	str := fmt.Sprintf("%02d:%02d:%02d", hours, minutes, secs)
	return str, nil
}

// FormatClockTime converts clock time to a more expected format of clock time.
func FormatClockTime(strtime string) (string, error) {
	sec, err := ClockTimeToSeconds(strtime)
	if err != nil {
		return "", ErrInvalidClockFormat
	}

	out, err := SecondsToClockTime(sec)
	if err != nil {
		return "", ErrInvalidClockFormat
	}

	return out, nil
}
