/*
   Copyright The containerd Authors.

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package filters

import (
	"fmt"
	"io"

	"github.com/containerd/errdefs"
)

/*
Parse the strings into a filter that may be used with an adaptor.

The filter is made up of zero or more selectors.

The format is a comma separated list of expressions, in the form of
`<fieldpath><op><value>`, known as selectors. All selectors must match the
target object for the filter to be true.

We define the operators "==" for equality, "!=" for not equal and "~=" for a
regular expression. If the operator and value are not present, the matcher will
test for the presence of a value, as defined by the target object.

The formal grammar is as follows:

selectors := selector ("," selector)*
selector  := fieldpath (operator value)
fieldpath := field ('.' field)*
field     := quoted | [A-Za-z] [A-Za-z0-9_]+
operator  := "==" | "!=" | "~="
value     := quoted | [^\s,]+
quoted    := <go string syntax>
*/
func Parse(s string) (Filter, error) {
	// special case empty to match all
	if s == "" {
		return Always, nil
	}

	p := parser{input: s}
	return p.parse()
}

// ParseAll parses each filter in ss and returns a filter that will return true
// if any filter matches the expression.
//
// If no filters are provided, the filter will match anything.
func ParseAll(ss ...string) (Filter, error) {
	if len(ss) == 0 {
		return Always, nil
	}

	var fs []Filter
	for _, s := range ss {
		f, err := Parse(s)
		if err != nil {
			return nil, fmt.Errorf("%s: %w", err.Error(), errdefs.ErrInvalidArgument)
		}

		fs = append(fs, f)
	}

	return Any(fs), nil
}

type parser struct {
	input   string
	scanner scanner
}

func (p *parser) parse() (Filter, error) {
	p.scanner.init(p.input)

	ss, err := p.selectors()
	if err != nil {
		return nil, fmt.Errorf("filters: %w", err)
	}

	return ss, nil
}

func (p *parser) selectors() (Filter, error) {
	s, err := p.selector()
	if err != nil {
		return nil, err
	}

	ss := All{s}

loop:
	for {
		tok := p.scanner.peek()
		switch tok {
		case ',':
			pos, tok, _ := p.scanner.scan()
			if tok != tokenSeparator {
				return nil, p.mkerr(pos, "expected a separator")
			}

			s, err := p.selector()
			if err != nil {
				return nil, err
			}

			ss = append(ss, s)
		case tokenEOF:
			break loop
		default:
			return nil, p.mkerr(p.scanner.ppos, "unexpected input: %v", string(tok))
		}
	}

	return ss, nil
}

func (p *parser) selector() (selector, error) {
	fieldpath, err := p.fieldpath()
	if err != nil {
		return selector{}, err
	}

	switch p.scanner.peek() {
	case ',', tokenSeparator, tokenEOF:
		return selector{
			fieldpath: fieldpath,
			operator:  operatorPresent,
		}, nil
	}

	op, err := p.operator()
	if err != nil {
		return selector{}, err
	}

	var allowAltQuotes bool
	if op == operatorMatches {
		allowAltQuotes = true
	}

	value, err := p.value(allowAltQuotes)
	if err != nil {
		if err == io.EOF {
			return selector{}, io.ErrUnexpectedEOF
		}
		return selector{}, err
	}

	return selector{
		fieldpath: fieldpath,
		value:     value,
		operator:  op,
	}, nil
}

func (p *parser) fieldpath() ([]string, error) {
	f, err := p.field()
	if err != nil {
		return nil, err
	}

	fs := []string{f}
loop:
	for {
		tok := p.scanner.peek() // lookahead to consume field separator

		switch tok {
		case '.':
			pos, tok, _ := p.scanner.scan() // consume separator
			if tok != tokenSeparator {
				return nil, p.mkerr(pos, "expected a field separator (`.`)")
			}

			f, err := p.field()
			if err != nil {
				return nil, err
			}

			fs = append(fs, f)
		default:
			// let the layer above handle the other bad cases.
			break loop
		}
	}

	return fs, nil
}

func (p *parser) field() (string, error) {
	pos, tok, s := p.scanner.scan()
	switch tok {
	case tokenField:
		return s, nil
	case tokenQuoted:
		return p.unquote(pos, s, false)
	case tokenIllegal:
		return "", p.mkerr(pos, "%s", p.scanner.err)
	}

	return "", p.mkerr(pos, "expected field or quoted")
}

func (p *parser) operator() (operator, error) {
	pos, tok, s := p.scanner.scan()
	switch tok {
	case tokenOperator:
		switch s {
		case "==":
			return operatorEqual, nil
		case "!=":
			return operatorNotEqual, nil
		case "~=":
			return operatorMatches, nil
		default:
			return 0, p.mkerr(pos, "unsupported operator %q", s)
		}
	case tokenIllegal:
		return 0, p.mkerr(pos, "%s", p.scanner.err)
	}

	return 0, p.mkerr(pos, `expected an operator ("=="|"!="|"~=")`)
}

func (p *parser) value(allowAltQuotes bool) (string, error) {
	pos, tok, s := p.scanner.scan()

	switch tok {
	case tokenValue, tokenField:
		return s, nil
	case tokenQuoted:
		return p.unquote(pos, s, allowAltQuotes)
	case tokenIllegal:
		return "", p.mkerr(pos, "%s", p.scanner.err)
	}

	return "", p.mkerr(pos, "expected value or quoted")
}

func (p *parser) unquote(pos int, s string, allowAlts bool) (string, error) {
	if !allowAlts && s[0] != '\'' && s[0] != '"' {
		return "", p.mkerr(pos, "invalid quote encountered")
	}

	uq, err := unquote(s)
	if err != nil {
		return "", p.mkerr(pos, "unquoting failed: %v", err)
	}

	return uq, nil
}

type parseError struct {
	input string
	pos   int
	msg   string
}

func (pe parseError) Error() string {
	if pe.pos < len(pe.input) {
		before := pe.input[:pe.pos]
		location := pe.input[pe.pos : pe.pos+1] // need to handle end
		after := pe.input[pe.pos+1:]

		return fmt.Sprintf("[%s >|%s|< %s]: %v", before, location, after, pe.msg)
	}

	return fmt.Sprintf("[%s]: %v", pe.input, pe.msg)
}

func (p *parser) mkerr(pos int, format string, args ...interface{}) error {
	return fmt.Errorf("parse error: %w", parseError{
		input: p.input,
		pos:   pos,
		msg:   fmt.Sprintf(format, args...),
	})
}
