package parser

import (
	"bytes"
	"log"
	"strings"
	"unicode"
)

func buildStylesStruct(styleBlock TemplateBlockDataBuffer) TemplateBlockData {
	return TemplateBlockData{
		Attrs: styleBlock.Attrs,
		Data:  styleBlock.Data.String(),
	}
}

type RawType struct {
	Before    string
	Left      string
	Right     string
	Between   string
	AfterName string
	Semicolon bool
	After     string
}

type SourceData struct {
	Offset int
	Line   int
	Column int
}

type SourceType struct {
	Start SourceData
	End   SourceData
}

type StyleItem struct {
	Raws     RawType
	Typ      string
	Nodes    []StyleItem
	Name     string
	Source   SourceType
	Params   string
	Text     string
	Prop     string
	Value    string
	Selector string
}

type styleItemType int

const StyleEOF = -1
const (
	STError styleItemType = iota
	STRoot
	STComment
	STInlineComment
	STRule
	STDecl
	STAtRule
)

var StyleItemType = map[styleItemType]string{
	StyleEOF:        "EOF",
	STRoot:          "root",
	STComment:       "comment",
	STInlineComment: "inlinecomment",
	STRule:          "rule",
	STDecl:          "decl",
	STAtRule:        "atrule",
}

type StylesLexer struct {
	input          []byte
	pos            int
	offset         int
	line           int
	column         int
	state          lexStylesStateFn
	prevType       string
	item           *StyleItem
	items          chan StyleItem
	itemDepth      []int
	itemOpenCount  int
	itemCloseCount int
	error          string
	EOF            bool
}

type lexStylesStateFn func(l *StylesLexer) lexStylesStateFn

func lexStyleBlock(styleBlock TemplateBlockDataBuffer) *StylesLexer {
	l := &StylesLexer{
		input:          []byte(styleBlock.Data.String()),
		state:          lexStyles,
		items:          make(chan StyleItem, 2),
		offset:         0,
		line:           1,
		column:         1,
		itemDepth:      []int{0, -1},
		itemOpenCount:  0,
		itemCloseCount: 0,
	}

	// Initialize lexer state
	l.item = &StyleItem{
		Typ: StyleItemType[STRoot],
		Source: SourceType{
			Start: SourceData{
				Offset: 0,
				Line:   1,
				Column: 1,
			},
		},
		Nodes: []StyleItem{},
	}

	go l.runStyleLexer()

	return l
}

func (l *StylesLexer) runStyleLexer() {
	for state := l.state; state != nil; {
		state = state(l)
	}

	// l.emit(&StyleItem{Typ: StyleItemType[StyleEOF]}, "runStyleLexer")
	close(l.items)
}

func lexStyles(l *StylesLexer) lexStylesStateFn {
	l.boundsCheck()
	if l.EOF {
		l.emit(l.item, "lexStyles")
		return nil
	}

	if l.itemOpenCount != 0 && l.itemOpenCount == l.itemCloseCount {
		findNextType(l)
		return nil
	}

	for {
		l.boundsCheck()
		if l.EOF {
			return nil
		}

		switch ch := l.curr(); {
		case ch == '/':
			l.itemDepth[len(l.itemDepth)-1]++
			return lexStylesComment
		default:
			l.itemDepth[len(l.itemDepth)-1]++
			return lexStyleRule
		}
	}
}

func lexStyleRule(l *StylesLexer) lexStylesStateFn {
	l.boundsCheck()
	if l.EOF {
		return nil
	}

	item := &StyleItem{}

	item.Typ = StyleItemType[STRule]
	item.Source.Start.Offset = l.offset
	item.Source.Start.Line = l.line
	item.Source.Start.Column = l.column

	var buf bytes.Buffer
	openPos := 0

	for {
		l.boundsCheck()
		if l.EOF {
			return nil
		}

		if l.curr() == '{' {
			openPos = l.pos + 1

			item.Selector = strings.TrimSpace(buf.String())

			bufStr := buf.String()
			btw := ""

			// Write Raws Between
			for i := 0; i < len(bufStr); i++ {
				if !unicode.IsSpace(rune(bufStr[i])) {
					btw = ""
					continue
				}
				btw += string(bufStr[i])
			}

			item.Raws.Between = btw

			buf = *bytes.NewBuffer([]byte{})

			l.next()
			continue
		}

		buf.WriteByte(l.curr())

		l.next()

		if l.itemOpenCount != 0 && l.itemOpenCount == l.itemCloseCount {
			break
		}
	}

	// TODO: Fix math stuff. thats a bug.
	item.Source.End.Offset = l.offset - 1
	item.Source.End.Line = l.line
	item.Source.End.Column = l.column - 1

	for l.curr() != '}' {
		l.prev()
	}
	l.prev()

	for unicode.IsSpace(rune(l.curr())) {
		l.prev()
	}

	if l.curr() == ';' {
		item.Raws.Semicolon = true
	}

	l.next()

	var after bytes.Buffer

	for l.curr() != '}' {
		if !unicode.IsSpace(rune(l.curr())) {
			break
		}
		after.WriteByte(l.curr())
		l.next()
	}

	item.Raws.After = after.String()

	for l.pos != openPos {
		l.prev()
	}

	typ := findNextType(l)
	appendCurrentNode(l, item)
	handleNextState(l, typ)

	for {
		l.next()
		if !unicode.IsSpace(rune(l.curr())) {
			break
		}
	}

	return lexStyles
}

func lexStyleDecl(l *StylesLexer) lexStylesStateFn {
	l.boundsCheck()
	if l.EOF {
		return nil
	}

	if l.prevType == StyleItemType[STDecl] {
		l.itemDepth[len(l.itemDepth)-1]++
	} else {
		l.itemDepth = append(l.itemDepth, 0)
	}

	if l.curr() == ';' {
		l.next()
	}

	item := &StyleItem{}

	item.Typ = StyleItemType[STDecl]

	// Write Raws Before
	var before bytes.Buffer
	for {
		if !unicode.IsSpace(rune(l.curr())) {
			break
		}

		before.WriteByte(l.curr())
		l.next()
	}

	item.Raws.Before = before.String()

	item.Source.Start.Offset = l.offset
	item.Source.Start.Line = l.line
	item.Source.Start.Column = l.column

	// Write Prop
	var prop bytes.Buffer
	for {
		if !unicode.IsSpace(rune(l.curr())) {
			prop.WriteByte(l.curr())
		}

		if l.peekNext() == ':' {
			l.next()
			break
		}

		l.next()
	}

	item.Prop = prop.String()

	// Write Raws Between
	var between bytes.Buffer
	for {
		if !unicode.IsSpace(rune(l.curr())) && l.curr() != ':' {
			break
		}

		between.WriteByte(l.curr())
		l.next()
	}

	item.Raws.Between = between.String()

	// Write Value
	var value bytes.Buffer
	for {
		if unicode.IsSpace(rune(l.curr())) || l.curr() == ';' {
			break
		}

		value.WriteByte(l.curr())
		l.next()
	}

	item.Value = value.String()

	item.Source.End.Offset = l.offset
	item.Source.End.Line = l.line
	item.Source.End.Column = l.column

	if l.prevType == StyleItemType[STDecl] && unicode.IsSpace(rune(l.curr())) {
		item.Source.End.Offset = l.offset - 1
	}

	if l.curr() != ';' {
		item.Source.End.Column = l.column - 1
	}

	typ := findNextType(l)
	appendCurrentNode(l, item)
	handleNextState(l, typ)

	return lexStyles
}

func findNextType(l *StylesLexer) string {
	l.boundsCheck()
	if l.EOF {
		return "close"
	}

	i := l.pos
	typ := "close"

	for {
		if l.input[i] == '}' && l.itemOpenCount == l.itemCloseCount+1 {
			typ = "close"
			break
		}

		if l.input[i] == ':' {
			typ = StyleItemType[STDecl]
			break
		}

		if l.input[i] == '{' {
			if l.curr() == ';' {
				l.next()
			}
			typ = StyleItemType[STRule]
			break
		}

		if l.input[i] == '@' {
			// TODO
			// typ = StyleItemType[STAtRule]
			break
		}

		i++
		if i > len(l.input)-1 {
			break
		}
	}

	if typ == StyleItemType[STRule] {
		return StyleItemType[STRule]
	}

	if typ == StyleItemType[STDecl] {
		return StyleItemType[STDecl]
	}

	return "close"
}

func handleNextState(l *StylesLexer, typ string) lexStylesStateFn {
	if typ == StyleItemType[STRule] {
		return lexStyleRule(l)
	}

	if typ == StyleItemType[STDecl] {
		return lexStyleDecl(l)
	}

	for l.curr() != '}' {
		l.next()
	}

	return lexStyles(l)
}

func lexStylesComment(l *StylesLexer) lexStylesStateFn {
	l.boundsCheck()
	if l.EOF {
		return nil
	}

	if l.peekNext() == '*' {
		return lexStylesMultilineComment
	}

	if l.peekNext() == '/' {
		return lexStylesInlineComment
	}

	return lexStyles
}

func lexStylesMultilineComment(l *StylesLexer) lexStylesStateFn {
	l.boundsCheck()
	if l.EOF {
		return nil
	}

	item := &StyleItem{}

	item.Typ = StyleItemType[STComment]
	item.Source.Start.Offset = l.offset
	item.Source.Start.Line = l.line
	item.Source.Start.Column = l.column

	l.next()
	l.next()

	for {
		l.boundsCheck()
		if l.EOF {
			return nil
		}

		if l.curr() == '*' && l.peekNext() == '/' {
			l.next()
			l.next()

			stopEating := false

			// Handle left raw
			for i := 0; i < len(item.Text); i++ {
				if !stopEating && unicode.IsSpace(rune(item.Text[i])) {
					item.Raws.Left = item.Raws.Left + string(item.Text[i])
					continue
				}
				stopEating = true
			}

			stopEating = false

			// Handle right raw
			for i := len(item.Text) - 1; i >= 0; i-- {
				if !stopEating && unicode.IsSpace(rune(item.Text[i])) {
					item.Raws.Right = string(item.Text[i]) + item.Raws.Right
					continue
				}
				stopEating = true
			}

			item.Text = strings.TrimSpace(item.Text)
			item.Source.End.Offset = 3 + len(item.Text) + len(item.Raws.Left) + len(item.Raws.Right)
			item.Source.End.Line = l.line
			item.Source.End.Column = l.column - 1

			appendCurrentNode(l, item)

			return lexStyles
		}

		item.Text += string(l.curr())
		l.next()
	}
}

func lexStylesInlineComment(l *StylesLexer) lexStylesStateFn {
	l.boundsCheck()
	if l.EOF {
		return nil
	}

	item := &StyleItem{}

	item.Typ = StyleItemType[STInlineComment]
	item.Source.Start.Offset = l.offset
	item.Source.Start.Line = l.line
	item.Source.Start.Column = l.column

	l.next()
	l.next()

	for {
		l.boundsCheck()

		if l.curr() == '\n' || l.EOF {
			stopEating := false

			// Handle left raw
			for i := 0; i < len(item.Text); i++ {
				if !stopEating && unicode.IsSpace(rune(item.Text[i])) {
					item.Raws.Left = item.Raws.Left + string(item.Text[i])
					continue
				}
				stopEating = true
			}

			break
		}

		item.Text += string(l.curr())
		l.next()
	}

	item.Text = strings.TrimSpace(item.Text)
	item.Source.End.Offset = 2 + len(item.Text) + len(item.Raws.Left)
	item.Source.End.Line = l.line
	item.Source.End.Column = l.column - 1

	appendCurrentNode(l, item)

	// l.emit(*itm, "lexStylesInlineComment")

	return lexStyles
}

// +++++++++++++++++++++++ Methods +++++++++++++++++++++++

func (l *StylesLexer) emit(itm *StyleItem, caller string) {
	// DEBUG
	// fmt.Printf("\n%s: %+v\n", caller, itm)

	printItem := &StyleItem{}
	printStyleToken(printItem, itm)

	l.items <- *printItem
	// l.item = &StyleItem{}
}

func appendCurrentNode(l *StylesLexer, item *StyleItem) {
	var parent = l.item

	l.prevType = item.Typ

	for i := 1; i < len(l.itemDepth); i++ {
		if i == len(l.itemDepth)-1 {
			(*parent).Nodes = append((*parent).Nodes, *item)
			break
		}

		parent = &parent.Nodes[len(parent.Nodes)-1]
	}
}

func printStyleToken(pi *StyleItem, item *StyleItem) {
	(*pi).Selector = (*item).Selector
	(*pi).Nodes = (*item).Nodes
	(*pi).Raws.Before = (*item).Raws.Before
	(*pi).Raws.Left = (*item).Raws.Left
	(*pi).Raws.Right = (*item).Raws.Right
	(*pi).Raws.Between = (*item).Raws.Between
	(*pi).Raws.AfterName = (*item).Raws.AfterName
	(*pi).Raws.Semicolon = (*item).Raws.Semicolon
	(*pi).Raws.After = (*item).Raws.After
	(*pi).Typ = (*item).Typ
	(*pi).Name = (*item).Name
	(*pi).Source.Start.Offset = (*item).Source.Start.Offset
	(*pi).Source.Start.Line = (*item).Source.Start.Line
	(*pi).Source.Start.Column = (*item).Source.Start.Column
	(*pi).Source.End.Offset = (*item).Source.End.Offset
	(*pi).Source.End.Line = (*item).Source.End.Line
	(*pi).Source.End.Column = (*item).Source.End.Column
	(*pi).Params = (*item).Params
	(*pi).Text = (*item).Text
	(*pi).Prop = (*item).Prop
	(*pi).Value = (*item).Value
	(*pi).Selector = (*item).Selector
}

func (l *StylesLexer) boundsCheck() {
	if l.pos >= len(l.input) {
		l.EOF = true
	} else {
		l.EOF = false
	}
	if l.pos > len(l.input) {
		log.Fatalf("Out of bounds error. Character %d of %d", l.pos, len(l.input))
	}
}

func (l *StylesLexer) curr() byte {
	l.boundsCheck()
	if l.EOF {
		return 0
	}
	return l.input[l.pos]
}

func (l *StylesLexer) next() styleItemType {
	l.boundsCheck()
	if l.EOF {
		return StyleEOF
	}

	if l.curr() == '{' {
		l.itemOpenCount++

	}

	if l.curr() == '}' {
		l.itemCloseCount++
	}

	l.offset++
	l.column++

	if l.curr() == byte('\n') {
		l.column = 1
		l.line++
	}

	l.pos += 1
	return 0
}

func (l *StylesLexer) prev() styleItemType {
	if l.pos <= 0 {
		return StyleEOF
	}

	l.pos -= 1

	if l.curr() == byte('\n') {
		l.line--
		l.column--
	}

	l.offset--
	l.column--

	if l.curr() == '{' {
		l.itemOpenCount--
	}

	if l.curr() == '}' {
		l.itemCloseCount--
	}

	return 0
}

func (l *StylesLexer) peekNext() byte {
	if l.pos >= len(l.input)-1 {
		return 0
	}
	return l.input[l.pos+1]
}

func (l *StylesLexer) peekPrev() byte {
	if l.pos <= 0 {
		return 0
	}
	return l.input[l.pos-1]
}

func (l *StylesLexer) eatWhitespace() {
	for unicode.IsSpace(rune(l.curr())) {
		l.next()
	}
}
