package bilib

import (
	"bytes"
	"os"
	"sort"
	"strconv"
	"strings"
	"time"
)

// Names comprises a set of file names
type Names map[string]interface{}

// ToNames converts string slice to a set of names
func ToNames(list []string) Names {
	ns := Names{}
	for _, f := range list {
		ns.Add(f)
	}
	return ns
}

// Add adds new file name to the set
func (ns Names) Add(name string) {
	ns[name] = nil
}

// Has checks whether given name is present in the set
func (ns Names) Has(name string) bool {
	_, ok := ns[name]
	return ok
}

// NotEmpty checks whether set is not empty
func (ns Names) NotEmpty() bool {
	return len(ns) > 0
}

// ToList converts name set to string slice
func (ns Names) ToList() []string {
	list := []string{}
	for file := range ns {
		list = append(list, file)
	}
	sort.Strings(list)
	return list
}

// Save saves name set in a text file
func (ns Names) Save(path string) error {
	return SaveList(ns.ToList(), path)
}

// SaveList saves file name list in a text file
func SaveList(list []string, path string) error {
	buf := &bytes.Buffer{}
	for _, s := range list {
		_, _ = buf.WriteString(strconv.Quote(s))
		_ = buf.WriteByte('\n')
	}
	return os.WriteFile(path, buf.Bytes(), PermSecure)
}

// AliasMap comprises a pair of names that are not equal but treated as equal for comparison purposes
// For example, when normalizing unicode and casing
// This helps reduce repeated normalization functions, which really slow things down
type AliasMap map[string]string

// Add adds new pair to the set, in both directions
func (am AliasMap) Add(name1, name2 string) {
	if name1 != name2 {
		am[name1] = name2
		am[name2] = name1
	}
}

// Alias returns the alternate version, if any, else the original.
func (am AliasMap) Alias(name1 string) string {
	// note: we don't need to check normalization settings, because we already did it in March.
	// the AliasMap will only exist if March paired up two unequal filenames.
	name2, ok := am[name1]
	if ok {
		return name2
	}
	return name1
}

// ParseGlobs determines whether a string contains {brackets}
// and returns the substring (including both brackets) for replacing
// substring is first opening bracket to last closing bracket --
// good for {{this}} but not {this}{this}
func ParseGlobs(s string) (hasGlobs bool, substring string) {
	open := strings.Index(s, "{")
	close := strings.LastIndex(s, "}")
	if open >= 0 && close > open {
		return true, s[open : close+1]
	}
	return false, ""
}

// TrimBrackets converts {{this}} to this
func TrimBrackets(s string) string {
	return strings.Trim(s, "{}")
}

// TimeFormat converts a user-supplied string to a Go time constant, if possible
func TimeFormat(timeFormat string) string {
	switch timeFormat {
	case "Layout":
		timeFormat = time.Layout
	case "ANSIC":
		timeFormat = time.ANSIC
	case "UnixDate":
		timeFormat = time.UnixDate
	case "RubyDate":
		timeFormat = time.RubyDate
	case "RFC822":
		timeFormat = time.RFC822
	case "RFC822Z":
		timeFormat = time.RFC822Z
	case "RFC850":
		timeFormat = time.RFC850
	case "RFC1123":
		timeFormat = time.RFC1123
	case "RFC1123Z":
		timeFormat = time.RFC1123Z
	case "RFC3339":
		timeFormat = time.RFC3339
	case "RFC3339Nano":
		timeFormat = time.RFC3339Nano
	case "Kitchen":
		timeFormat = time.Kitchen
	case "Stamp":
		timeFormat = time.Stamp
	case "StampMilli":
		timeFormat = time.StampMilli
	case "StampMicro":
		timeFormat = time.StampMicro
	case "StampNano":
		timeFormat = time.StampNano
	case "DateTime":
		// timeFormat = time.DateTime // missing in go1.19
		timeFormat = "2006-01-02 15:04:05"
	case "DateOnly":
		// timeFormat = time.DateOnly // missing in go1.19
		timeFormat = "2006-01-02"
	case "TimeOnly":
		// timeFormat = time.TimeOnly // missing in go1.19
		timeFormat = "15:04:05"
	case "MacFriendlyTime", "macfriendlytime", "mac":
		timeFormat = "2006-01-02 0304PM" // not actually a Go constant -- but useful as macOS filenames can't have colons
	}
	return timeFormat
}

// AppyTimeGlobs converts "myfile-{DateOnly}.txt" to "myfile-2006-01-02.txt"
func AppyTimeGlobs(s string, t time.Time) string {
	hasGlobs, substring := ParseGlobs(s)
	if !hasGlobs {
		return s
	}
	timeString := t.Local().Format(TimeFormat(TrimBrackets(substring)))
	return strings.ReplaceAll(s, substring, timeString)
}