package music

import (
	"crypto/sha1"
	"fmt"
	"path/filepath"
	"strings"
)

func GenerateStem(fileName, prefix string, used map[string]struct{}) (string, error) {
	return GenerateStemWithMax(fileName, prefix, MaxStemLen, used)
}

func GenerateStemWithMax(fileName, prefix string, maxStemLen int, used map[string]struct{}) (string, error) {
	if maxStemLen <= 0 {
		maxStemLen = MaxStemLen
	}
	maxCore := maxStemLen - len(prefix)
	if maxCore < 3 {
		return "", fmt.Errorf("prefix too long: %q", prefix)
	}
	base := strings.TrimSuffix(fileName, filepath.Ext(fileName))
	words := SlugWords(base)
	core := MakeMusicCore(words, maxCore)
	if core == "" {
		sum := sha1.Sum([]byte(fileName))
		core = fmt.Sprintf("%x", sum[:])[:maxCore]
	}
	stem := strings.TrimRight((prefix + core)[:Min(maxStemLen, len(prefix)+len(core))], "_")
	if stem == "" {
		return "", fmt.Errorf("could not derive music stem for %s", fileName)
	}
	return UniqueNameWithMax(stem, maxStemLen, used), nil
}

func SlugWords(value string) []string {
	value = BracketRE.ReplaceAllString(value, " ")
	value = strings.ReplaceAll(value, "&", " and ")
	value = strings.ReplaceAll(value, "'", "")
	value = strings.ReplaceAll(value, "\u2019", "")
	value = strings.ToLower(value)
	var ascii strings.Builder
	for _, r := range value {
		switch {
		case r >= 'a' && r <= 'z':
			ascii.WriteRune(r)
		case r >= '0' && r <= '9':
			ascii.WriteRune(r)
		default:
			ascii.WriteRune(' ')
		}
	}
	rawWords := SplitRE.Split(ascii.String(), -1)
	words := make([]string, 0, len(rawWords))
	for _, word := range rawWords {
		if word == "" {
			continue
		}
		if _, drop := DropWords[word]; drop {
			continue
		}
		if YearRE.MatchString(word) {
			continue
		}
		words = append(words, word)
	}
	return words
}

func MakeMusicCore(words []string, maxCore int) string {
	core := ""
	for _, word := range words {
		candidate := word
		if core != "" {
			candidate = core + "_" + word
		}
		if len(candidate) <= maxCore {
			core = candidate
			continue
		}
		break
	}
	if core != "" {
		return core
	}
	abbrev := make([]string, 0, Min(4, len(words)))
	for _, word := range words {
		if len(abbrev) == 4 {
			break
		}
		abbrev = append(abbrev, AbbreviateWord(word, 5))
	}
	for _, word := range abbrev {
		candidate := word
		if core != "" {
			candidate = core + "_" + word
		}
		if len(candidate) <= maxCore {
			core = candidate
			continue
		}
		break
	}
	if core != "" {
		return core
	}
	if len(words) > 0 {
		return strings.Trim(words[0][:Min(maxCore, len(words[0]))], "_")
	}
	return ""
}

func AbbreviateWord(word string, maxLen int) string {
	if len(word) <= maxLen {
		return word
	}
	consonants := VowelRE.ReplaceAllString(word, "")
	candidate := word[:1]
	if len(consonants) > 1 {
		candidate += consonants[1:]
	}
	if len(candidate) >= 3 {
		return candidate[:Min(maxLen, len(candidate))]
	}
	return word[:Min(maxLen, len(word))]
}

func UniqueName(stem string, used map[string]struct{}) string {
	return UniqueNameWithMax(stem, MaxStemLen, used)
}

func UniqueNameWithMax(stem string, maxStemLen int, used map[string]struct{}) string {
	if maxStemLen <= 0 {
		maxStemLen = MaxStemLen
	}
	if _, exists := used[stem]; !exists {
		used[stem] = struct{}{}
		return stem
	}
	for index := 1; ; index++ {
		suffix := fmt.Sprintf("_%d", index)
		prefixLen := Min(len(stem), Max(0, maxStemLen-len(suffix)))
		candidate := strings.TrimRight(stem[:prefixLen], "_") + suffix
		if _, exists := used[candidate]; exists {
			continue
		}
		used[candidate] = struct{}{}
		return candidate
	}
}

func ReserveName(stem string, used map[string]struct{}) error {
	stem = strings.ToLower(strings.TrimSpace(stem))
	if stem == "" {
		return fmt.Errorf("output filename is empty")
	}
	if _, exists := used[stem]; exists {
		return fmt.Errorf("output filename %s collides with an existing asset", stem)
	}
	used[stem] = struct{}{}
	return nil
}

func ValidateManualOutputFile(name string) error {
	return ValidateManualOutputFileWithRules(name, ".bmu", MaxStemLen)
}

func ValidateManualOutputFileWithRules(name, extension string, maxStemLen int) error {
	name = strings.ToLower(strings.TrimSpace(name))
	extension = strings.ToLower(strings.TrimSpace(extension))
	if extension == "" {
		extension = ".bmu"
	}
	if maxStemLen <= 0 {
		maxStemLen = MaxStemLen
	}
	if !strings.HasSuffix(name, extension) {
		return fmt.Errorf("output file must end with %s", extension)
	}
	stem := strings.TrimSuffix(name, extension)
	if stem == "" {
		return fmt.Errorf("output file stem is empty")
	}
	if len(stem) > maxStemLen {
		return fmt.Errorf("output file stem %q exceeds %d characters", stem, maxStemLen)
	}
	for _, r := range stem {
		switch {
		case r >= 'a' && r <= 'z':
		case r >= '0' && r <= '9':
		case r == '_':
		default:
			return fmt.Errorf("output file stem %q contains unsupported character %q", stem, string(r))
		}
	}
	return nil
}
