package css_parser

import (
	"fmt"

	"github.com/evanw/esbuild/internal/compat"
	"github.com/evanw/esbuild/internal/css_ast"
	"github.com/evanw/esbuild/internal/logger"
)

func (p *parser) lowerNestingInRule(rule css_ast.Rule, results []css_ast.Rule) []css_ast.Rule {
	switch r := rule.Data.(type) {
	case *css_ast.RSelector:
		scope := func(loc logger.Loc) css_ast.ComplexSelector {
			return css_ast.ComplexSelector{
				Selectors: []css_ast.CompoundSelector{{
					SubclassSelectors: []css_ast.SubclassSelector{{
						Range: logger.Range{Loc: loc},
						Data:  &css_ast.SSPseudoClass{Name: "scope"},
					}},
				}},
			}
		}

		parentSelectorsWithPseudo := make([]css_ast.ComplexSelector, 0, len(r.Selectors))
		parentSelectorsNoPseudo := make([]css_ast.ComplexSelector, 0, len(r.Selectors))
		for i, sel := range r.Selectors {
			// Top-level "&" should be replaced with ":scope" to avoid recursion.
			// From https://www.w3.org/TR/css-nesting-1/#nest-selector:
			//
			//   "When used in the selector of a nested style rule, the nesting
			//   selector represents the elements matched by the parent rule. When
			//   used in any other context, it represents the same elements as
			//   :scope in that context (unless otherwise defined)."
			//
			substituted := make([]css_ast.CompoundSelector, 0, len(sel.Selectors))
			for _, x := range sel.Selectors {
				substituted = p.substituteAmpersandsInCompoundSelector(x, scope, substituted, keepLeadingCombinator)
			}
			r.Selectors[i] = css_ast.ComplexSelector{Selectors: substituted}

			// Filter out pseudo elements because they are ignored by nested style
			// rules. This is because pseudo-elements are not valid within :is():
			// https://www.w3.org/TR/selectors-4/#matches-pseudo. This restriction
			// may be relaxed in the future, but this restriction has shipped so
			// we're stuck with it: https://github.com/w3c/csswg-drafts/issues/7433.
			//
			// Note: This is only for the parent selector list that is used to
			// substitute "&" within child rules. Do not filter out the pseudo
			// element from the top-level selector list.
			if !sel.UsesPseudoElement() {
				parentSelectorsNoPseudo = append(parentSelectorsNoPseudo, css_ast.ComplexSelector{Selectors: substituted})
			}

			// This filtering is only done conditionally because it seems to only
			// apply sometimes. Specifically it doesn't seem to apply when the
			// nested rule is an at-rule. So we use the unfiltered list in that
			// case. See: https://github.com/evanw/esbuild/issues/4265
			parentSelectorsWithPseudo = append(parentSelectorsWithPseudo, css_ast.ComplexSelector{Selectors: substituted})
		}

		// Emit this selector before its nested children
		start := len(results)
		results = append(results, rule)

		// Lower all children and filter out ones that become empty
		context := lowerNestingContext{
			parentSelectorsWithPseudo: parentSelectorsWithPseudo,
			parentSelectorsNoPseudo:   parentSelectorsNoPseudo,
			loweredRules:              results,
		}
		r.Rules = p.lowerNestingInRulesAndReturnRemaining(r.Rules, &context)

		// Omit this selector entirely if it's now empty
		if len(r.Rules) == 0 {
			copy(context.loweredRules[start:], context.loweredRules[start+1:])
			context.loweredRules = context.loweredRules[:len(context.loweredRules)-1]
		}
		return context.loweredRules

	case *css_ast.RKnownAt:
		var rules []css_ast.Rule
		for _, child := range r.Rules {
			rules = p.lowerNestingInRule(child, rules)
		}
		r.Rules = rules

	case *css_ast.RAtLayer:
		var rules []css_ast.Rule
		for _, child := range r.Rules {
			rules = p.lowerNestingInRule(child, rules)
		}
		r.Rules = rules

	case *css_ast.RAtMedia:
		var rules []css_ast.Rule
		for _, child := range r.Rules {
			rules = p.lowerNestingInRule(child, rules)
		}
		r.Rules = rules
	}

	return append(results, rule)
}

// Lower all children and filter out ones that become empty
func (p *parser) lowerNestingInRulesAndReturnRemaining(rules []css_ast.Rule, context *lowerNestingContext) []css_ast.Rule {
	n := 0
	for _, child := range rules {
		child = p.lowerNestingInRuleWithContext(child, context)
		if child.Data != nil {
			rules[n] = child
			n++
		}
	}
	return rules[:n]
}

func compoundSelectorTermCount(sel css_ast.CompoundSelector) int {
	count := 0
	for _, ss := range sel.SubclassSelectors {
		count++
		if list, ok := ss.Data.(*css_ast.SSPseudoClassWithSelectorList); ok {
			count += complexSelectorTermCount(list.Selectors)
		}
	}
	return count
}

func complexSelectorTermCount(selectors []css_ast.ComplexSelector) int {
	count := 0
	for _, sel := range selectors {
		for _, inner := range sel.Selectors {
			count += compoundSelectorTermCount(inner)
		}
	}
	return count
}

func (p *parser) addExpansionError(loc logger.Loc, n int) {
	p.log.AddErrorWithNotes(&p.tracker, logger.Range{Loc: loc}, "CSS nesting is causing too much expansion",
		[]logger.MsgData{{Text: fmt.Sprintf("CSS nesting expansion was terminated because a rule was generated with %d selectors. "+
			"This limit exists to prevent esbuild from using too much time and/or memory. "+
			"Please change your CSS to use fewer levels of nesting.", n)}})
}

type lowerNestingContext struct {
	parentSelectorsWithPseudo []css_ast.ComplexSelector
	parentSelectorsNoPseudo   []css_ast.ComplexSelector
	loweredRules              []css_ast.Rule
}

func (p *parser) lowerNestingInRuleWithContext(rule css_ast.Rule, context *lowerNestingContext) css_ast.Rule {
	switch r := rule.Data.(type) {
	case *css_ast.RSelector:
		oldSelectorsLen := len(r.Selectors)
		oldSelectorsComplexity := complexSelectorTermCount(r.Selectors)

		// "a { & b {} }" => "a b {}"
		// "a { &b {} }" => "a:is(b) {}"
		// "a { &:hover {} }" => "a:hover {}"
		// ".x { &b {} }" => "b.x {}"
		// "a, b { .c, d {} }" => ":is(a, b) :is(.c, d) {}"
		// "a, b { &.c, & d, e & {} }" => ":is(a, b).c, :is(a, b) d, e :is(a, b) {}"

		// Pass 1: Canonicalize and analyze our selectors
		for i := range r.Selectors {
			sel := &r.Selectors[i]

			// Inject the implicit "&" now for simplicity later on
			if sel.IsRelative() {
				sel.Selectors = append([]css_ast.CompoundSelector{{NestingSelectorLocs: []logger.Loc{rule.Loc}}}, sel.Selectors...)
			}
		}

		// Pass 2: Substitute "&" for the parent selector
		if !p.options.unsupportedCSSFeatures.Has(compat.IsPseudoClass) || len(context.parentSelectorsNoPseudo) <= 1 {
			// If we can use ":is", or we don't have to because there's only one
			// parent selector, or we are using ":is()" to match zero parent selectors
			// (even if ":is" is unsupported), then substituting "&" for the parent
			// selector is easy.
			for i := range r.Selectors {
				complex := &r.Selectors[i]
				results := make([]css_ast.CompoundSelector, 0, len(complex.Selectors))
				parent := p.multipleComplexSelectorsToSingleComplexSelector(context.parentSelectorsNoPseudo)
				for _, compound := range complex.Selectors {
					results = p.substituteAmpersandsInCompoundSelector(compound, parent, results, keepLeadingCombinator)
				}
				complex.Selectors = results
			}
		} else {
			// Otherwise if we can't use ":is", the transform is more complicated.
			// Avoiding ":is" can lead to a combinatorial explosion of cases so we
			// want to avoid this if possible. For example:
			//
			//   .first, .second, .third {
			//     & > & {
			//       color: red;
			//     }
			//   }
			//
			// If we can use ":is" (the easy case above) then we can do this:
			//
			//   :is(.first, .second, .third) > :is(.first, .second, .third) {
			//     color: red;
			//   }
			//
			// But if we can't use ":is" then we have to do this instead:
			//
			//   .first > .first,
			//   .first > .second,
			//   .first > .third,
			//   .second > .first,
			//   .second > .second,
			//   .second > .third,
			//   .third > .first,
			//   .third > .second,
			//   .third > .third {
			//     color: red;
			//   }
			//
			// That combinatorial explosion is what the loop below implements. Note
			// that PostCSS's implementation of nesting gets this wrong. It generates
			// this instead:
			//
			//   .first > .first,
			//   .second > .second,
			//   .third > .third {
			//     color: red;
			//   }
			//
			// That's not equivalent, so that's an incorrect transformation.
			var selectors []css_ast.ComplexSelector
			var indices []int
			for {
				// Every time we encounter another "&", add another dimension
				offset := 0
				parent := func(loc logger.Loc) css_ast.ComplexSelector {
					if offset == len(indices) {
						indices = append(indices, 0)
					}
					index := indices[offset]
					offset++
					return context.parentSelectorsNoPseudo[index]
				}

				// Do the substitution for this particular combination
				for i := range r.Selectors {
					complex := r.Selectors[i]
					results := make([]css_ast.CompoundSelector, 0, len(complex.Selectors))
					for _, compound := range complex.Selectors {
						results = p.substituteAmpersandsInCompoundSelector(compound, parent, results, keepLeadingCombinator)
					}
					complex.Selectors = results
					selectors = append(selectors, complex)
					offset = 0
				}

				// Do addition with carry on the indices across dimensions
				carry := len(indices)
				for carry > 0 {
					index := &indices[carry-1]
					if *index+1 < len(context.parentSelectorsNoPseudo) {
						*index++
						break
					}
					*index = 0
					carry--
				}
				if carry == 0 {
					break
				}
			}
			r.Selectors = selectors
		}

		// Put limits on the combinatorial explosion to avoid using too much time and/or memory
		if n := len(r.Selectors); n > oldSelectorsLen && n > 0xFF00 {
			p.addExpansionError(rule.Loc, n)
			return css_ast.Rule{}
		}
		if n := complexSelectorTermCount(r.Selectors); n > oldSelectorsComplexity && n > 0xFF00 {
			p.addExpansionError(rule.Loc, n)
			return css_ast.Rule{}
		}

		// Lower all child rules using our newly substituted selector
		context.loweredRules = p.lowerNestingInRule(rule, context.loweredRules)
		return css_ast.Rule{}

	case *css_ast.RKnownAt:
		childContext := lowerNestingContext{
			parentSelectorsWithPseudo: context.parentSelectorsWithPseudo,
			parentSelectorsNoPseudo:   context.parentSelectorsNoPseudo,
		}
		r.Rules = p.lowerNestingInRulesAndReturnRemaining(r.Rules, &childContext)

		// "div { @supports (color: red) { color: red } }" "@supports (color: red) { div { color: red } }"
		if len(r.Rules) > 0 {
			childContext.loweredRules = append([]css_ast.Rule{{Loc: rule.Loc, Data: &css_ast.RSelector{
				Selectors: context.parentSelectorsWithPseudo,
				Rules:     r.Rules,
			}}}, childContext.loweredRules...)
		}

		// "div { @supports (color: red) { &:hover { color: red } } }" "@supports (color: red) { div:hover { color: red } }"
		if len(childContext.loweredRules) > 0 {
			r.Rules = childContext.loweredRules
			context.loweredRules = append(context.loweredRules, rule)
		}

		return css_ast.Rule{}

	case *css_ast.RAtMedia:
		childContext := lowerNestingContext{
			parentSelectorsWithPseudo: context.parentSelectorsWithPseudo,
			parentSelectorsNoPseudo:   context.parentSelectorsNoPseudo,
		}
		r.Rules = p.lowerNestingInRulesAndReturnRemaining(r.Rules, &childContext)

		// "div { @media screen { color: red } }" "@media screen { div { color: red } }"
		if len(r.Rules) > 0 {
			childContext.loweredRules = append([]css_ast.Rule{{Loc: rule.Loc, Data: &css_ast.RSelector{
				Selectors: context.parentSelectorsWithPseudo,
				Rules:     r.Rules,
			}}}, childContext.loweredRules...)
		}

		// "div { @media screen { &:hover { color: red } } }" "@media screen { div:hover { color: red } }"
		if len(childContext.loweredRules) > 0 {
			r.Rules = childContext.loweredRules
			context.loweredRules = append(context.loweredRules, rule)
		}

		return css_ast.Rule{}

	case *css_ast.RAtLayer:
		// Lower all children and filter out ones that become empty
		childContext := lowerNestingContext{
			parentSelectorsWithPseudo: context.parentSelectorsWithPseudo,
			parentSelectorsNoPseudo:   context.parentSelectorsNoPseudo,
		}
		r.Rules = p.lowerNestingInRulesAndReturnRemaining(r.Rules, &childContext)

		// "div { @layer foo { color: red } }" "@layer foo { div { color: red } }"
		if len(r.Rules) > 0 {
			childContext.loweredRules = append([]css_ast.Rule{{Loc: rule.Loc, Data: &css_ast.RSelector{
				Selectors: context.parentSelectorsWithPseudo,
				Rules:     r.Rules,
			}}}, childContext.loweredRules...)
		}

		// "div { @layer foo { &:hover { color: red } } }" "@layer foo { div:hover { color: red } }"
		// "div { @layer foo {} }" => "@layer foo {}" (layers have side effects, so don't remove empty ones)
		r.Rules = childContext.loweredRules
		context.loweredRules = append(context.loweredRules, rule)
		return css_ast.Rule{}
	}

	return rule
}

type leadingCombinatorStrip uint8

const (
	keepLeadingCombinator leadingCombinatorStrip = iota
	stripLeadingCombinator
)

func (p *parser) substituteAmpersandsInCompoundSelector(
	sel css_ast.CompoundSelector,
	replacementFn func(logger.Loc) css_ast.ComplexSelector,
	results []css_ast.CompoundSelector,
	strip leadingCombinatorStrip,
) []css_ast.CompoundSelector {
	for _, nestingSelectorLoc := range sel.NestingSelectorLocs {
		replacement := replacementFn(nestingSelectorLoc)

		// Convert the replacement to a single compound selector
		var single css_ast.CompoundSelector
		if sel.Combinator.Byte == 0 && (len(replacement.Selectors) == 1 || len(results) == 0) {
			// ".foo { :hover & {} }" => ":hover .foo {}"
			// ".foo .bar { &:hover {} }" => ".foo .bar:hover {}"
			last := len(replacement.Selectors) - 1
			results = append(results, replacement.Selectors[:last]...)
			single = replacement.Selectors[last]
			if strip == stripLeadingCombinator {
				single.Combinator = css_ast.Combinator{}
			}
			sel.Combinator = single.Combinator
		} else if len(replacement.Selectors) == 1 {
			// ".foo { > &:hover {} }" => ".foo > .foo:hover {}"
			single = replacement.Selectors[0]
			if strip == stripLeadingCombinator {
				single.Combinator = css_ast.Combinator{}
			}
		} else {
			// ".foo .bar { :hover & {} }" => ":hover :is(.foo .bar) {}"
			// ".foo .bar { > &:hover {} }" => ".foo .bar > :is(.foo .bar):hover {}"
			p.reportNestingWithGeneratedPseudoClassIs(nestingSelectorLoc)
			single = css_ast.CompoundSelector{
				SubclassSelectors: []css_ast.SubclassSelector{{
					Range: logger.Range{Loc: nestingSelectorLoc},
					Data: &css_ast.SSPseudoClassWithSelectorList{
						Kind:      css_ast.PseudoClassIs,
						Selectors: []css_ast.ComplexSelector{replacement.Clone()},
					},
				}},
			}
		}

		var subclassSelectorPrefix []css_ast.SubclassSelector

		// Insert the type selector
		if single.TypeSelector != nil {
			if sel.TypeSelector != nil {
				p.reportNestingWithGeneratedPseudoClassIs(nestingSelectorLoc)
				subclassSelectorPrefix = append(subclassSelectorPrefix, css_ast.SubclassSelector{
					Range: sel.TypeSelector.Range(),
					Data: &css_ast.SSPseudoClassWithSelectorList{
						Kind:      css_ast.PseudoClassIs,
						Selectors: []css_ast.ComplexSelector{{Selectors: []css_ast.CompoundSelector{{TypeSelector: sel.TypeSelector}}}},
					},
				})
			}
			sel.TypeSelector = single.TypeSelector
		}

		// Insert the subclass selectors
		subclassSelectorPrefix = append(subclassSelectorPrefix, single.SubclassSelectors...)

		// Write the changes back
		if len(subclassSelectorPrefix) > 0 {
			sel.SubclassSelectors = append(subclassSelectorPrefix, sel.SubclassSelectors...)
		}
	}
	sel.NestingSelectorLocs = nil

	// "div { :is(&.foo) {} }" => ":is(div.foo) {}"
	for _, ss := range sel.SubclassSelectors {
		if class, ok := ss.Data.(*css_ast.SSPseudoClassWithSelectorList); ok {
			outer := make([]css_ast.ComplexSelector, 0, len(class.Selectors))
			for _, complex := range class.Selectors {
				inner := make([]css_ast.CompoundSelector, 0, len(complex.Selectors))
				for _, sel := range complex.Selectors {
					inner = p.substituteAmpersandsInCompoundSelector(sel, replacementFn, inner, stripLeadingCombinator)
				}
				outer = append(outer, css_ast.ComplexSelector{Selectors: inner})
			}
			class.Selectors = outer
		}
	}

	return append(results, sel)
}

// Turn the list of selectors into a single selector by wrapping lists
// without a single element with ":is(...)". Note that this may result
// in an empty ":is()" selector (which matches nothing).
func (p *parser) multipleComplexSelectorsToSingleComplexSelector(selectors []css_ast.ComplexSelector) func(logger.Loc) css_ast.ComplexSelector {
	if len(selectors) == 1 {
		return func(logger.Loc) css_ast.ComplexSelector {
			return selectors[0]
		}
	}

	var leadingCombinator css_ast.Combinator
	clones := make([]css_ast.ComplexSelector, len(selectors))

	for i, sel := range selectors {
		// "> a, > b" => "> :is(a, b)" (the caller should have already checked that all leading combinators are the same)
		leadingCombinator = sel.Selectors[0].Combinator
		clones[i] = sel.Clone()
	}

	return func(loc logger.Loc) css_ast.ComplexSelector {
		return css_ast.ComplexSelector{
			Selectors: []css_ast.CompoundSelector{{
				Combinator: leadingCombinator,
				SubclassSelectors: []css_ast.SubclassSelector{{
					Range: logger.Range{Loc: loc},
					Data: &css_ast.SSPseudoClassWithSelectorList{
						Kind:      css_ast.PseudoClassIs,
						Selectors: clones,
					},
				}},
			}},
		}
	}
}

func (p *parser) reportNestingWithGeneratedPseudoClassIs(nestingSelectorLoc logger.Loc) {
	if p.options.unsupportedCSSFeatures.Has(compat.IsPseudoClass) {
		_, didWarn := p.nestingWarnings[nestingSelectorLoc]
		if didWarn {
			// Only warn at each location once
			return
		}
		if p.nestingWarnings == nil {
			p.nestingWarnings = make(map[logger.Loc]struct{})
		}
		p.nestingWarnings[nestingSelectorLoc] = struct{}{}
		text := "Transforming this CSS nesting syntax is not supported in the configured target environment"
		if p.options.originalTargetEnv != "" {
			text = fmt.Sprintf("%s (%s)", text, p.options.originalTargetEnv)
		}
		r := logger.Range{Loc: nestingSelectorLoc, Len: 1}
		p.log.AddIDWithNotes(logger.MsgID_CSS_UnsupportedCSSNesting, logger.Warning, &p.tracker, r, text, []logger.MsgData{{
			Text: "The nesting transform for this case must generate an \":is(...)\" but the configured target environment does not support the \":is\" pseudo-class."}})
	}
}
