package lib

import (
	"fmt"
	"os"
	"slices"
	"sort"
	"strconv"
	"strings"
	"unicode/utf8"

	"github.com/rivo/uniseg"
)

func BooleanXOR(a, b bool) bool {
	return a != b
}

func BoolToInt(b bool) int64 {
	if !b {
		return 0
	}
	return 1
}

func Plural(n int) string {
	if n == 1 {
		return ""
	}
	return "s"
}

// In Go as in all languages I'm aware of with a string-split, "a,b,c" splits
// on "," to ["a", "b", "c" and "a" splits to ["a"], both of which are fine --
// but "" splits to [""] when I wish it were []. This function does the latter.
func SplitString(input string, separator string) []string {
	if input == "" {
		return []string{}
	}
	return strings.Split(input, separator)
}

func StringListToSet(stringList []string) map[string]bool {
	if stringList == nil {
		return nil
	}

	stringSet := make(map[string]bool)
	for _, s := range stringList {
		stringSet[s] = true
	}
	return stringSet
}

// SortedStrings returns a new slice containing the strings in strs in ascending order.
func SortedStrings(strs []string) []string {
	result := make([]string, len(strs))
	copy(result, strs)
	slices.Sort(result)
	return result
}

func IntMin2(a, b int64) int64 {
	if a < b {
		return a
	}
	return b
}

// TryIntFromString tries decimal, hex, octal, and binary.
func TryIntFromString(input string) (int64, bool) {
	// Go's strconv parses "1_2" as 12; not OK for Miller syntax. (Also not valid JSON.)
	for i := 0; i < len(input); i++ {
		if input[i] == '_' {
			return 0, false
		}
	}

	// Following twos-complement formatting familiar from all manner of
	// languages, including C which was Miller's original implementation
	// language, we want to allow 0x00....00 through 0x7f....ff as positive
	// 64-bit integers and 0x80....00 through 0xff....ff as negative ones. Go's
	// signed-int parsing explicitly doesn't allow that, but we don't want Go
	// semantics to dictate Miller semantics.  So, we try signed-int parsing
	// for 0x00....00 through 0x7f....ff, as well as positive or negative
	// decimal. Failing that, we try unsigned-int parsing for 0x80....00
	// through 0xff....ff.
	i64, ierr := strconv.ParseInt(input, 0 /* check all*/, 64)
	if ierr == nil {
		return i64, true
	}
	u64, uerr := strconv.ParseUint(input, 0 /* check all*/, 64)
	if uerr == nil {
		return int64(u64), true
	}
	return 0, false
}

// TryIntFromStringWithBase allows the user to choose the base that's used,
// rather than inferring from 0x prefix, etc as TryIntFromString does.
func TryIntFromStringWithBase(input string, base int64) (int64, bool) {
	// Go's strconv parses "1_2" as 12; not OK for Miller syntax. (Also not valid JSON.)
	for i := 0; i < len(input); i++ {
		if input[i] == '_' {
			return 0, false
		}
	}

	i64, ierr := strconv.ParseInt(input, int(base), 64)
	if ierr == nil {
		return i64, true
	}
	u64, uerr := strconv.ParseUint(input, int(base), 64)
	if uerr == nil {
		return int64(u64), true
	}
	return 0, false
}

func TryFloatFromString(input string) (float64, bool) {
	// Go's strconv parses "1_2.3_4" as 12.34; not OK for Miller syntax. (Also not valid JSON.)
	for i := 0; i < len(input); i++ {
		if input[i] == '_' {
			return 0, false
		}
	}

	fval, err := strconv.ParseFloat(input, 64)
	if err == nil {
		return fval, true
	}
	return 0, false
}

func TryBoolFromBoolString(input string) (bool, bool) {
	if input == "true" {
		return true, true
	}
	if input == "false" {
		return false, true
	}
	return false, false
}

// Go doesn't preserve insertion order in its arrays, so here we make an
// accessor for getting the keys in sorted order for the benefit of
// map-printers.
func GetArrayKeysSorted(input map[string]string) []string {
	keys := make([]string, len(input))
	i := 0
	for key := range input {
		keys[i] = key
		i++
	}
	sort.Strings(keys)
	return keys
}

// WriteTempFile places the contents string into a temp file, which the caller
// must remove.
func WriteTempFile(contents string) (string, error) {
	// Use "" as first argument to os.CreateTemp to use default directory.
	// Nominally "/tmp" or somesuch on all unix-like systems, but not for Windows.
	handle, err := os.CreateTemp("", "mlr-temp")
	if err != nil {
		return "", fmt.Errorf("could not create temp file: %w", err)
	}

	_, err = handle.WriteString(contents)
	if err != nil {
		return "", fmt.Errorf("could not populate temp file: %w", err)
	}

	err = handle.Close()
	if err != nil {
		return "", fmt.Errorf("could not finish write of temp file: %w", err)
	}
	return handle.Name(), nil
}

func StripEmpties(input []string) []string {
	output := make([]string, 0, len(input))
	for _, e := range input {
		if e != "" {
			output = append(output, e)
		}
	}
	return output
}

func UTF8Strlen(s string) int64 {
	return int64(utf8.RuneCountInString(s))
}

// DisplayWidth returns the number of terminal columns occupied by s, taking
// into account East-Asian fullwidth characters (which occupy two columns) and
// zero-width combining marks. Used for column alignment in pprint/markdown/xtab
// output. Note: zero-width-joiner emoji sequences and ambiguous-width characters
// remain terminal-dependent.
func DisplayWidth(s string) int {
	return uniseg.StringWidth(s)
}
