package wand

import (
	"code.squareroundforest.org/arpio/bind"
	"slices"
	"strconv"
	"strings"
	"unicode"
)

type value struct {
	isBool  bool
	boolean bool
	str     string
}

type option struct {
	name      string
	value     value
	shortForm bool
}

type commandLine struct {
	options    []option
	positional []string
}

func boolValue(b bool) value {
	return value{isBool: true, boolean: b}
}

func stringValue(s string) value {
	return value{str: s}
}

func isOption(arg string) bool {
	a := []rune(arg)
	if len(a) <= 2 {
		return false
	}

	if string(a[:2]) != "--" {
		return false
	}

	if !unicode.IsLower(a[2]) {
		return false
	}

	for _, r := range a[3:] {
		if unicode.IsLower(r) {
			continue
		}

		if unicode.IsDigit(r) {
			continue
		}

		if r == '-' {
			continue
		}

		if r == '=' {
			return true
		}

		return false
	}

	return true
}

func isShortOptionSet(arg string) bool {
	a := []rune(arg)
	if len(a) < 2 {
		return false
	}

	if a[0] != '-' {
		return false
	}

	if !unicode.IsLower(a[1]) {
		return false
	}

	for _, r := range a[2:] {
		if r == '=' {
			return true
		}

		if !unicode.IsLower(r) {
			return false
		}
	}

	return true
}

func defaultCommand(cmd Cmd) Cmd {
	if cmd.impl != nil {
		return cmd
	}

	for _, sc := range cmd.subcommands {
		if sc.isDefault {
			return sc
		}
	}

	return cmd
}

func subcommand(cmd Cmd, name string) (Cmd, bool) {
	for _, sc := range cmd.subcommands {
		if sc.name == name {
			return sc, true
		}
	}

	return Cmd{}, false
}

func selectCommand(cmd Cmd, args []string) (Cmd, []string, []string) {
	if len(args) == 0 {
		return defaultCommand(cmd), []string{cmd.name}, nil
	}

	sc, ok := subcommand(cmd, args[0])
	if !ok {
		return defaultCommand(cmd), []string{cmd.name}, args
	}

	sc, fullCommand, args := selectCommand(sc, args[1:])
	fullCommand = append([]string{cmd.name}, fullCommand...)
	return sc, fullCommand, args
}

func boolOption(name string, value bool) option {
	return option{
		name:  name,
		value: boolValue(value),
	}
}

func stringOption(name, value string) option {
	return option{
		name:  name,
		value: stringValue(value),
	}
}

func shortForm(o option) option {
	o.shortForm = true
	return o
}

func canBeValue(arg string) bool {
	if arg == "--" {
		return false
	}

	if isOption(arg) {
		return false
	}

	if isShortOptionSet(arg) {
		return false
	}

	return true
}

func canBeBoolValue(arg string) bool {
	_, err := strconv.ParseBool(arg)
	return err == nil
}

func readOption(boolOptions []string, arg string, args []string) (option, []string) {
	eqi := strings.Index(arg, "=")
	if eqi >= 0 {
		arg, value := arg[:eqi], arg[eqi+1:]
		if slices.Contains(boolOptions, arg) && canBeBoolValue(value) {
			v, _ := strconv.ParseBool(value)
			return boolOption(arg, v), args
		}

		return stringOption(arg, value), args
	}

	var (
		next                               string
		nextCanBeValue, nextCanBeBoolValue bool
	)

	if len(args) > 0 {
		next = args[0]
		nextCanBeValue = canBeValue(next)
		nextCanBeBoolValue = canBeBoolValue(next)
	}

	if slices.Contains(boolOptions, arg) {
		value := true
		if nextCanBeBoolValue {
			value, _ = strconv.ParseBool(next)
			args = args[1:]
		}

		return boolOption(arg, value), args
	}

	if !nextCanBeValue {
		return boolOption(arg, true), args
	}

	return stringOption(arg, next), args[1:]
}

func readShortOptionSet(boolOptions []string, arg string, args []string) ([]option, []string) {
	last := len(arg) - 1
	eqi := strings.Index(arg, "=")
	if eqi >= 0 {
		last = eqi - 1
	}

	var o []option
	for _, a := range arg[:last] {
		o = append(o, shortForm(boolOption(string(a), true)))
	}

	if slices.Contains(boolOptions, arg[last:]) && (len(args) == 0 || !canBeBoolValue(args[0])) {
		o = append(o, shortForm(boolOption(arg[last:], true)))
		return o, args
	}

	var lastOption option
	lastOption, args = readOption(boolOptions, arg[last:], args)
	o = append(o, shortForm(lastOption))
	return o, args
}

func readArgs(boolOptions, args []string) commandLine {
	var c commandLine
	if len(args) == 0 {
		return c
	}

	arg, args := args[0], args[1:]
	switch {
	case arg == "--":
		if len(args) > 0 {
			arg, args = args[0], args[1:]
			c.positional = append(c.positional, arg)
			args = append([]string{"--"}, args...)
		}
	case isOption(arg):
		var f option
		arg = arg[2:]
		f, args = readOption(boolOptions, arg, args)
		c.options = append(c.options, f)
	case isShortOptionSet(arg):
		var f []option
		arg = arg[1:]
		f, args = readShortOptionSet(boolOptions, arg, args)
		c.options = append(c.options, f...)
	default:
		c.positional = append(c.positional, arg)
	}

	cc := readArgs(boolOptions, args)
	c.options = append(c.options, cc.options...)
	c.positional = append(c.positional, cc.positional...)
	return c
}

func hasHelpOption(cmd Cmd, o []option) bool {
	var mf map[string][]bind.Field
	if cmd.impl != nil {
		mf = mapFields(cmd.impl)
	}

	sf := make(map[string]string)
	for i := 0; i < len(cmd.shortForms); i += 2 {
		sf[cmd.shortForms[i]] = cmd.shortForms[i+1]
	}

	for _, oi := range o {
		if !oi.value.isBool {
			continue
		}

		n := oi.name
		if oi.shortForm && n == "h" {
			l, ok := sf["h"]
			if !ok {
				continue
			}

			if l != "help" {
				continue
			}

			n = "help"
		}

		if n == "help" {
			if _, ok := mf["help"]; !ok {
				return true
			}
		}
	}

	return false
}
