package eval

import (
	"errors"
	"fmt"
	"os"
	"strings"
	"unicode"

	"src.elv.sh/pkg/eval/vals"
	"src.elv.sh/pkg/glob"
	"src.elv.sh/pkg/parse"
)

// An ephemeral value generated when evaluating tilde and wildcards.
type globPattern struct {
	glob.Pattern
	Flags  globFlag
	Buts   []string
	TypeCb func(os.FileMode) bool
}

type globFlag uint

var typeCbMap = map[string]func(os.FileMode) bool{
	"dir":     os.FileMode.IsDir,
	"regular": os.FileMode.IsRegular,
}

const (
	// noMatchOK indicates that the "nomatch-ok" glob index modifier was
	// present.
	noMatchOK globFlag = 1 << iota
)

func (f globFlag) Has(g globFlag) bool {
	return (f & g) == g
}

var _ vals.ErrIndexer = globPattern{}

var (
	ErrMustFollowWildcard    = errors.New("must follow wildcard")
	ErrModifierMustBeString  = errors.New("modifier must be string")
	ErrWildcardNoMatch       = errors.New("wildcard has no match")
	ErrMultipleTypeModifiers = errors.New("only one type modifier allowed")
	ErrUnknownTypeModifier   = errors.New("unknown type modifier")
)

var runeMatchers = map[string]func(rune) bool{
	"control": unicode.IsControl,
	"digit":   unicode.IsDigit,
	"graphic": unicode.IsGraphic,
	"letter":  unicode.IsLetter,
	"lower":   unicode.IsDigit,
	"mark":    unicode.IsMark,
	"number":  unicode.IsNumber,
	"print":   unicode.IsPrint,
	"punct":   unicode.IsPunct,
	"space":   unicode.IsSpace,
	"symbol":  unicode.IsSymbol,
	"title":   unicode.IsTitle,
	"upper":   unicode.IsUpper,
}

func (gp globPattern) Kind() string { return "glob-pattern" }

func (gp globPattern) Index(k any) (any, error) {
	modifierv, ok := k.(string)
	if !ok {
		return nil, ErrModifierMustBeString
	}
	modifier := modifierv
	switch {
	case modifier == "nomatch-ok":
		gp.Flags |= noMatchOK
	case strings.HasPrefix(modifier, "but:"):
		gp.Buts = append(gp.Buts, modifier[len("but:"):])
	case modifier == "match-hidden":
		lastSeg, err := gp.lastWildSeg()
		if err != nil {
			return nil, err
		}
		gp.Segments[len(gp.Segments)-1] = glob.Wild{
			Type: lastSeg.Type, MatchHidden: true, Matchers: lastSeg.Matchers,
		}
	case strings.HasPrefix(modifier, "type:"):
		if gp.TypeCb != nil {
			return nil, ErrMultipleTypeModifiers
		}
		typeName := modifier[len("type:"):]
		cb, ok := typeCbMap[typeName]
		if !ok {
			return nil, ErrUnknownTypeModifier
		}
		gp.TypeCb = cb
	default:
		var matcher func(rune) bool
		if m, ok := runeMatchers[modifier]; ok {
			matcher = m
		} else if strings.HasPrefix(modifier, "set:") {
			set := modifier[len("set:"):]
			matcher = func(r rune) bool {
				return strings.ContainsRune(set, r)
			}
		} else if strings.HasPrefix(modifier, "range:") {
			rangeExpr := modifier[len("range:"):]
			badRangeExpr := fmt.Errorf("bad range modifier: %s", parse.Quote(rangeExpr))
			runes := []rune(rangeExpr)
			if len(runes) != 3 {
				return nil, badRangeExpr
			}
			from, sep, to := runes[0], runes[1], runes[2]
			switch sep {
			case '-':
				matcher = func(r rune) bool {
					return from <= r && r <= to
				}
			case '~':
				matcher = func(r rune) bool {
					return from <= r && r < to
				}
			default:
				return nil, badRangeExpr
			}
		} else {
			return nil, fmt.Errorf("unknown modifier %s", vals.ReprPlain(modifierv))
		}
		err := gp.addMatcher(matcher)
		return gp, err
	}
	return gp, nil
}

func (gp globPattern) Concat(v any) (any, error) {
	switch rhs := v.(type) {
	case string:
		gp.append(stringToSegments(rhs)...)
		return gp, nil
	case globPattern:
		// We know rhs contains exactly one segment.
		gp.append(rhs.Segments[0])
		gp.Flags |= rhs.Flags
		gp.Buts = append(gp.Buts, rhs.Buts...)
		// This handles illegal cases such as `**[type:regular]x*[type:directory]`.
		if gp.TypeCb != nil && rhs.TypeCb != nil {
			return nil, ErrMultipleTypeModifiers
		}
		if rhs.TypeCb != nil {
			gp.TypeCb = rhs.TypeCb
		}
		return gp, nil
	}

	return nil, vals.ErrConcatNotImplemented
}

func (gp globPattern) RConcat(v any) (any, error) {
	switch lhs := v.(type) {
	case string:
		segs := stringToSegments(lhs)
		// We know gp contains exactly one segment.
		segs = append(segs, gp.Segments[0])
		return globPattern{Pattern: glob.Pattern{Segments: segs}, Flags: gp.Flags,
			Buts: gp.Buts, TypeCb: gp.TypeCb}, nil
	}

	return nil, vals.ErrConcatNotImplemented
}

func (gp *globPattern) lastWildSeg() (glob.Wild, error) {
	if len(gp.Segments) == 0 {
		return glob.Wild{}, ErrBadglobPattern
	}
	if !glob.IsWild(gp.Segments[len(gp.Segments)-1]) {
		return glob.Wild{}, ErrMustFollowWildcard
	}
	return gp.Segments[len(gp.Segments)-1].(glob.Wild), nil
}

func (gp *globPattern) addMatcher(matcher func(rune) bool) error {
	lastSeg, err := gp.lastWildSeg()
	if err != nil {
		return err
	}
	gp.Segments[len(gp.Segments)-1] = glob.Wild{
		Type: lastSeg.Type, MatchHidden: lastSeg.MatchHidden,
		Matchers: append(lastSeg.Matchers, matcher),
	}
	return nil
}

func (gp *globPattern) append(segs ...glob.Segment) {
	gp.Segments = append(gp.Segments, segs...)
}

func wildcardToSegment(s string) (glob.Segment, error) {
	switch s {
	case "*":
		return glob.Wild{Type: glob.Star, MatchHidden: false, Matchers: nil}, nil
	case "**":
		return glob.Wild{Type: glob.StarStar, MatchHidden: false, Matchers: nil}, nil
	case "?":
		return glob.Wild{Type: glob.Question, MatchHidden: false, Matchers: nil}, nil
	default:
		return nil, fmt.Errorf("bad wildcard: %q", s)
	}
}

func stringToSegments(s string) []glob.Segment {
	segs := []glob.Segment{}
	for i := 0; i < len(s); {
		j := i
		for ; j < len(s) && s[j] != '/'; j++ {
		}
		if j > i {
			segs = append(segs, glob.Literal{Data: s[i:j]})
		}
		if j < len(s) {
			for ; j < len(s) && s[j] == '/'; j++ {
			}
			segs = append(segs, glob.Slash{})
			i = j
		} else {
			break
		}
	}
	return segs
}

func doGlob(gp globPattern, abort <-chan struct{}) ([]any, error) {
	but := make(map[string]struct{})
	for _, s := range gp.Buts {
		but[s] = struct{}{}
	}

	vs := make([]any, 0)
	if !gp.Glob(func(pathInfo glob.PathInfo) bool {
		select {
		case <-abort:
			logger.Println("glob aborted")
			return false
		default:
		}

		if _, ignore := but[pathInfo.Path]; ignore {
			return true
		}

		if gp.TypeCb == nil || gp.TypeCb(pathInfo.Info.Mode()) {
			vs = append(vs, pathInfo.Path)
		}
		return true
	}) {
		return nil, ErrInterrupted
	}
	if len(vs) == 0 && !gp.Flags.Has(noMatchOK) {
		return nil, ErrWildcardNoMatch
	}
	return vs, nil
}
