// Copyright 2017 The Prometheus 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 labels

import (
	"bytes"
	"encoding/json"
	"fmt"
	"regexp"
	"strconv"
	"strings"
	"unicode"

	"github.com/prometheus/common/model"
)

// MatchType is an enum for label matching types.
type MatchType int

// Possible MatchTypes.
const (
	MatchEqual MatchType = iota
	MatchNotEqual
	MatchRegexp
	MatchNotRegexp
)

func (m MatchType) String() string {
	typeToStr := map[MatchType]string{
		MatchEqual:     "=",
		MatchNotEqual:  "!=",
		MatchRegexp:    "=~",
		MatchNotRegexp: "!~",
	}
	if str, ok := typeToStr[m]; ok {
		return str
	}
	panic("unknown match type")
}

// Matcher models the matching of a label.
type Matcher struct {
	Type  MatchType
	Name  string
	Value string

	re *regexp.Regexp
}

// NewMatcher returns a matcher object.
func NewMatcher(t MatchType, n, v string) (*Matcher, error) {
	m := &Matcher{
		Type:  t,
		Name:  n,
		Value: v,
	}
	if t == MatchRegexp || t == MatchNotRegexp {
		re, err := regexp.Compile("^(?:" + v + ")$")
		if err != nil {
			return nil, err
		}
		m.re = re
	}
	return m, nil
}

func (m *Matcher) String() string {
	if strings.ContainsFunc(m.Name, isReserved) {
		return fmt.Sprintf(`%s%s%s`, strconv.Quote(m.Name), m.Type, strconv.Quote(m.Value))
	}
	return fmt.Sprintf(`%s%s"%s"`, m.Name, m.Type, openMetricsEscape(m.Value))
}

// Matches returns whether the matcher matches the given string value.
func (m *Matcher) Matches(s string) bool {
	switch m.Type {
	case MatchEqual:
		return s == m.Value
	case MatchNotEqual:
		return s != m.Value
	case MatchRegexp:
		return m.re.MatchString(s)
	case MatchNotRegexp:
		return !m.re.MatchString(s)
	}
	panic("labels.Matcher.Matches: invalid match type")
}

type apiV1Matcher struct {
	Name    string `json:"name"`
	Value   string `json:"value"`
	IsRegex bool   `json:"isRegex"`
	IsEqual bool   `json:"isEqual"`
}

// MarshalJSON retains backwards compatibility with types.Matcher for the v1 API.
func (m Matcher) MarshalJSON() ([]byte, error) {
	return json.Marshal(apiV1Matcher{
		Name:    m.Name,
		Value:   m.Value,
		IsRegex: m.Type == MatchRegexp || m.Type == MatchNotRegexp,
		IsEqual: m.Type == MatchRegexp || m.Type == MatchEqual,
	})
}

func (m *Matcher) UnmarshalJSON(data []byte) error {
	v1m := apiV1Matcher{
		IsEqual: true,
	}

	if err := json.Unmarshal(data, &v1m); err != nil {
		return err
	}

	var t MatchType
	switch {
	case v1m.IsEqual && !v1m.IsRegex:
		t = MatchEqual
	case !v1m.IsEqual && !v1m.IsRegex:
		t = MatchNotEqual
	case v1m.IsEqual && v1m.IsRegex:
		t = MatchRegexp
	case !v1m.IsEqual && v1m.IsRegex:
		t = MatchNotRegexp
	}

	matcher, err := NewMatcher(t, v1m.Name, v1m.Value)
	if err != nil {
		return err
	}
	*m = *matcher
	return nil
}

// openMetricsEscape is similar to the usual string escaping, but more
// restricted. It merely replaces a new-line character with '\n', a double-quote
// character with '\"', and a backslash with '\\', which is the escaping used by
// OpenMetrics.
func openMetricsEscape(s string) string {
	r := strings.NewReplacer(
		`\`, `\\`,
		"\n", `\n`,
		`"`, `\"`,
	)
	return r.Replace(s)
}

// Matchers is a slice of Matchers that is sortable, implements Stringer, and
// provides a Matches method to match a LabelSet against all Matchers in the
// slice. Note that some users of Matchers might require it to be sorted.
type Matchers []*Matcher

func (ms Matchers) Len() int      { return len(ms) }
func (ms Matchers) Swap(i, j int) { ms[i], ms[j] = ms[j], ms[i] }

func (ms Matchers) Less(i, j int) bool {
	if ms[i].Name > ms[j].Name {
		return false
	}
	if ms[i].Name < ms[j].Name {
		return true
	}
	if ms[i].Value > ms[j].Value {
		return false
	}
	if ms[i].Value < ms[j].Value {
		return true
	}
	return ms[i].Type < ms[j].Type
}

// Matches checks whether all matchers are fulfilled against the given label set.
func (ms Matchers) Matches(lset model.LabelSet) bool {
	for _, m := range ms {
		if !m.Matches(string(lset[model.LabelName(m.Name)])) {
			return false
		}
	}
	return true
}

func (ms Matchers) String() string {
	var buf bytes.Buffer

	buf.WriteByte('{')
	for i, m := range ms {
		if i > 0 {
			buf.WriteByte(',')
		}
		buf.WriteString(m.String())
	}
	buf.WriteByte('}')

	return buf.String()
}

// This is copied from matchers/parse/lexer.go. It will be removed when
// the transition window from classic matchers to UTF-8 matchers is complete,
// as then we can use double quotes when printing the label name for all
// matchers. Until then, the classic parser does not understand double quotes
// around the label name, so we use this function as a heuristic to tell if
// the matcher was parsed with the UTF-8 parser or the classic parser.
func isReserved(r rune) bool {
	return unicode.IsSpace(r) || strings.ContainsRune("{}!=~,\\\"'`", r)
}
