package css_parser

import (
	"fmt"
	"strings"

	"github.com/evanw/esbuild/internal/ast"
	"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"
)

// This is mostly a normal CSS parser with one exception: the addition of
// support for parsing https://drafts.csswg.org/css-nesting-1/.

type parser struct {
	log           logger.Log
	source        logger.Source
	options       Options
	tokens        []css_lexer.Token
	stack         []css_lexer.T
	index         int
	end           int
	prevError     logger.Loc
	importRecords []ast.ImportRecord
}

type Options struct {
	UnsupportedCSSFeatures compat.CSSFeature
	MangleSyntax           bool
}

func Parse(log logger.Log, source logger.Source, options Options) css_ast.AST {
	p := parser{
		log:       log,
		source:    source,
		options:   options,
		tokens:    css_lexer.Tokenize(log, source),
		prevError: logger.Loc{Start: -1},
	}
	p.end = len(p.tokens)
	tree := css_ast.AST{}
	tree.Rules = p.parseListOfRules(ruleContext{
		isTopLevel:     true,
		parseSelectors: true,
	})
	tree.ImportRecords = p.importRecords
	p.expect(css_lexer.TEndOfFile)
	return tree
}

func (p *parser) advance() {
	if p.index < p.end {
		p.index++
	}
}

func (p *parser) at(index int) css_lexer.Token {
	if index < p.end {
		return p.tokens[index]
	}
	if p.end < len(p.tokens) {
		return css_lexer.Token{
			Kind:  css_lexer.TEndOfFile,
			Range: logger.Range{Loc: p.tokens[p.end].Range.Loc},
		}
	}
	return css_lexer.Token{
		Kind:  css_lexer.TEndOfFile,
		Range: logger.Range{Loc: logger.Loc{Start: int32(len(p.source.Contents))}},
	}
}

func (p *parser) current() css_lexer.Token {
	return p.at(p.index)
}

func (p *parser) next() css_lexer.Token {
	return p.at(p.index + 1)
}

func (p *parser) raw() string {
	t := p.current()
	return p.source.Contents[t.Range.Loc.Start:t.Range.End()]
}

func (p *parser) decoded() string {
	return p.current().DecodedText(p.source.Contents)
}

func (p *parser) peek(kind css_lexer.T) bool {
	return kind == p.current().Kind
}

func (p *parser) eat(kind css_lexer.T) bool {
	if p.peek(kind) {
		p.advance()
		return true
	}
	return false
}

func (p *parser) expect(kind css_lexer.T) bool {
	if p.eat(kind) {
		return true
	}
	t := p.current()
	var text string
	if kind == css_lexer.TSemicolon && p.index > 0 && p.at(p.index-1).Kind == css_lexer.TWhitespace {
		// Have a nice error message for forgetting a trailing semicolon
		text = "Expected \";\""
		t = p.at(p.index - 1)
	} else {
		switch t.Kind {
		case css_lexer.TEndOfFile, css_lexer.TWhitespace:
			text = fmt.Sprintf("Expected %s but found %s", kind.String(), t.Kind.String())
			t.Range.Len = 0
		case css_lexer.TBadURL, css_lexer.TBadString:
			text = fmt.Sprintf("Expected %s but found %s", kind.String(), t.Kind.String())
		default:
			text = fmt.Sprintf("Expected %s but found %q", kind.String(), p.raw())
		}
	}
	if t.Range.Loc.Start > p.prevError.Start {
		p.log.AddRangeWarning(&p.source, t.Range, text)
		p.prevError = t.Range.Loc
	}
	return false
}

func (p *parser) unexpected() {
	if t := p.current(); t.Range.Loc.Start > p.prevError.Start {
		var text string
		switch t.Kind {
		case css_lexer.TEndOfFile, css_lexer.TWhitespace:
			text = fmt.Sprintf("Unexpected %s", t.Kind.String())
			t.Range.Len = 0
		case css_lexer.TBadURL, css_lexer.TBadString:
			text = fmt.Sprintf("Unexpected %s", t.Kind.String())
		default:
			text = fmt.Sprintf("Unexpected %q", p.raw())
		}
		p.log.AddRangeWarning(&p.source, t.Range, text)
		p.prevError = t.Range.Loc
	}
}

type ruleContext struct {
	isTopLevel     bool
	parseSelectors bool
}

func (p *parser) parseListOfRules(context ruleContext) []css_ast.R {
	rules := []css_ast.R{}
	for {
		switch p.current().Kind {
		case css_lexer.TEndOfFile, css_lexer.TCloseBrace:
			return rules

		case css_lexer.TWhitespace:
			p.advance()
			continue

		case css_lexer.TAtKeyword:
			rules = append(rules, p.parseAtRule(atRuleContext{}))
			continue

		case css_lexer.TCDO, css_lexer.TCDC:
			if context.isTopLevel {
				p.advance()
				continue
			}
		}

		if context.parseSelectors {
			rules = append(rules, p.parseSelectorRule())
		} else {
			rules = append(rules, p.parseQualifiedRuleFrom(p.index, false /* isAlreadyInvalid */))
		}
	}
}

func (p *parser) parseListOfDeclarations() (list []css_ast.R) {
	for {
		switch p.current().Kind {
		case css_lexer.TWhitespace, css_lexer.TSemicolon:
			p.advance()

		case css_lexer.TEndOfFile, css_lexer.TCloseBrace:
			p.processDeclarations(list)
			return

		case css_lexer.TAtKeyword:
			list = append(list, p.parseAtRule(atRuleContext{
				isDeclarationList: true,
			}))

		case css_lexer.TDelimAmpersand:
			// Reference: https://drafts.csswg.org/css-nesting-1/
			list = append(list, p.parseSelectorRule())

		default:
			list = append(list, p.parseDeclaration())
		}
	}
}

func (p *parser) parseURLOrString() (string, logger.Range, bool) {
	t := p.current()
	switch t.Kind {
	case css_lexer.TString:
		text := p.decoded()
		p.advance()
		return text, t.Range, true

	case css_lexer.TURL:
		text := p.decoded()
		p.advance()
		return text, t.Range, true

	case css_lexer.TFunction:
		if p.decoded() == "url" {
			p.advance()
			t = p.current()
			text := p.decoded()
			if p.expect(css_lexer.TString) && p.expect(css_lexer.TCloseParen) {
				return text, t.Range, true
			}
		}
	}

	return "", logger.Range{}, false
}

func (p *parser) expectURLOrString() (url string, r logger.Range, ok bool) {
	url, r, ok = p.parseURLOrString()
	if !ok {
		p.expect(css_lexer.TURL)
	}
	return
}

type atRuleKind uint8

const (
	atRuleUnknown atRuleKind = iota
	atRuleDeclarations
	atRuleInheritContext
	atRuleEmpty
)

var specialAtRules = map[string]atRuleKind{
	"font-face": atRuleDeclarations,
	"page":      atRuleDeclarations,

	"document": atRuleInheritContext,
	"media":    atRuleInheritContext,
	"scope":    atRuleInheritContext,
	"supports": atRuleInheritContext,
}

type atRuleContext struct {
	isDeclarationList bool
}

func (p *parser) parseAtRule(context atRuleContext) css_ast.R {
	// Parse the name
	atToken := p.decoded()
	atRange := p.current().Range
	kind := specialAtRules[atToken]
	p.advance()

	// Parse the prelude
	preludeStart := p.index
	switch atToken {
	case "charset":
		kind = atRuleEmpty
		p.expect(css_lexer.TWhitespace)
		if p.peek(css_lexer.TString) {
			encoding := p.decoded()
			if encoding != "UTF-8" {
				p.log.AddRangeWarning(&p.source, p.current().Range,
					fmt.Sprintf("\"UTF-8\" will be used instead of unsupported charset %q", encoding))
			}
			p.advance()
			p.expect(css_lexer.TSemicolon)
			return &css_ast.RAtCharset{Encoding: encoding}
		}
		p.expect(css_lexer.TString)

	case "namespace":
		kind = atRuleEmpty
		p.eat(css_lexer.TWhitespace)
		prefix := ""
		if p.peek(css_lexer.TIdent) {
			prefix = p.decoded()
			p.advance()
			p.eat(css_lexer.TWhitespace)
		}
		if path, _, ok := p.expectURLOrString(); ok {
			p.eat(css_lexer.TWhitespace)
			p.expect(css_lexer.TSemicolon)
			return &css_ast.RAtNamespace{Prefix: prefix, Path: path}
		}

	case "import":
		kind = atRuleEmpty
		p.eat(css_lexer.TWhitespace)
		if path, r, ok := p.expectURLOrString(); ok {
			p.eat(css_lexer.TWhitespace)
			p.expect(css_lexer.TSemicolon)
			importRecordIndex := uint32(len(p.importRecords))
			p.importRecords = append(p.importRecords, ast.ImportRecord{
				Kind:  ast.ImportAt,
				Path:  logger.Path{Text: path},
				Range: r,
			})
			return &css_ast.RAtImport{ImportRecordIndex: importRecordIndex}
		}

	case "keyframes", "-webkit-keyframes", "-moz-keyframes", "-ms-keyframes", "-o-keyframes":
		p.eat(css_lexer.TWhitespace)
		var name string

		if p.peek(css_lexer.TIdent) {
			name = p.decoded()
			p.advance()
		} else if !p.expect(css_lexer.TIdent) && !p.eat(css_lexer.TString) && !p.peek(css_lexer.TOpenBrace) {
			// Consider string names a syntax error even though they are allowed by
			// the specification and they work in Firefox because they do not work in
			// Chrome or Safari.
			break
		}

		p.eat(css_lexer.TWhitespace)
		if p.expect(css_lexer.TOpenBrace) {
			var blocks []css_ast.KeyframeBlock

		blocks:
			for {
				switch p.current().Kind {
				case css_lexer.TWhitespace:
					p.advance()
					continue

				case css_lexer.TCloseBrace, css_lexer.TEndOfFile:
					break blocks

				case css_lexer.TOpenBrace:
					p.expect(css_lexer.TPercentage)
					p.parseComponentValue()

				default:
					var selectors []string

				selectors:
					for {
						t := p.current()
						switch t.Kind {
						case css_lexer.TWhitespace:
							p.advance()
							continue

						case css_lexer.TOpenBrace, css_lexer.TEndOfFile:
							break selectors

						case css_lexer.TIdent, css_lexer.TPercentage:
							text := p.decoded()
							if t.Kind == css_lexer.TIdent {
								if text == "from" {
									if p.options.MangleSyntax {
										text = "0%" // "0%" is equivalent to but shorter than "from"
									}
								} else if text != "to" {
									p.expect(css_lexer.TPercentage)
								}
							} else if p.options.MangleSyntax && text == "100%" {
								text = "to" // "to" is equivalent to but shorter than "100%"
							}
							selectors = append(selectors, text)
							p.advance()

						default:
							p.expect(css_lexer.TPercentage)
							p.parseComponentValue()
						}

						p.eat(css_lexer.TWhitespace)
						if t.Kind != css_lexer.TComma && !p.peek(css_lexer.TOpenBrace) {
							p.expect(css_lexer.TComma)
						}
					}

					if p.expect(css_lexer.TOpenBrace) {
						rules := p.parseListOfDeclarations()
						p.expect(css_lexer.TCloseBrace)
						blocks = append(blocks, css_ast.KeyframeBlock{
							Selectors: selectors,
							Rules:     rules,
						})
					}
				}
			}

			p.expect(css_lexer.TCloseBrace)
			return &css_ast.RAtKeyframes{
				AtToken: atToken,
				Name:    name,
				Blocks:  blocks,
			}
		}

	default:
		// Warn about unsupported at-rules since they will be passed through
		// unmodified and may be part of a CSS preprocessor syntax that should
		// have been compiled away but wasn't.
		//
		// The list of supported at-rules that esbuild draws from is here:
		// https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule. Deprecated
		// and Firefox-only at-rules have been removed.
		if kind == atRuleUnknown {
			p.log.AddRangeWarning(&p.source, atRange, fmt.Sprintf("%q is not a known rule name", "@"+atToken))
		}
	}

	// Parse an unknown prelude
prelude:
	for {
		switch p.current().Kind {
		case css_lexer.TOpenBrace, css_lexer.TEndOfFile:
			break prelude

		case css_lexer.TSemicolon, css_lexer.TCloseBrace:
			prelude := p.convertTokens(p.tokens[preludeStart:p.index])

			// Report an error for rules that should have blocks
			if kind != atRuleEmpty && kind != atRuleUnknown {
				p.expect(css_lexer.TOpenBrace)
				p.eat(css_lexer.TSemicolon)
				return &css_ast.RUnknownAt{AtToken: atToken, Prelude: prelude}
			}

			// Otherwise, parse an unknown at rule
			p.expect(css_lexer.TSemicolon)
			return &css_ast.RUnknownAt{AtToken: atToken, Prelude: prelude}

		default:
			p.parseComponentValue()
		}
	}
	prelude := p.convertTokens(p.tokens[preludeStart:p.index])
	blockStart := p.index

	switch kind {
	case atRuleEmpty:
		// Report an error for rules that shouldn't have blocks
		p.expect(css_lexer.TSemicolon)
		p.parseBlock(css_lexer.TOpenBrace, css_lexer.TCloseBrace)
		block := p.convertTokens(p.tokens[blockStart:p.index])
		return &css_ast.RUnknownAt{AtToken: atToken, Prelude: prelude, Block: block}

	case atRuleDeclarations:
		// Parse known rules whose blocks consist of whatever the current context is
		p.advance()
		rules := p.parseListOfDeclarations()
		p.expect(css_lexer.TCloseBrace)
		return &css_ast.RKnownAt{AtToken: atToken, Prelude: prelude, Rules: rules}

	case atRuleInheritContext:
		// Parse known rules whose blocks consist of whatever the current context is
		p.advance()
		var rules []css_ast.R
		if context.isDeclarationList {
			rules = p.parseListOfDeclarations()
		} else {
			rules = p.parseListOfRules(ruleContext{
				parseSelectors: true,
			})
		}
		p.expect(css_lexer.TCloseBrace)
		return &css_ast.RKnownAt{AtToken: atToken, Prelude: prelude, Rules: rules}

	default:
		// Otherwise, parse an unknown rule
		p.parseBlock(css_lexer.TOpenBrace, css_lexer.TCloseBrace)
		block := p.convertTokensWithImports(p.tokens[blockStart:p.index])
		return &css_ast.RUnknownAt{AtToken: atToken, Prelude: prelude, Block: block}
	}
}

func (p *parser) convertTokens(tokens []css_lexer.Token) []css_ast.Token {
	result, _ := p.convertTokensHelper(tokens, css_lexer.TEndOfFile, false)
	return result
}

func (p *parser) convertTokensWithImports(tokens []css_lexer.Token) []css_ast.Token {
	result, _ := p.convertTokensHelper(tokens, css_lexer.TEndOfFile, true)
	return result
}

func (p *parser) convertTokensHelper(tokens []css_lexer.Token, close css_lexer.T, allowImports bool) ([]css_ast.Token, []css_lexer.Token) {
	var result []css_ast.Token
loop:
	for len(tokens) > 0 {
		t := tokens[0]
		tokens = tokens[1:]
		token := css_ast.Token{
			Kind: t.Kind,
			Text: t.DecodedText(p.source.Contents),
		}

		switch t.Kind {
		case css_lexer.TWhitespace:
			if last := len(result) - 1; last >= 0 {
				result[last].HasWhitespaceAfter = true
			}
			continue

		case css_lexer.TComma:
			// Assume that whitespace can always be removed before a comma
			if last := len(result) - 1; last >= 0 {
				result[last].HasWhitespaceAfter = false
			}

			// Assume whitespace can always be added after a comma (it will be
			// automatically omitted by the printer if we're minifying)
			token.HasWhitespaceAfter = true

		case css_lexer.TNumber:
			if p.options.MangleSyntax {
				if text, ok := mangleNumber(token.Text); ok {
					token.Text = text
				}
			}

		case css_lexer.TPercentage:
			if p.options.MangleSyntax {
				if text, ok := mangleNumber(token.PercentValue()); ok {
					token.Text = text + "%"
				}
			}

		case css_lexer.TDimension:
			token.UnitOffset = t.UnitOffset

			if p.options.MangleSyntax {
				if text, ok := mangleNumber(token.DimensionValue()); ok {
					token.Text = text + token.DimensionUnit()
					token.UnitOffset = uint16(len(text))
				}
			}

		case css_lexer.TURL:
			token.ImportRecordIndex = uint32(len(p.importRecords))
			p.importRecords = append(p.importRecords, ast.ImportRecord{
				Kind:     ast.ImportURL,
				Path:     logger.Path{Text: token.Text},
				Range:    t.Range,
				IsUnused: !allowImports,
			})
			token.Text = ""

		case css_lexer.TFunction:
			var nested []css_ast.Token
			original := tokens
			nested, tokens = p.convertTokensHelper(tokens, css_lexer.TCloseParen, allowImports)
			token.Children = &nested

			// Treat a URL function call with a string just like a URL token
			if token.Text == "url" && len(nested) == 1 && nested[0].Kind == css_lexer.TString {
				token.Kind = css_lexer.TURL
				token.Text = ""
				token.Children = nil
				token.ImportRecordIndex = uint32(len(p.importRecords))
				p.importRecords = append(p.importRecords, ast.ImportRecord{
					Kind:     ast.ImportURL,
					Path:     logger.Path{Text: nested[0].Text},
					Range:    original[0].Range,
					IsUnused: !allowImports,
				})
			}

		case css_lexer.TOpenParen:
			var nested []css_ast.Token
			nested, tokens = p.convertTokensHelper(tokens, css_lexer.TCloseParen, allowImports)
			token.Children = &nested

		case css_lexer.TOpenBrace:
			var nested []css_ast.Token
			nested, tokens = p.convertTokensHelper(tokens, css_lexer.TCloseBrace, allowImports)
			token.Children = &nested

		case css_lexer.TOpenBracket:
			var nested []css_ast.Token
			nested, tokens = p.convertTokensHelper(tokens, css_lexer.TCloseBracket, allowImports)
			token.Children = &nested

		default:
			if t.Kind == close {
				break loop
			}
		}

		result = append(result, token)
	}
	return result, tokens
}

func mangleNumber(t string) (string, bool) {
	original := t

	if dot := strings.IndexByte(t, '.'); dot != -1 {
		// Remove trailing zeros
		for len(t) > 0 && t[len(t)-1] == '0' {
			t = t[:len(t)-1]
		}

		// Remove the decimal point if it's unnecessary
		if dot+1 == len(t) {
			t = t[:dot]
		} else {
			// Remove a leading zero
			if len(t) >= 3 && t[0] == '0' && t[1] == '.' && t[2] >= '0' && t[2] <= '9' {
				t = t[1:]
			} else if len(t) >= 4 && (t[0] == '+' || t[0] == '-') && t[1] == '0' && t[2] == '.' && t[3] >= '0' && t[3] <= '9' {
				t = t[0:1] + t[2:]
			}
		}
	}

	return t, t != original
}

func (p *parser) parseSelectorRule() css_ast.R {
	preludeStart := p.index

	// Try parsing the prelude as a selector list
	if list, ok := p.parseSelectorList(); ok {
		rule := css_ast.RSelector{Selectors: list}
		if p.expect(css_lexer.TOpenBrace) {
			rule.Rules = p.parseListOfDeclarations()
			p.expect(css_lexer.TCloseBrace)
			return &rule
		}
	}

	// Otherwise, parse a generic qualified rule
	return p.parseQualifiedRuleFrom(preludeStart, true /* isAlreadyInvalid */)
}

func (p *parser) parseQualifiedRuleFrom(preludeStart int, isAlreadyInvalid bool) *css_ast.RQualified {
loop:
	for {
		switch p.current().Kind {
		case css_lexer.TOpenBrace, css_lexer.TEndOfFile:
			break loop

		case css_lexer.TSemicolon:
			// Error recovery if the block is omitted (likely some CSS meta-syntax)
			if !isAlreadyInvalid {
				p.expect(css_lexer.TOpenBrace)
			}
			prelude := p.convertTokens(p.tokens[preludeStart:p.index])
			p.advance()
			return &css_ast.RQualified{Prelude: prelude}

		default:
			p.parseComponentValue()
		}
	}

	rule := css_ast.RQualified{
		Prelude: p.convertTokens(p.tokens[preludeStart:p.index]),
	}

	if p.eat(css_lexer.TOpenBrace) {
		rule.Rules = p.parseListOfDeclarations()
		p.expect(css_lexer.TCloseBrace)
	} else if !isAlreadyInvalid {
		p.expect(css_lexer.TOpenBrace)
	}

	return &rule
}

func (p *parser) parseDeclaration() css_ast.R {
	// Parse the key
	keyStart := p.index
	ok := false
	if p.expect(css_lexer.TIdent) {
		p.eat(css_lexer.TWhitespace)
		if p.expect(css_lexer.TColon) {
			ok = true
		}
	}

	// Parse the value
	valueStart := p.index
stop:
	for {
		switch p.current().Kind {
		case css_lexer.TEndOfFile, css_lexer.TSemicolon, css_lexer.TCloseBrace:
			break stop

		case css_lexer.TOpenBrace:
			// Error recovery if there is an unexpected block (likely some CSS meta-syntax)
			p.parseComponentValue()
			p.eat(css_lexer.TWhitespace)
			if ok && !p.peek(css_lexer.TSemicolon) {
				p.expect(css_lexer.TSemicolon)
			}
			break stop

		default:
			p.parseComponentValue()
		}
	}

	// Stop now if this is not a valid declaration
	if !ok {
		return &css_ast.RBadDeclaration{
			Tokens: p.convertTokens(p.tokens[keyStart:p.index]),
		}
	}

	// Remove leading and trailing whitespace from the value
	value := trimWhitespace(p.tokens[valueStart:p.index])

	// Remove trailing "!important"
	important := false
	if last := len(value) - 1; last >= 0 {
		if t := value[last]; t.Kind == css_lexer.TIdent && strings.EqualFold(t.DecodedText(p.source.Contents), "important") {
			i := len(value) - 2
			if i >= 0 && value[i].Kind == css_lexer.TWhitespace {
				i--
			}
			if i >= 0 && value[i].Kind == css_lexer.TDelimExclamation {
				if i >= 1 && value[i-1].Kind == css_lexer.TWhitespace {
					i--
				}
				value = value[:i]
				important = true
			}
		}
	}

	keyToken := p.tokens[keyStart]
	keyText := keyToken.DecodedText(p.source.Contents)
	return &css_ast.RDeclaration{
		Key:       css_ast.KnownDeclarations[keyText],
		KeyText:   keyText,
		KeyRange:  keyToken.Range,
		Value:     p.convertTokensWithImports(value),
		Important: important,
	}
}

func (p *parser) parseComponentValue() {
	switch p.current().Kind {
	case css_lexer.TFunction:
		p.parseBlock(css_lexer.TFunction, css_lexer.TCloseParen)

	case css_lexer.TOpenParen:
		p.parseBlock(css_lexer.TOpenParen, css_lexer.TCloseParen)

	case css_lexer.TOpenBrace:
		p.parseBlock(css_lexer.TOpenBrace, css_lexer.TCloseBrace)

	case css_lexer.TOpenBracket:
		p.parseBlock(css_lexer.TOpenBracket, css_lexer.TCloseBracket)

	case css_lexer.TEndOfFile:
		p.unexpected()

	default:
		p.advance()
	}
}

func (p *parser) parseBlock(open css_lexer.T, close css_lexer.T) {
	if p.expect(open) {
		for !p.eat(close) {
			if p.peek(css_lexer.TEndOfFile) {
				p.expect(close)
				return
			}

			p.parseComponentValue()
		}
	}
}

func trimWhitespace(tokens []css_lexer.Token) []css_lexer.Token {
	if len(tokens) > 0 && tokens[0].Kind == css_lexer.TWhitespace {
		tokens = tokens[1:]
	}
	if i := len(tokens) - 1; i >= 0 && tokens[i].Kind == css_lexer.TWhitespace {
		tokens = tokens[:i]
	}
	return tokens
}
