package expr

import (
	"bytes"
	"path/filepath"
	"regexp"

	"github.com/woodpecker-ci/expr/parse"
)

// state represents the state of an execution. It's not part of the
// statement so that multiple executions of the same statement
// can execute in parallel.
type state struct {
	node parse.Node
	vars Row
}

// at marks the state to be on node n, for error reporting.
func (s *state) at(node parse.Node) {
	s.node = node
}

// Walk functions step through the major pieces of the template structure,
// generating output as they go.
func (s *state) walk(node parse.BoolExpr) bool {
	s.at(node)

	switch node := node.(type) {
	case *parse.ComparisonExpr:
		return s.eval(node)
	case *parse.AndExpr:
		return s.walk(node.Left) && s.walk(node.Right)
	case *parse.OrExpr:
		return s.walk(node.Left) || s.walk(node.Right)
	case *parse.NotExpr:
		return !s.walk(node.Expr)
	case *parse.ParenBoolExpr:
		return s.walk(node.Expr)
	default:
		panic("invalid node type")
	}
}

func (s *state) eval(node *parse.ComparisonExpr) bool {
	switch node.Operator {
	case parse.OperatorEq:
		return s.evalEq(node)
	case parse.OperatorGt:
		return s.evalGt(node)
	case parse.OperatorGte:
		return s.evalGte(node)
	case parse.OperatorLt:
		return s.evalLt(node)
	case parse.OperatorLte:
		return s.evalLte(node)
	case parse.OperatorNeq:
		return !s.evalEq(node)
	case parse.OperatorGlob:
		return s.evalGlob(node)
	case parse.OperatorNotGlob:
		return !s.evalGlob(node)
	case parse.OperatorRe:
		return s.evalRegexp(node)
	case parse.OperatorNotRe:
		return !s.evalRegexp(node)
	case parse.OperatorIn:
		return s.evalIn(node)
	case parse.OperatorNotIn:
		return !s.evalIn(node)
	default:
		panic("inalid operator type")
	}
}

func (s *state) evalEq(node *parse.ComparisonExpr) bool {
	return bytes.Equal(
		s.toValue(node.Left),
		s.toValue(node.Right),
	)
}

func (s *state) evalGt(node *parse.ComparisonExpr) bool {
	return bytes.Compare(
		s.toValue(node.Left),
		s.toValue(node.Right),
	) == 1
}

func (s *state) evalGte(node *parse.ComparisonExpr) bool {
	return bytes.Compare(
		s.toValue(node.Left),
		s.toValue(node.Right),
	) >= 0
}

func (s *state) evalLt(node *parse.ComparisonExpr) bool {
	return bytes.Compare(
		s.toValue(node.Left),
		s.toValue(node.Right),
	) == -1
}

func (s *state) evalLte(node *parse.ComparisonExpr) bool {
	return bytes.Compare(
		s.toValue(node.Left),
		s.toValue(node.Right),
	) <= 0
}

func (s *state) evalGlob(node *parse.ComparisonExpr) bool {
	match, _ := filepath.Match(
		string(s.toValue(node.Right)),
		string(s.toValue(node.Left)),
	)
	return match
}

func (s *state) evalRegexp(node *parse.ComparisonExpr) bool {
	match, _ := regexp.Match(
		string(s.toValue(node.Right)),
		s.toValue(node.Left),
	)
	return match
}

func (s *state) evalIn(node *parse.ComparisonExpr) bool {
	left := s.toValue(node.Left)
	right, ok := node.Right.(*parse.ArrayLit)
	if !ok {
		panic("expected array literal")
	}

	for _, expr := range right.Values {
		if bytes.Equal(left, s.toValue(expr)) {
			return true
		}
	}
	return false
}

func (s *state) toValue(expr parse.ValExpr) []byte {
	switch node := expr.(type) {
	case *parse.Field:
		return s.vars.Field(node.Name)
	case *parse.BasicLit:
		return node.Value
	default:
		panic("invalid expression type")
	}
}

// errRecover is the handler that turns panics into returns.
func errRecover(err *error) {
	if e := recover(); e != nil {
		*err = e.(error)
	}
}
