// Copyright 2022 The Dawn & Tint Authors
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//  1. Redistributions of source code must retain the above copyright notice, this
//     list of conditions and the following disclaimer.
//
//  2. Redistributions in binary form must reproduce the above copyright notice,
//     this list of conditions and the following disclaimer in the documentation
//     and/or other materials provided with the distribution.
//
//  3. Neither the name of the copyright holder nor the names of its
//     contributors may be used to endorse or promote products derived from
//     this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

// Package query provides helpers for parsing and mutating WebGPU CTS queries.
//
// The full query syntax is described at:
// https://github.com/gpuweb/cts/blob/main/docs/terms.md#queries
//
// Note that this package supports a superset of the official CTS query syntax,
// as this package permits parsing and printing of queries that do not end in a
// wildcard, whereas the CTS requires that all queries end in wildcards unless
// they identify a specific test.
// For example, the following queries are considered valid by this  package, but
// would be rejected by the CTS:
// `suite`, `suite:file`, `suite:file,file`, `suite:file,file:test`.
//
// This relaxation is intentional as the Query type is used for constructing and
// reducing query trees, and always requiring a wildcard adds unnecessary
// complexity.
package query

import (
	"fmt"
	"strings"
)

// Query represents a WebGPU test query
// Example queries:
//
//	'suite'
//	'suite:*'
//	'suite:file'
//	'suite:file,*'
//	'suite:file,file'
//	'suite:file,file,*'
//	'suite:file,file,file:test'
//	'suite:file,file,file:test:*'
//	'suite:file,file,file:test,test:case;*'
type Query struct {
	Suite string
	Files string
	Tests string
	Cases string
}

// Target is the target of a query, either a Suite, File, Test or Case.
type Target int

// Enumerators of Target
const (
	// The query targets a suite
	Suite Target = iota
	// The query targets one or more files
	Files
	// The query targets one or more tests
	Tests
	// The query targets one or more test cases
	Cases

	TargetCount
)

// Format writes the Target to the fmt.State
func (l Target) Format(f fmt.State, verb rune) {
	switch l {
	case Suite:
		fmt.Fprint(f, "suite")
	case Files:
		fmt.Fprint(f, "files")
	case Tests:
		fmt.Fprint(f, "tests")
	case Cases:
		fmt.Fprint(f, "cases")
	default:
		fmt.Fprint(f, "<invalid>")
	}
}

// Delimiter constants used by the query format
const (
	TargetDelimiter = ":"
	FileDelimiter   = ","
	TestDelimiter   = ","
	CaseDelimiter   = ";"
)

// Parse parses a query string
func Parse(s string) Query {
	parts := strings.Split(s, TargetDelimiter)
	q := Query{}
	switch len(parts) {
	default:
		q.Cases = strings.Join(parts[3:], TargetDelimiter)
		fallthrough
	case 3:
		q.Tests = parts[2]
		fallthrough
	case 2:
		q.Files = parts[1]
		fallthrough
	case 1:
		q.Suite = parts[0]
	}
	return q
}

// AppendFiles returns a new query with the strings appended to the 'files'
func (q Query) AppendFiles(f ...string) Query {
	if len(f) > 0 {
		if q.Files == "" {
			q.Files = strings.Join(f, FileDelimiter)
		} else {
			q.Files = q.Files + FileDelimiter + strings.Join(f, FileDelimiter)
		}
	}
	return q
}

// SplitFiles returns the separated 'files' part of the query
func (q Query) SplitFiles() []string {
	if q.Files != "" {
		return strings.Split(q.Files, FileDelimiter)
	}
	return nil
}

// AppendTests returns a new query with the strings appended to the 'tests'
func (q Query) AppendTests(t ...string) Query {
	if len(t) > 0 {
		if q.Tests == "" {
			q.Tests = strings.Join(t, TestDelimiter)
		} else {
			q.Tests = q.Tests + TestDelimiter + strings.Join(t, TestDelimiter)
		}
	}
	return q
}

// SplitTests returns the separated 'tests' part of the query
func (q Query) SplitTests() []string {
	if q.Tests != "" {
		return strings.Split(q.Tests, TestDelimiter)
	}
	return nil
}

// AppendCases returns a new query with the strings appended to the 'cases'
func (q Query) AppendCases(c ...string) Query {
	if len(c) > 0 {
		if q.Cases == "" {
			q.Cases = strings.Join(c, CaseDelimiter)
		} else {
			q.Cases = q.Cases + CaseDelimiter + strings.Join(c, CaseDelimiter)
		}
	}
	return q
}

// SplitCases returns the separated 'cases' part of the query
func (q Query) SplitCases() []string {
	if q.Cases != "" {
		return strings.Split(q.Cases, CaseDelimiter)
	}
	return nil
}

// Case parameters is a map of parameter name to parameter value
type CaseParameters map[string]string

// CaseParameters returns all the case parameters of the query
func (q Query) CaseParameters() CaseParameters {
	if q.Cases != "" {
		out := CaseParameters{}
		for _, c := range strings.Split(q.Cases, CaseDelimiter) {
			idx := strings.IndexRune(c, '=')
			if idx < 0 {
				out[c] = ""
			} else {
				k, v := c[:idx], c[idx+1:]
				out[k] = v
			}
		}
		return out
	}
	return nil
}

// Append returns the query with the additional strings appended to the target
func (q Query) Append(t Target, n ...string) Query {
	switch t {
	case Suite:
		switch len(n) {
		case 0:
			return q
		case 1:
			if q.Suite != "" {
				panic("cannot append suite when query already contains suite")
			}
			return Query{Suite: n[0]}
		default:
			panic("cannot append more than one suite")
		}
	case Files:
		return q.AppendFiles(n...)
	case Tests:
		return q.AppendTests(n...)
	case Cases:
		return q.AppendCases(n...)
	}
	panic("invalid target")
}

// Target returns the target of the query
func (q Query) Target() Target {
	if q.Files != "" {
		if q.Tests != "" {
			if q.Cases != "" {
				return Cases
			}
			return Tests
		}
		return Files
	}
	return Suite
}

// IsWildcard returns true if the query ends with a wildcard
func (q Query) IsWildcard() bool {
	switch q.Target() {
	case Suite:
		return q.Suite == "*"
	case Files:
		return strings.HasSuffix(q.Files, "*")
	case Tests:
		return strings.HasSuffix(q.Tests, "*")
	case Cases:
		return strings.HasSuffix(q.Cases, "*")
	}
	panic("invalid target")
}

// String returns the query formatted as a string
func (q Query) String() string {
	sb := strings.Builder{}
	sb.WriteString(q.Suite)
	if q.Files != "" {
		sb.WriteString(TargetDelimiter)
		sb.WriteString(q.Files)
		if q.Tests != "" {
			sb.WriteString(TargetDelimiter)
			sb.WriteString(q.Tests)
			if q.Cases != "" {
				sb.WriteString(TargetDelimiter)
				sb.WriteString(q.Cases)
			}
		}
	}
	return sb.String()
}

// ExpectationFileString returns the query formatted as a string, suitable for
// use directly in expectation files.
func (q Query) ExpectationFileString() string {
	baseString := q.String()
	// Expectation files want a trailing ":" for test queries.
	if q.Target() == Tests && !q.IsWildcard() {
		baseString += TargetDelimiter
	}
	return baseString
}

// Compare compares the relative order of q and o, returning:
//
//	-1 if q should come before o
//	 1 if q should come after o
//	 0 if q and o are identical
func (q Query) Compare(o Query) int {
	for _, cmp := range []struct{ a, b string }{
		{q.Suite, o.Suite},
		{q.Files, o.Files},
		{q.Tests, o.Tests},
		{q.Cases, o.Cases},
	} {
		if cmp.a < cmp.b {
			return -1
		}
		if cmp.a > cmp.b {
			return 1
		}
	}

	return 0
}

// Contains returns true if q is a superset of o
func (q Query) Contains(o Query) bool {
	if q.Suite != o.Suite {
		return false
	}
	{
		a, b := q.SplitFiles(), o.SplitFiles()
		for i, f := range a {
			if f == "*" {
				return true
			}
			if i >= len(b) || b[i] != f {
				return false
			}
		}
		if len(a) < len(b) {
			return false
		}
	}
	{
		a, b := q.SplitTests(), o.SplitTests()
		for i, f := range a {
			if f == "*" {
				return true
			}
			if i >= len(b) || b[i] != f {
				return false
			}
		}
		if len(a) < len(b) {
			return false
		}
	}
	{
		a, b := q.CaseParameters(), o.CaseParameters()
		for key, av := range a {
			if bv, found := b[key]; found && av != bv {
				return false
			}
		}
	}
	return true
}

// Callback function for Query.Walk()
//
//	q is the query for the current segment.
//	t is the target of the query q.
//	n is the name of the new segment.
type WalkCallback func(q Query, t Target, n string) error

// Walk calls 'f' for each suite, file, test segment, and calls f once for all
// cases. If f returns an error then walking is immediately terminated and the
// error is returned.
func (q Query) Walk(f WalkCallback) error {
	p := Query{Suite: q.Suite}

	if err := f(p, Suite, q.Suite); err != nil {
		return err
	}

	for _, file := range q.SplitFiles() {
		p = p.AppendFiles(file)
		if err := f(p, Files, file); err != nil {
			return err
		}
	}

	for _, test := range q.SplitTests() {
		p = p.AppendTests(test)
		if err := f(p, Tests, test); err != nil {
			return err
		}
	}

	if q.Cases != "" {
		if err := f(q, Cases, q.Cases); err != nil {
			return err
		}
	}

	return nil
}
