package css_parser

import (
	"strings"

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

func (p *parser) commaToken(loc logger.Loc) css_ast.Token {
	t := css_ast.Token{
		Loc:  loc,
		Kind: css_lexer.TComma,
		Text: ",",
	}
	if !p.options.minifyWhitespace {
		t.Whitespace = css_ast.WhitespaceAfter
	}
	return t
}

func expandTokenQuad(tokens []css_ast.Token, allowedIdent string) (result [4]css_ast.Token, ok bool) {
	n := len(tokens)
	if n < 1 || n > 4 {
		return
	}

	// Don't do this if we encounter any unexpected tokens such as "var()"
	for i := 0; i < n; i++ {
		if t := tokens[i]; !t.Kind.IsNumeric() && (t.Kind != css_lexer.TIdent || allowedIdent == "" || t.Text != allowedIdent) {
			return
		}
	}

	result[0] = tokens[0]
	if n > 1 {
		result[1] = tokens[1]
	} else {
		result[1] = result[0]
	}
	if n > 2 {
		result[2] = tokens[2]
	} else {
		result[2] = result[0]
	}
	if n > 3 {
		result[3] = tokens[3]
	} else {
		result[3] = result[1]
	}

	ok = true
	return
}

func compactTokenQuad(a css_ast.Token, b css_ast.Token, c css_ast.Token, d css_ast.Token, minifyWhitespace bool) []css_ast.Token {
	tokens := []css_ast.Token{a, b, c, d}
	if tokens[3].EqualIgnoringWhitespace(tokens[1]) {
		if tokens[2].EqualIgnoringWhitespace(tokens[0]) {
			if tokens[1].EqualIgnoringWhitespace(tokens[0]) {
				tokens = tokens[:1]
			} else {
				tokens = tokens[:2]
			}
		} else {
			tokens = tokens[:3]
		}
	}
	for i := range tokens {
		var whitespace css_ast.WhitespaceFlags
		if !minifyWhitespace || i > 0 {
			whitespace |= css_ast.WhitespaceBefore
		}
		if i+1 < len(tokens) {
			whitespace |= css_ast.WhitespaceAfter
		}
		tokens[i].Whitespace = whitespace
	}
	return tokens
}

func (p *parser) processDeclarations(rules []css_ast.Rule, composesContext *composesContext) (rewrittenRules []css_ast.Rule) {
	margin := boxTracker{key: css_ast.DMargin, keyText: "margin", allowAuto: true}
	padding := boxTracker{key: css_ast.DPadding, keyText: "padding", allowAuto: false}
	inset := boxTracker{key: css_ast.DInset, keyText: "inset", allowAuto: true}
	borderRadius := borderRadiusTracker{}
	rewrittenRules = make([]css_ast.Rule, 0, len(rules))
	didWarnAboutComposes := false
	wouldClipColorFlag := false
	var declarationKeys map[string]struct{}

	// Don't automatically generate the "inset" property if it's not supported
	if p.options.unsupportedCSSFeatures.Has(compat.InsetProperty) {
		inset.key = css_ast.DUnknown
		inset.keyText = ""
	}

	// If this is a local class selector, track which CSS properties it declares.
	// This is used to warn when CSS "composes" is used incorrectly.
	if composesContext != nil {
		for _, ref := range composesContext.parentRefs {
			composes, ok := p.composes[ref]
			if !ok {
				composes = &css_ast.Composes{}
				p.composes[ref] = composes
			}
			properties := composes.Properties
			if properties == nil {
				properties = make(map[string]logger.Loc)
				composes.Properties = properties
			}
			for _, rule := range rules {
				if decl, ok := rule.Data.(*css_ast.RDeclaration); ok && decl.Key != css_ast.DComposes {
					properties[decl.KeyText] = decl.KeyRange.Loc
				}
			}
		}
	}

	for i := 0; i < len(rules); i++ {
		rule := rules[i]
		rewrittenRules = append(rewrittenRules, rule)
		decl, ok := rule.Data.(*css_ast.RDeclaration)
		if !ok {
			continue
		}

		// If the previous loop iteration would have clipped a color, we will
		// duplicate it and insert the clipped copy before the unclipped copy
		var wouldClipColor *bool
		if wouldClipColorFlag {
			wouldClipColorFlag = false
			clone := *decl
			clone.Value = css_ast.CloneTokensWithoutImportRecords(clone.Value)
			decl = &clone
			rule.Data = decl
			n := len(rewrittenRules) - 2
			rewrittenRules = append(rewrittenRules[:n], rule, rewrittenRules[n])
		} else {
			wouldClipColor = &wouldClipColorFlag
		}

		switch decl.Key {
		case css_ast.DComposes:
			// Only process "composes" directives if we're in "local-css" or
			// "global-css" mode. In these cases, "composes" directives will always
			// be removed (because they are being processed) even if they contain
			// errors. Otherwise we leave "composes" directives there untouched and
			// don't check them for errors.
			if p.options.symbolMode != symbolModeDisabled {
				if composesContext == nil {
					if !didWarnAboutComposes {
						didWarnAboutComposes = true
						p.log.AddID(logger.MsgID_CSS_CSSSyntaxError, logger.Warning, &p.tracker, decl.KeyRange, "\"composes\" is not valid here")
					}
				} else if composesContext.problemRange.Len > 0 {
					if !didWarnAboutComposes {
						didWarnAboutComposes = true
						p.log.AddIDWithNotes(logger.MsgID_CSS_CSSSyntaxError, logger.Warning, &p.tracker, decl.KeyRange, "\"composes\" only works inside single class selectors",
							[]logger.MsgData{p.tracker.MsgData(composesContext.problemRange, "The parent selector is not a single class selector because of the syntax here:")})
					}
				} else {
					p.handleComposesPragma(*composesContext, decl.Value)
				}
				rewrittenRules = rewrittenRules[:len(rewrittenRules)-1]
			}

		case css_ast.DBackground:
			for i, t := range decl.Value {
				t = p.lowerAndMinifyColor(t, wouldClipColor)
				t = p.lowerAndMinifyGradient(t, wouldClipColor)
				decl.Value[i] = t
			}

		case css_ast.DBackgroundImage,
			css_ast.DBorderImage,
			css_ast.DMaskImage:

			for i, t := range decl.Value {
				t = p.lowerAndMinifyGradient(t, wouldClipColor)
				decl.Value[i] = t
			}

		case css_ast.DBackgroundColor,
			css_ast.DBorderBlockEndColor,
			css_ast.DBorderBlockStartColor,
			css_ast.DBorderBottomColor,
			css_ast.DBorderColor,
			css_ast.DBorderInlineEndColor,
			css_ast.DBorderInlineStartColor,
			css_ast.DBorderLeftColor,
			css_ast.DBorderRightColor,
			css_ast.DBorderTopColor,
			css_ast.DCaretColor,
			css_ast.DColor,
			css_ast.DColumnRuleColor,
			css_ast.DFill,
			css_ast.DFloodColor,
			css_ast.DLightingColor,
			css_ast.DOutlineColor,
			css_ast.DStopColor,
			css_ast.DStroke,
			css_ast.DTextDecorationColor,
			css_ast.DTextEmphasisColor:

			if len(decl.Value) == 1 {
				decl.Value[0] = p.lowerAndMinifyColor(decl.Value[0], wouldClipColor)
			}

		case css_ast.DTransform:
			if p.options.minifySyntax {
				decl.Value = p.mangleTransforms(decl.Value)
			}

		case css_ast.DBoxShadow:
			decl.Value = p.lowerAndMangleBoxShadows(decl.Value, wouldClipColor)

		// Container name
		case css_ast.DContainer:
			p.processContainerShorthand(decl.Value)
		case css_ast.DContainerName:
			p.processContainerName(decl.Value)

		// Animation name
		case css_ast.DAnimation:
			p.processAnimationShorthand(decl.Value)
		case css_ast.DAnimationName:
			p.processAnimationName(decl.Value)

		// List style
		case css_ast.DListStyle:
			p.processListStyleShorthand(decl.Value)
		case css_ast.DListStyleType:
			if len(decl.Value) == 1 {
				p.processListStyleType(&decl.Value[0])
			}

		// Font
		case css_ast.DFont:
			if p.options.minifySyntax {
				decl.Value = p.mangleFont(decl.Value)
			}
		case css_ast.DFontFamily:
			if p.options.minifySyntax {
				if value, ok := p.mangleFontFamily(decl.Value); ok {
					decl.Value = value
				}
			}
		case css_ast.DFontWeight:
			if len(decl.Value) == 1 && p.options.minifySyntax {
				decl.Value[0] = p.mangleFontWeight(decl.Value[0])
			}

		// Margin
		case css_ast.DMargin:
			if p.options.minifySyntax {
				margin.mangleSides(rewrittenRules, decl, p.options.minifyWhitespace)
			}
		case css_ast.DMarginTop:
			if p.options.minifySyntax {
				margin.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxTop)
			}
		case css_ast.DMarginRight:
			if p.options.minifySyntax {
				margin.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxRight)
			}
		case css_ast.DMarginBottom:
			if p.options.minifySyntax {
				margin.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxBottom)
			}
		case css_ast.DMarginLeft:
			if p.options.minifySyntax {
				margin.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxLeft)
			}

		// Padding
		case css_ast.DPadding:
			if p.options.minifySyntax {
				padding.mangleSides(rewrittenRules, decl, p.options.minifyWhitespace)
			}
		case css_ast.DPaddingTop:
			if p.options.minifySyntax {
				padding.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxTop)
			}
		case css_ast.DPaddingRight:
			if p.options.minifySyntax {
				padding.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxRight)
			}
		case css_ast.DPaddingBottom:
			if p.options.minifySyntax {
				padding.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxBottom)
			}
		case css_ast.DPaddingLeft:
			if p.options.minifySyntax {
				padding.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxLeft)
			}

		// Inset
		case css_ast.DInset:
			if p.options.unsupportedCSSFeatures.Has(compat.InsetProperty) {
				if decls, ok := p.lowerInset(rule.Loc, decl); ok {
					rewrittenRules = rewrittenRules[:len(rewrittenRules)-1]
					for i := range decls {
						rewrittenRules = append(rewrittenRules, decls[i])
						if p.options.minifySyntax {
							inset.mangleSide(rewrittenRules, decls[i].Data.(*css_ast.RDeclaration), p.options.minifyWhitespace, i)
						}
					}
					break
				}
			}
			if p.options.minifySyntax {
				inset.mangleSides(rewrittenRules, decl, p.options.minifyWhitespace)
			}
		case css_ast.DTop:
			if p.options.minifySyntax {
				inset.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxTop)
			}
		case css_ast.DRight:
			if p.options.minifySyntax {
				inset.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxRight)
			}
		case css_ast.DBottom:
			if p.options.minifySyntax {
				inset.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxBottom)
			}
		case css_ast.DLeft:
			if p.options.minifySyntax {
				inset.mangleSide(rewrittenRules, decl, p.options.minifyWhitespace, boxLeft)
			}

		// Border radius
		case css_ast.DBorderRadius:
			if p.options.minifySyntax {
				borderRadius.mangleCorners(rewrittenRules, decl, p.options.minifyWhitespace)
			}
		case css_ast.DBorderTopLeftRadius:
			if p.options.minifySyntax {
				borderRadius.mangleCorner(rewrittenRules, decl, p.options.minifyWhitespace, borderRadiusTopLeft)
			}
		case css_ast.DBorderTopRightRadius:
			if p.options.minifySyntax {
				borderRadius.mangleCorner(rewrittenRules, decl, p.options.minifyWhitespace, borderRadiusTopRight)
			}
		case css_ast.DBorderBottomRightRadius:
			if p.options.minifySyntax {
				borderRadius.mangleCorner(rewrittenRules, decl, p.options.minifyWhitespace, borderRadiusBottomRight)
			}
		case css_ast.DBorderBottomLeftRadius:
			if p.options.minifySyntax {
				borderRadius.mangleCorner(rewrittenRules, decl, p.options.minifyWhitespace, borderRadiusBottomLeft)
			}
		}

		if prefixes, ok := p.options.cssPrefixData[decl.Key]; ok {
			if declarationKeys == nil {
				// Only generate this map if it's needed
				declarationKeys = make(map[string]struct{})
				for _, rule := range rules {
					if decl, ok := rule.Data.(*css_ast.RDeclaration); ok {
						declarationKeys[decl.KeyText] = struct{}{}
					}
				}
			}
			if (prefixes & compat.WebkitPrefix) != 0 {
				rewrittenRules = p.insertPrefixedDeclaration(rewrittenRules, "-webkit-", rule.Loc, decl, declarationKeys)
			}
			if (prefixes & compat.KhtmlPrefix) != 0 {
				rewrittenRules = p.insertPrefixedDeclaration(rewrittenRules, "-khtml-", rule.Loc, decl, declarationKeys)
			}
			if (prefixes & compat.MozPrefix) != 0 {
				rewrittenRules = p.insertPrefixedDeclaration(rewrittenRules, "-moz-", rule.Loc, decl, declarationKeys)
			}
			if (prefixes & compat.MsPrefix) != 0 {
				rewrittenRules = p.insertPrefixedDeclaration(rewrittenRules, "-ms-", rule.Loc, decl, declarationKeys)
			}
			if (prefixes & compat.OPrefix) != 0 {
				rewrittenRules = p.insertPrefixedDeclaration(rewrittenRules, "-o-", rule.Loc, decl, declarationKeys)
			}
		}

		// If this loop iteration would have clipped a color, the out-of-gamut
		// colors will not be clipped and this flag will be set. We then set up the
		// next iteration of the loop to duplicate this rule and process it again
		// with color clipping enabled.
		if wouldClipColorFlag {
			if p.options.unsupportedCSSFeatures.Has(compat.ColorFunctions) {
				// Only do this if there was no previous instance of that property so
				// we avoid overwriting any manually-specified fallback values
				for j := len(rewrittenRules) - 2; j >= 0; j-- {
					if prev, ok := rewrittenRules[j].Data.(*css_ast.RDeclaration); ok && prev.Key == decl.Key {
						wouldClipColorFlag = false
						break
					}
				}
				if wouldClipColorFlag {
					// If the code above would have clipped a color outside of the sRGB gamut,
					// process this rule again so we can generate the clipped version next time
					i -= 1
					continue
				}
			}
			wouldClipColorFlag = false
		}
	}

	// Compact removed rules
	if p.options.minifySyntax {
		end := 0
		for _, rule := range rewrittenRules {
			if rule.Data != nil {
				rewrittenRules[end] = rule
				end++
			}
		}
		rewrittenRules = rewrittenRules[:end]
	}

	return
}

func (p *parser) insertPrefixedDeclaration(rules []css_ast.Rule, prefix string, loc logger.Loc, decl *css_ast.RDeclaration, declarationKeys map[string]struct{}) []css_ast.Rule {
	keyText := prefix + decl.KeyText

	// Don't insert a prefixed declaration if there already is one
	if _, ok := declarationKeys[keyText]; ok {
		// We found a previous declaration with a matching prefixed property.
		// The value is ignored, which matches the behavior of "autoprefixer".
		return rules
	}

	// Additional special cases for when the prefix applies
	switch decl.Key {
	case css_ast.DBackgroundClip:
		// The prefix is only needed for "background-clip: text"
		if len(decl.Value) != 1 || decl.Value[0].Kind != css_lexer.TIdent || !strings.EqualFold(decl.Value[0].Text, "text") {
			return rules
		}

	case css_ast.DPosition:
		// The prefix is only needed for "position: sticky"
		if len(decl.Value) != 1 || decl.Value[0].Kind != css_lexer.TIdent || !strings.EqualFold(decl.Value[0].Text, "sticky") {
			return rules
		}

	case css_ast.DWidth, css_ast.DMinWidth, css_ast.DMaxWidth,
		css_ast.DHeight, css_ast.DMinHeight, css_ast.DMaxHeight:
		// The prefix is only needed for "width: stretch"
		if len(decl.Value) != 1 || decl.Value[0].Kind != css_lexer.TIdent || !strings.EqualFold(decl.Value[0].Text, "stretch") {
			return rules
		}
	}

	value := css_ast.CloneTokensWithoutImportRecords(decl.Value)

	// Additional special cases for how to transform the contents
	switch decl.Key {
	case css_ast.DPosition:
		// The prefix applies to the value, not the property
		keyText = decl.KeyText
		value[0].Text = "-webkit-sticky"

	case css_ast.DWidth, css_ast.DMinWidth, css_ast.DMaxWidth,
		css_ast.DHeight, css_ast.DMinHeight, css_ast.DMaxHeight:
		// The prefix applies to the value, not the property
		keyText = decl.KeyText

		// This currently only applies to "stretch" (already checked above)
		switch prefix {
		case "-webkit-":
			value[0].Text = "-webkit-fill-available"
		case "-moz-":
			value[0].Text = "-moz-available"
		}

	case css_ast.DUserSelect:
		// The prefix applies to the value as well as the property
		if prefix == "-moz-" && len(value) == 1 && value[0].Kind == css_lexer.TIdent && strings.EqualFold(value[0].Text, "none") {
			value[0].Text = "-moz-none"
		}

	case css_ast.DMaskComposite:
		// WebKit uses different names for these values
		if prefix == "-webkit-" {
			for i, token := range value {
				if token.Kind == css_lexer.TIdent {
					switch token.Text {
					case "add":
						value[i].Text = "source-over"
					case "subtract":
						value[i].Text = "source-out"
					case "intersect":
						value[i].Text = "source-in"
					case "exclude":
						value[i].Text = "xor"
					}
				}
			}
		}
	}

	// If we didn't change the key, manually search for a previous duplicate rule
	if keyText == decl.KeyText {
		for _, rule := range rules {
			if prevDecl, ok := rule.Data.(*css_ast.RDeclaration); ok && prevDecl.KeyText == keyText && css_ast.TokensEqual(prevDecl.Value, value, nil) {
				return rules
			}
		}
	}

	// Overwrite the latest declaration with the prefixed declaration
	rules[len(rules)-1] = css_ast.Rule{Loc: loc, Data: &css_ast.RDeclaration{
		KeyText:   keyText,
		KeyRange:  decl.KeyRange,
		Value:     value,
		Important: decl.Important,
	}}

	// Re-add the latest declaration after the inserted declaration
	rules = append(rules, css_ast.Rule{Loc: loc, Data: decl})
	return rules
}

func (p *parser) lowerInset(loc logger.Loc, decl *css_ast.RDeclaration) ([]css_ast.Rule, bool) {
	if tokens, ok := expandTokenQuad(decl.Value, ""); ok {
		mask := ^css_ast.WhitespaceAfter
		if p.options.minifyWhitespace {
			mask = 0
		}
		for i := range tokens {
			tokens[i].Whitespace &= mask
		}
		return []css_ast.Rule{
			{Loc: loc, Data: &css_ast.RDeclaration{
				KeyText:   "top",
				KeyRange:  decl.KeyRange,
				Key:       css_ast.DTop,
				Value:     tokens[0:1],
				Important: decl.Important,
			}},
			{Loc: loc, Data: &css_ast.RDeclaration{
				KeyText:   "right",
				KeyRange:  decl.KeyRange,
				Key:       css_ast.DRight,
				Value:     tokens[1:2],
				Important: decl.Important,
			}},
			{Loc: loc, Data: &css_ast.RDeclaration{
				KeyText:   "bottom",
				KeyRange:  decl.KeyRange,
				Key:       css_ast.DBottom,
				Value:     tokens[2:3],
				Important: decl.Important,
			}},
			{Loc: loc, Data: &css_ast.RDeclaration{
				KeyText:   "left",
				KeyRange:  decl.KeyRange,
				Key:       css_ast.DLeft,
				Value:     tokens[3:4],
				Important: decl.Important,
			}},
		}, true
	}
	return nil, false
}
