/*
   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 defines a syntax and parser that can be used for the
// filtration of items across the containerd API. The core is built on the
// concept of protobuf field paths, with quoting.  Several operators allow the
// user to flexibly select items based on field presence, equality, inequality
// and regular expressions. Flexible adaptors support working with any type.
//
// The syntax is fairly familiar, if you've used container ecosystem
// projects.  At the core, we base it on the concept of protobuf field
// paths, augmenting with the ability to quote portions of the field path
// to match arbitrary labels. These "selectors" come in the following
// syntax:
//
// ```
// <fieldpath>[<operator><value>]
// ```
//
// A basic example is as follows:
//
// ```
// name==foo
// ```
//
// This would match all objects that have a field `name` with the value
// `foo`. If we only want to test if the field is present, we can omit the
// operator. This is most useful for matching labels in containerd. The
// following will match objects that have the field "labels" and have the
// label "foo" defined:
//
// ```
// labels.foo
// ```
//
// We also allow for quoting of parts of the field path to allow matching
// of arbitrary items:
//
// ```
// labels."very complex label"==something
// ```
//
// We also define `!=` and `~=` as operators. The `!=` will match all
// objects that don't match the value for a field and `~=` will compile the
// target value as a regular expression and match the field value against that.
//
// Selectors can be combined using a comma, such that the resulting
// selector will require all selectors are matched for the object to match.
// The following example will match objects that are named `foo` and have
// the label `bar`:
//
// ```
// name==foo,labels.bar
// ```
package filters

import (
	"regexp"

	"github.com/containerd/containerd/log"
)

// Filter matches specific resources based the provided filter
type Filter interface {
	Match(adaptor Adaptor) bool
}

// FilterFunc is a function that handles matching with an adaptor
type FilterFunc func(Adaptor) bool

// Match matches the FilterFunc returning true if the object matches the filter
func (fn FilterFunc) Match(adaptor Adaptor) bool {
	return fn(adaptor)
}

// Always is a filter that always returns true for any type of object
var Always FilterFunc = func(adaptor Adaptor) bool {
	return true
}

// Any allows multiple filters to be matched against the object
type Any []Filter

// Match returns true if any of the provided filters are true
func (m Any) Match(adaptor Adaptor) bool {
	for _, m := range m {
		if m.Match(adaptor) {
			return true
		}
	}

	return false
}

// All allows multiple filters to be matched against the object
type All []Filter

// Match only returns true if all filters match the object
func (m All) Match(adaptor Adaptor) bool {
	for _, m := range m {
		if !m.Match(adaptor) {
			return false
		}
	}

	return true
}

type operator int

const (
	operatorPresent = iota
	operatorEqual
	operatorNotEqual
	operatorMatches
)

func (op operator) String() string {
	switch op {
	case operatorPresent:
		return "?"
	case operatorEqual:
		return "=="
	case operatorNotEqual:
		return "!="
	case operatorMatches:
		return "~="
	}

	return "unknown"
}

type selector struct {
	fieldpath []string
	operator  operator
	value     string
	re        *regexp.Regexp
}

func (m selector) Match(adaptor Adaptor) bool {
	value, present := adaptor.Field(m.fieldpath)

	switch m.operator {
	case operatorPresent:
		return present
	case operatorEqual:
		return present && value == m.value
	case operatorNotEqual:
		return value != m.value
	case operatorMatches:
		if m.re == nil {
			r, err := regexp.Compile(m.value)
			if err != nil {
				log.L.Errorf("error compiling regexp %q", m.value)
				return false
			}

			m.re = r
		}

		return m.re.MatchString(value)
	default:
		return false
	}
}
