// Copyright 2024, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

package cssparser

import (
	"fmt"
	"strings"
	"unicode"
)

type Parser struct {
	Input      string
	Pos        int
	Length     int
	InQuote    bool
	QuoteChar  rune
	OpenParens int
	Debug      bool
}

func MakeParser(input string) *Parser {
	return &Parser{
		Input:  input,
		Length: len(input),
	}
}

func (p *Parser) Parse() (map[string]string, error) {
	result := make(map[string]string)
	lastProp := ""
	for {
		p.skipWhitespace()
		if p.eof() {
			break
		}
		propName, err := p.parseIdentifierColon(lastProp)
		if err != nil {
			return nil, err
		}
		lastProp = propName
		p.skipWhitespace()
		value, err := p.parseValue(propName)
		if err != nil {
			return nil, err
		}
		result[propName] = value
		p.skipWhitespace()
		if p.eof() {
			break
		}
		if !p.expectChar(';') {
			break
		}
	}
	p.skipWhitespace()
	if !p.eof() {
		return nil, fmt.Errorf("bad style attribute, unexpected character %q at pos %d", string(p.Input[p.Pos]), p.Pos+1)
	}
	return result, nil
}

func (p *Parser) parseIdentifierColon(lastProp string) (string, error) {
	start := p.Pos
	for !p.eof() {
		c := p.peekChar()
		if isIdentChar(c) || c == '-' {
			p.advance()
		} else {
			break
		}
	}
	attrName := p.Input[start:p.Pos]
	p.skipWhitespace()
	if p.eof() {
		return "", fmt.Errorf("bad style attribute, expected colon after property %q, got EOF, at pos %d", attrName, p.Pos+1)
	}
	if attrName == "" {
		return "", fmt.Errorf("bad style attribute, invalid property name after property %q, at pos %d", lastProp, p.Pos+1)
	}
	if !p.expectChar(':') {
		return "", fmt.Errorf("bad style attribute, bad property name starting with %q, expected colon, got %q, at pos %d", attrName, string(p.Input[p.Pos]), p.Pos+1)
	}
	return attrName, nil
}

func (p *Parser) parseValue(propName string) (string, error) {
	start := p.Pos
	quotePos := 0
	parenPosStack := make([]int, 0)
	for !p.eof() {
		c := p.peekChar()
		if p.InQuote {
			if c == p.QuoteChar {
				p.InQuote = false
			} else if c == '\\' {
				p.advance()
			}
		} else {
			if c == '"' || c == '\'' {
				p.InQuote = true
				p.QuoteChar = c
				quotePos = p.Pos
			} else if c == '(' {
				p.OpenParens++
				parenPosStack = append(parenPosStack, p.Pos)
			} else if c == ')' {
				if p.OpenParens == 0 {
					return "", fmt.Errorf("unmatched ')' at pos %d", p.Pos+1)
				}
				p.OpenParens--
				parenPosStack = parenPosStack[:len(parenPosStack)-1]
			} else if c == ';' && p.OpenParens == 0 {
				break
			}
		}
		p.advance()
	}
	if p.eof() && p.InQuote {
		return "", fmt.Errorf("bad style attribute, while parsing attribute %q, unmatched quote at pos %d", propName, quotePos+1)
	}
	if p.eof() && p.OpenParens > 0 {
		return "", fmt.Errorf("bad style attribute, while parsing property %q, unmatched '(' at pos %d", propName, parenPosStack[len(parenPosStack)-1]+1)
	}
	return strings.TrimSpace(p.Input[start:p.Pos]), nil
}

func isIdentChar(r rune) bool {
	return unicode.IsLetter(r) || unicode.IsDigit(r)
}

func (p *Parser) skipWhitespace() {
	for !p.eof() && unicode.IsSpace(p.peekChar()) {
		p.advance()
	}
}

func (p *Parser) expectChar(expected rune) bool {
	if !p.eof() && p.peekChar() == expected {
		p.advance()
		return true
	}
	return false
}

func (p *Parser) peekChar() rune {
	if p.Pos >= p.Length {
		return 0
	}
	return rune(p.Input[p.Pos])
}

func (p *Parser) advance() {
	p.Pos++
}

func (p *Parser) eof() bool {
	return p.Pos >= p.Length
}