package parser

import (
	"bytes"
	"esperframework/utils"
	"fmt"
	"log"
	"strings"
	"unicode"
	"unicode/utf8"
)

type MarkupItem struct {
	Typ         string       `json:"typ"`
	Data        string       `json:"data,omitempty"`
	Name        string       `json:"name,omitempty"`
	Start       int          `json:"start"`
	End         int          `json:"end"`
	Line        int          `json:"line"`
	SelfClosing bool         `json:"selfClosing,omitempty"`
	Value       []MarkupItem `json:"value,omitempty"`
	Attributes  []MarkupItem `json:"attributes,omitempty"`
	Children    []MarkupItem `json:"children,omitempty"`
}

type markupItemType int

const MarkupEOF = -1
const (
	MUError markupItemType = iota
	MUFragment
	MUComment
	MUText
	MUSlot
	MUElement
	MUInlineComponent
	MUClosingTag
	MUAttribute
)

var MarkupItemType = map[markupItemType]string{
	MarkupEOF:         "EOF",
	MUError:           "Error",
	MUFragment:        "Fragment",
	MUComment:         "Comment",
	MUText:            "Text",
	MUSlot:            "Slot",
	MUElement:         "Element",
	MUInlineComponent: "InlineComponent",
	MUClosingTag:      "ClosingTag",
	MUAttribute:       "Attribute",
}

type MarkupLexer struct {
	input []byte
	pos   int
	start int
	end   int
	line  int
	state lexMarkupStateFn
	item  MarkupItem
	items chan MarkupItem
	deps  map[string]int
	error string
	EOF   bool
}

type lexMarkupStateFn func(l *MarkupLexer) lexMarkupStateFn

func LexMarkupBlock(markup string, deps []HeadBlockItem) *MarkupLexer {
	depNames := map[string]int{}

	for _, dep := range deps {
		if dep.Kind.Value == "import" {
			if _, ok := dep.Attrs["name"]; !ok {
				log.Fatalf("\nrel import requires name attribute\n%s\n", dep.Attrs["name"])
			}
			depNames[dep.Attrs["name"]] = 0
		}
	}

	l := &MarkupLexer{
		input: []byte(markup),
		state: lexText,
		items: make(chan MarkupItem, 2),
		deps:  depNames,
		line:  1,
	}

	go l.runMarkupLexer()

	return l
}

func (l *MarkupLexer) runMarkupLexer() {
	for state := l.state; state != nil; {
		state = state(l)
	}
	l.emit(MarkupItem{Typ: MarkupItemType[MarkupEOF], End: l.pos}, "run")
	close(l.items)
}

func lexText(l *MarkupLexer) lexMarkupStateFn {
	l.boundsCheck()
	if l.EOF {
		return nil
	}

	l.start = l.pos
	l.item.Line = l.line

	text := &bytes.Buffer{}

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

		if l.curr() == '<' {
			if (*text).Len() > 0 {
				emitText(l, text)
				l.start = l.pos
			}

			if l.peekNext() == 0 {
				return nil
			}

			switch ch := l.peekNext(); {
			case ch == '/':
				return lexClosingTag

			case ch == '!':
				return lexComment

			case unicode.IsLetter(rune(ch)):
				name := l.getElementName()
				l.backTrackToOpeningAngleBracket()

				if _, ok := l.deps[string(name)]; ok {
					return lexInlineComponent
				}

				return lexElement
			}
		}

		(*text).Write([]byte{l.curr()})

		if l.next() == MarkupEOF {
			emitText(l, text)
			return nil
		}
	}
}

func emitText(l *MarkupLexer, text *bytes.Buffer) {
	if (*text).Len() < 1 {
		return
	}

	l.item.Typ = MarkupItemType[MUText]
	l.item.Data = (*text).String()
	l.item.Start = l.start
	l.item.End = l.pos
	l.item.SelfClosing = true

	l.emit(l.item, "emitText")

	text = &bytes.Buffer{}

	l.boundsCheck()
	if l.EOF {
		l.emit(MarkupItem{Typ: MarkupItemType[MarkupEOF]}, "emitText")
	}
}

func lexElement(l *MarkupLexer) lexMarkupStateFn {
	return lexElementLikeComponent(l, MarkupItemType[MUElement], "lexElement")
}

func lexInlineComponent(l *MarkupLexer) lexMarkupStateFn {
	return lexElementLikeComponent(l, MarkupItemType[MUInlineComponent], "lexInlineComponent")
}

func lexElementLikeComponent(l *MarkupLexer, itemType string, ident string) lexMarkupStateFn {
	l.start = l.pos

	name := l.getElementName()
	l.eatWhitespace()

	l.item.Typ = itemType
	l.item.Name = name
	l.item.Start = l.start

	if l.isCloseOpeningTagToken() {
		l.eatCloseToken()

		l.item.Line = l.line

		// Handle empty element/inline component tags (ie. <Links></Links>)
		//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
		ff1 := 0
		handleClosingTag := false

		for _, ch := range l.input[l.pos:] {
			if unicode.IsSpace(rune(ch)) {
				l.next()
				ff1 += 1
			} else if ch == '<' && l.peekNext() == '/' {
				l.next()
				l.next()
				handleClosingTag = true
				break
			} else if l.pos >= len(l.input)-1 {
				break
			} else {
				for i := 0; i < ff1; i++ {
					l.prev()
				}
				break
			}
		}

		if handleClosingTag {
			var str bytes.Buffer
			var ff2 = 0

			for _, ch := range l.input[l.pos:] {
				l.next()
				ff2 += 1

				if ch == '>' {
					// This case handles empty tags (ie. <Links></Links>)
					// To make the markup block parser happy we treat this as a self closing tag
					l.item.SelfClosing = true
					break
				}

				str.Write([]byte{ch})
			}

			if l.item.Name != str.String() {
				for i := 0; i < ff2+3; i++ {
					l.prev()
				}
			}
		}
		//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

		l.item.End = l.pos

		l.emit(l.item, ident)
		return lexText
	}

	l.eatWhitespace()
	lexAttrs(l)

	return lexText
}

func lexAttrs(l *MarkupLexer) lexMarkupStateFn {
	l.start = l.pos

	var attrKey bytes.Buffer
	var attrValue bytes.Buffer
	var delim byte

	lexValue := false
	isQuotedValue := false
	delimCount := 0
	wasAttrKey := false
	wasAppendItem := false
	appendItem := false
	emitToken := false

	for {
		if lexValue {
			if l.curr() == '=' {
				l.next()
				l.eatWhitespace()
			}

			if !isQuotedValue && l.isQuote(l.curr()) {
				delim = l.curr()
				isQuotedValue = true
				delimCount++

				attrValue.Write([]byte{l.curr()})
				l.next()
			}

			// quoted attrs
			if !l.isCloseOpeningTagToken() && isQuotedValue {
				attrValue.Write([]byte{l.curr()})
			}

			if isQuotedValue && delimCount == 1 && l.curr() == delim && l.peekPrev() != '\\' {
				appendItem = true
			}

			if l.isCloseOpeningTagToken() {
				emitToken = true
			}

			// non-quoted attrs
			decrementEnd := 0

			if !l.isCloseOpeningTagToken() && !isQuotedValue {
				decrementEnd = 1

				for !l.isCloseOpeningTagToken() && !unicode.IsSpace(rune(l.curr())) {
					// fmt.Println("IN_VAL:", l.currChar(), string(l.currChar()))
					attrValue.Write([]byte{l.curr()})
					l.next()
				}

				appendItem = true
			}

			if appendItem && len(attrValue.Bytes()) > 0 {
				// fmt.Println("APPEND_VAL:", attrValue.Bytes(), attrValue.String())

				l.item.Attributes = append(l.item.Attributes, MarkupItem{
					Typ:   MarkupItemType[MUAttribute],
					Name:  attrKey.String(),
					Start: l.start,
					End:   l.pos + 1 - decrementEnd,
					Line:  l.line,
				})

				l.item.Attributes[len(l.item.Attributes)-1].Value = append(l.item.Value, MarkupItem{
					Typ:   MarkupItemType[MUText],
					Data:  utils.AcceptQuotedText(attrValue.String()),
					Start: l.pos - len(utils.AcceptQuotedText(attrValue.String())),
					End:   l.pos,
					Line:  l.line,
				})

				if l.item.Name == "slot" && attrKey.String() == "type" && utils.AcceptQuotedText(attrValue.String()) == "static" {
					l.item.Typ = MarkupItemType[MUSlot]
				}

				lexValue = false
				attrKey = bytes.Buffer{}
				attrValue = bytes.Buffer{}
				isQuotedValue = false
				appendItem = false
				wasAppendItem = true
				delim = 0
				delimCount = 0

				if l.isCloseOpeningTagToken() {
					emitToken = true
				}
			}

			if emitToken {
				emitToken = false
				handleAttrExit(l)
				l.eatWhitespace()

				if l.isCloseOpeningTagToken() {
					l.eatCloseToken()
					return lexText
				}
			}

			l.next()
			l.eatWhitespace()

			if wasAppendItem {
				wasAppendItem = false
				l.start = l.pos
			}

			continue
		}
		// end lex value

		if l.peekNext() == '=' {
			lexValue = true
		}

		if l.isCloseOpeningTagToken() {
			// fmt.Println("IN_KEY_2", l.currChar(), string(l.currChar()))
			appendItem = true
			emitToken = true
		}

		if !l.isCloseOpeningTagToken() && !unicode.IsSpace(rune(l.curr())) && l.curr() != '=' {
			// fmt.Println("IN_KEY_1", l.currChar(), string(l.currChar()))
			attrKey.Write([]byte{l.curr()})
		}

		if unicode.IsSpace(rune(l.curr())) {
			appendItem = true
		}

		if appendItem && len(attrKey.Bytes()) > 0 {
			// fmt.Println("KEY", attrKey, attrKey.String())

			l.item.Attributes = append(l.item.Attributes, MarkupItem{
				Typ:   MarkupItemType[MUAttribute],
				Name:  attrKey.String(),
				Start: l.start,
				End:   l.pos,
				Line:  l.line,
			})

			wasAttrKey = true
			appendItem = false
			attrKey = bytes.Buffer{}

			if !emitToken {
				l.next()
				l.eatWhitespace()
				continue
			}
		}

		if emitToken {
			emitToken = false
			handleAttrExit(l)
			l.eatWhitespace()

			if l.isCloseOpeningTagToken() {
				l.eatCloseToken()
				return lexText
			}
		}

		if wasAttrKey {
			appendItem = false
			wasAttrKey = false
			attrKey = bytes.Buffer{}

			l.next()
			l.eatWhitespace()
			l.prev()

			l.start = l.pos
		} else {
			l.next()
		}
	}
}

func handleAttrExit(l *MarkupLexer) {
	l.eatWhitespace()

	if l.curr() == '/' && l.peekNext() == '>' {
		l.item.SelfClosing = true
		l.item.End = l.pos + 2
		l.item.Line = l.line
	}

	if l.curr() == '>' {
		l.item.End = l.pos + 1
		l.item.Line = l.line
	}

	l.emit(l.item, "handleAttrExit")
}

func lexClosingTag(l *MarkupLexer) lexMarkupStateFn {
	l.start = l.pos

	if l.curr() == '<' && l.peekNext() != '/' {
		l.next()
		bval := []byte{l.peekNext()}
		val, _ := utf8.DecodeRune(bval)
		if !unicode.IsLetter(val) {
			l.errorf("Invalid closing tag")
		}
	}

	var tag bytes.Buffer

	for l.curr() != '>' {
		if l.curr() != '<' && l.curr() != '/' {
			tag.Write([]byte{l.curr()})
		}

		l.next()
	}

	l.item.Name = strings.TrimSpace(tag.String())
	l.next()
	tag.Write([]byte(">"))

	l.item.Typ = MarkupItemType[MUClosingTag]
	l.item.Start = l.start
	l.item.End = l.pos
	l.item.Line = l.line

	l.emit(l.item, "lexClosingTag")

	return lexText
}

func lexComment(l *MarkupLexer) lexMarkupStateFn {
	var open = "<!--"
	var close = "-->"

	l.item.Start = l.start
	l.item.Line = l.line

	if bytes.Compare(l.input[l.pos:l.pos+len(open)], []byte(open)) != 0 {
		return l.errorf("Bad comment syntax")
	}

	l.item.Start = l.pos
	l.pos += len(open)

	var buf bytes.Buffer

	for bytes.Compare(l.input[l.pos:l.pos+len(close)], []byte(close)) != 0 {
		buf.Write([]byte{l.curr()})
		l.next()
	}

	l.item.Typ = MarkupItemType[MUComment]
	l.item.End = l.pos + len(close)
	l.item.SelfClosing = true
	l.item.Data = strings.Trim(buf.String(), "\r\n\t ")

	l.pos += len(close)

	l.emit(l.item, "lexComment")

	return lexText
}

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

func (l *MarkupLexer) emit(itm MarkupItem, caller string) {
	// DEBUG
	// fmt.Printf("%s: %+v\n", caller, itm)
	l.items <- itm
	l.start = l.pos
	l.item = MarkupItem{}
}

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

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

func (l *MarkupLexer) next() markupItemType {
	l.boundsCheck()
	if l.EOF {
		return MarkupEOF
	}

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

	l.pos += 1
	return 0
}

func (l *MarkupLexer) prev() markupItemType {
	if l.pos <= 0 {
		return MarkupEOF
	}

	l.pos -= 1

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

	return 0
}

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

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

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

// Results in currChar == the closing angle bracket
func (l *MarkupLexer) eatToClosingAngleBracket() {
	for l.curr() != '>' {
		l.next()
	}
}

func (l *MarkupLexer) eatCloseToken() {
	if l.curr() == '/' {
		l.item.SelfClosing = true
		l.next()
	}
	if l.curr() == '>' {
		l.next()
	}
}

// Results in currChar == the opening angle bracket
func (l *MarkupLexer) backTrackToOpeningAngleBracket() {
	for l.curr() != '<' {
		l.prev()
	}
}

func (l *MarkupLexer) backTrackToLetter() {
	for !unicode.IsLetter(rune(l.curr())) {
		l.boundsCheck()
		l.prev()
	}
}

func (l *MarkupLexer) getElementName() string {
	if l.curr() == '<' {
		l.next()
	}

	var name []byte

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

		name = append(name, l.curr())
		l.next()
	}

	return string(name)
}

func (l *MarkupLexer) isCloseOpeningTagToken() bool {
	if l.curr() == '/' && l.peekNext() == '>' {
		l.item.SelfClosing = true
		return true
	}

	if l.curr() == '>' {
		return true
	}

	return false
}

func (l *MarkupLexer) isQuote(ch byte) bool {
	if ch == '"' || ch == '\'' || ch == '`' {
		return true
	}
	return false
}

func (l *MarkupLexer) errorf(format string, args ...interface{}) lexMarkupStateFn {
	l.items <- MarkupItem{
		Typ:  MarkupItemType[MUError],
		Data: fmt.Sprintf(format, args...),
	}
	return nil
}
