// Copyright 2025 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 reducedglob

import (
	"slices"
	"strings"
)

// This package provides wildcard matching for "*" characters. However, unlike
// third-party "glob" Go packages or Python's fnmatch module, this intentionally
// does not match other types of wildcards such as "?". This is based on the
// Python implementation in typ
// https://chromium.googlesource.com/catapult/+/5b409767f0b2a05044d7139c3c000773da013775/third_party/typ/typ/reduced_glob.py

const (
	ESCAPED_WILDCARD   = "\\*"
	UNESCAPED_WILDCARD = "*"
)

// Struct representing a "compiled" glob that only matches "*" characters.
// Should not be created directly. Use NewReducedGlob() instead.
type ReducedGlob struct {
	pattern    string
	substrings []string
}

// NewReducedGlob creates and returns a new ReducedGlob instance with the
// provided pattern.
func NewReducedGlob(pattern string) *ReducedGlob {
	rg := ReducedGlob{pattern: pattern}
	rg.computeSubstrings()
	return &rg
}

// computeSubstrings performs the one-time computations to get the list of
// substrings a ReducedGlob looks for.
func (rg *ReducedGlob) computeSubstrings() {
	// Find all indices of * characters, ignoring those escaped via \*. Then, use
	// that to create an ordered list of substrings that we need to have, with
	// any number of characters permitted between them.

	// Calculate indices of un-escaped wildcards.
	allWildcardIndices := findAllIndices(rg.pattern, UNESCAPED_WILDCARD)
	escapedWildcardIndices := findAllIndices(rg.pattern, ESCAPED_WILDCARD)
	// Offset by 1 so that the indices match those in allWildcardIndices.
	for i, value := range escapedWildcardIndices {
		escapedWildcardIndices[i] = value + 1
	}

	unescapedWildcardIndices := []int{}
	for _, index := range allWildcardIndices {
		if !slices.Contains(escapedWildcardIndices, index) {
			unescapedWildcardIndices = append(unescapedWildcardIndices, index)
		}
	}

	// Split |pattern| into strings to match using the calculated indices.
	previousIndex := 0
	for _, index := range unescapedWildcardIndices {
		rg.substrings = append(rg.substrings, rg.pattern[previousIndex:index])
		previousIndex = index + 1
	}
	rg.substrings = append(rg.substrings, rg.pattern[previousIndex:])
	for i, substr := range rg.substrings {
		rg.substrings[i] = strings.ReplaceAll(substr, ESCAPED_WILDCARD, UNESCAPED_WILDCARD)
	}
}

// Matchcase checks whether |name| matches the stored pattern. Case-sensitive.
func (rg *ReducedGlob) Matchcase(name string) bool {
	if len(rg.substrings) == 0 {
		panic("ReducedGlob used without calling computeSubstrings()")
	}

	// Look for each substring in order, shifting the starting point to avoid
	// anything we've matched already.
	startingIndex := 0
	for i, substr := range rg.substrings {
		substrStartIndex := strings.Index(name[startingIndex:], substr)
		if substrStartIndex == -1 {
			return false
		}

		// The first substring is special since we need to ensure that |name| starts
		// with it. Otherwise, we could potentially match later in the string, which
		// would implicitly add a * to the front of the stored pattern.
		if i == 0 && substrStartIndex != 0 {
			return false
		}

		// Similarly, the last substring is special since we need to ensure that all
		// characters in |name| were matched. Otherwise, we would implicitly add a
		// * to the end of the stored pattern.
		if i+1 == len(rg.substrings) && !strings.HasSuffix(name, substr) {
			return false
		}

		// Consume everything we just matched.
		startingIndex += substrStartIndex + len(substr)
	}
	return true
}

// findAllIndices finds all indices where |substr| exists within |s|.
func findAllIndices(s, substr string) []int {
	allIndices := []int{}
	offset := 0
	index := strings.Index(s, substr)
	for index != -1 {
		allIndices = append(allIndices, index+offset)
		offset += index + 1
		index = strings.Index(s[offset:], substr)
	}
	return allIndices
}
