package parser

import (
	"bytes"
	"esperframework/options"
	"esperframework/utils"
	"fmt"
	"log"
	"os"
	"path/filepath"
	"strings"
	"unicode"
	"unicode/utf8"
)

var (
	SCRIPT = []byte("script")
	LINK   = []byte("link")
	REL    = []byte("rel")
	TYPE   = []byte("type")
	HREF   = []byte("href")
	SRC    = []byte("src")
)

type Kind struct {
	Name    string `json:"name"`
	RelType string `json:"relType"` // rel or type
	Value   string `json:"value"`
}

type Path struct {
	PathType   string `json:"pathType"` // href or src
	RelPath    string `json:"relPath"`
	AbsPath    string `json:"absPath"`
	HashedName string `json:"hashedName"`
}

type TemplateBlockAttrs map[string]string

type HeadBlockItem struct {
	Kind  Kind               `json:"kind"`
	Attrs TemplateBlockAttrs `json:"attrs"`
	Value string             `json:"value"`
	Path  Path               `json:"path"`
}

type TemplateBlockDataBuffer struct {
	Attrs TemplateBlockAttrs
	Data  bytes.Buffer
}

type TemplateBlockData struct {
	Attrs TemplateBlockAttrs `json:"attrs"`
	Data  string             `json:"data"`
}

type templateKind int

const (
	Layout templateKind = iota
	Page
	Component
	ServerRoute
)

var TemplateKind = map[templateKind]string{
	Layout:      "layout",
	Page:        "page",
	Component:   "component",
	ServerRoute: "server_route",
}

type TemplateDataBuffer struct {
	templateKind templateKind
	markupBlock  bytes.Buffer
	headBlock    []HeadBlockItem
	scriptBlock  TemplateBlockDataBuffer
	styleBlock   TemplateBlockDataBuffer
}

type TemplateMetaData struct {
	Path            string `json:"path"` // relative to project root
	TemplateKind    string `json:"templateKind"`
	MetaDataPath    string `json:"metaDataPath"` // relative to build cache root
	AdditionalDepth int    `json:"additionalDepth"`
}

type TemplateData struct {
	Markup   OutputMarkupTree  `json:"markup"`
	Styles   TemplateBlockData `json:"styles"`
	Script   TemplateBlockData `json:"script"`
	Head     []HeadBlockItem   `json:"head"`
	MetaData TemplateMetaData  `json:"metaData"`
}

type TBLexer struct {
	name            string
	input           []byte
	pos             int
	line            int
	offsetStart     int
	offsetEnd       int
	templateDataBuf TemplateDataBuffer
	options         *options.Options
	state           parseTemplateStateFn
	EOF             bool
}

type parseTemplateStateFn func(*TBLexer) parseTemplateStateFn

type newArgs struct {
	name     string
	osPath   string
	isAppDir bool
	fileKind string
}

var (
	HEAD_BLOCK_DELIM   = []byte("append:head")
	SCRIPT_BLOCK_DELIM = []byte("script")
	STYLE_BLOCK_DELIM  = []byte("style")
)

var seen = map[string]int{}

func New(l *TBLexer, args newArgs) TemplateData {
	l.run()

	// For testing (TODO: testing what?)
	if l.options == nil {
		return TemplateData{}
	}

	td := TemplateData{}

	// Handle named pages (ie. about.html in the root of the app dir. not /about/index.html)
	rel := handlePageRenames(l.options, args, &td)
	outPathNoExt := filepath.Join(l.options.CachePath, rel)
	metaDataPath := relativeToBuildCacheRoot(l.options.CachePath, outPathNoExt) + ".json"

	if _, ok := seen[metaDataPath]; ok {
		return td
	}

	if args.name != "parser_test" {
		seen[metaDataPath] = 0
	}

	td.MetaData.MetaDataPath = metaDataPath
	td.MetaData.Path = relativeToProjectRoot(l.options.Root, args.osPath)
	td.MetaData.TemplateKind = args.fileKind
	td.Head = l.templateDataBuf.headBlock

	var deps = buildDepGraph(l.options, td)

	// We are passing deps directly during testing
	if len(deps) == 0 && len(td.Head) != 0 {
		deps = td.Head
	}

	mt := &MarkupTree{
		Path: td.MetaData.Path,
		Markup: &Node{
			Typ:      MarkupItemType[MUFragment],
			Children: &[]Node{},
		},
		MetaData: td.MetaData,
	}

	// TODO: In progress
	// lexStyleBlock(l.templateDataBuf.styleBlock)

	markupLexer := LexMarkupBlock(l.templateDataBuf.markupBlock.String(), deps)

	return TemplateData{
		Markup: mt.parseMarkupBlock(markupLexer),
		Styles: buildStylesStruct(l.templateDataBuf.styleBlock),
		Script: TemplateBlockData{
			Attrs: l.templateDataBuf.scriptBlock.Attrs,
			Data:  l.templateDataBuf.scriptBlock.Data.String(),
		},
		Head:     td.Head,
		MetaData: td.MetaData,
	}
}

func handlePageRenames(opts *options.Options, args newArgs, td *TemplateData) string {
	// for testing
	if args.osPath == "" {
		return ""
	}

	rel := strings.Join(strings.Split(args.osPath, (*opts).Root+"/src/"), "")

	relPath, filename := filepath.Split(rel)

	fName, loName := splitFileNameFromLayoutDep(rel)

	if len(loName) > 0 {
		filename = fName + opts.Ext
	}

	if args.fileKind == TemplateKind[Component] || args.fileKind == TemplateKind[Layout] || args.name == "__not-found"+(*opts).Ext {
		return strings.Split(rel, filepath.Ext(rel))[0]
	}

	// len("index") == 5
	if args.fileKind == TemplateKind[Page] && len(filename) >= 5 && filename[:len("index")] == "index" {
		return strings.Split(rel, filepath.Ext(rel))[0]
	}

	dir := filepath.Base(filepath.Dir(rel))
	fnNoExt := strings.Split(filename, filepath.Ext(rel))[0]

	ext := opts.Ext
	if args.fileKind == TemplateKind[ServerRoute] {
		ext = ".go"
	}

	// Handle filename same as directory
	if dir == fnNoExt {
		if _, err := os.Stat(filepath.Join((*opts).Root, "src", relPath, "index"+ext)); !os.IsNotExist(err) {
			if _, err := os.Stat(filepath.Join((*opts).Root, "src", relPath, fnNoExt, "index"+ext)); !os.IsNotExist(err) {
				log.Fatalf("File exists at %s and %s. Unable to handle route for %s", filepath.Join(relPath, "index"+ext), filepath.Join(relPath, fnNoExt, "index"+ext), fnNoExt)
			}

			(*td).MetaData.AdditionalDepth++

			return filepath.Join(relPath, fnNoExt, "index")
		}

		return filepath.Join(relPath, "index")
	}

	// Handle filename not the same as directory
	(*td).MetaData.AdditionalDepth++

	return filepath.Join(relPath, fnNoExt, "index")
}

func splitFileNameFromLayoutDep(path string) (string, string) {
	fn, ld, _ := strings.Cut(filepath.Base(path), "@")
	return strings.Trim(fn, string(os.PathSeparator)), strings.Trim(ld, fmt.Sprintf("%s%s", filepath.Ext(path), string(os.PathSeparator)))
}

func (l *TBLexer) run() {
	for state := l.state; state != nil; {
		l.boundsCheck()
		if l.EOF {
			break
		}
		l.state = l.state(l)
	}
}

func extractMarkup(l *TBLexer) parseTemplateStateFn {
	l.boundsCheck()
	if l.EOF {
		return nil
	}

	// TODO: determine how text is handled inside of `pre` and `code` elements and handle parser state accordingly

	if l.input[l.pos] == '<' {
		l.next()

		switch {
		case bytes.Compare(l.input[l.pos:l.pos+len(HEAD_BLOCK_DELIM)], HEAD_BLOCK_DELIM) == 0:
			(*l).offsetStart = l.line
			l.eatToClosingAngleBracket()
			l.next()
			l.eatWhitespace()
			return lexHeadBlock

		case bytes.Compare(l.input[l.pos:l.pos+len(SCRIPT_BLOCK_DELIM)], SCRIPT_BLOCK_DELIM) == 0:
			(*l).pos = l.pos + len(SCRIPT_BLOCK_DELIM)
			l.eatWhitespace()

			am := l.lexBlockAttrs()
			if len(am) > 0 {
				(*l).templateDataBuf.scriptBlock.Attrs = am
			}

			for i := 0; i < l.line; i++ {
				(*l).templateDataBuf.scriptBlock.Data.Write([]byte("\n"))
			}

			(*l).offsetStart = l.line
			l.eatToClosingAngleBracket()
			l.next()
			l.eatWhitespace()
			return extractScriptBlock

		case bytes.Compare(l.input[l.pos:l.pos+len(STYLE_BLOCK_DELIM)], STYLE_BLOCK_DELIM) == 0:
			(*l).pos = l.pos + len(STYLE_BLOCK_DELIM)
			l.eatWhitespace()

			am := l.lexBlockAttrs()
			if len(am) > 0 {
				(*l).templateDataBuf.styleBlock.Attrs = am
			}

			for i := 0; i < l.line; i++ {
				(*l).templateDataBuf.styleBlock.Data.Write([]byte("\n"))
			}

			(*l).offsetStart = l.line
			l.eatToClosingAngleBracket()
			l.next()
			l.eatWhitespace()
			return extractStyleBlock
		}

		l.prev()
	}

	for i := 0; i < l.offsetEnd-l.offsetStart; i++ {
		(*l).templateDataBuf.markupBlock.Write([]byte("\n"))
	}

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

	(*l).offsetStart = 0
	(*l).offsetEnd = 0

	l.next()

	return extractMarkup
}

func (l *TBLexer) lexBlockAttrs() map[string]string {
	am := make(map[string]string)

	var attrKey bytes.Buffer
	var attrValue bytes.Buffer

	lexValue := false

	for {
		l.boundsCheck()
		if l.EOF {
			log.Fatal("Parsing terminated inside of lex block attribute function")
		}

		// Handle closing bracket
		if l.curr() == '>' {
			if attrKey.String() != "" {
				am[attrKey.String()] = utils.AcceptQuotedText(attrValue.String())
			}
			return am
		}

		if unicode.IsSpace(rune(l.curr())) {
			if attrKey.String() != "" {
				am[attrKey.String()] = utils.AcceptQuotedText(attrValue.String())
			}
			lexValue = false
			attrKey = bytes.Buffer{}
			attrValue = bytes.Buffer{}
			l.eatWhitespace()
		}

		if l.curr() == '=' {
			lexValue = true
			l.next()
			continue
		}

		if lexValue {
			attrValue.Write([]byte{l.curr()})
		} else {
			if l.curr() == '"' {
				l.next()
				continue
			}
			attrKey.Write([]byte{l.curr()})
		}

		l.next()
	}
}

func lexHeadBlock(l *TBLexer) parseTemplateStateFn {
	l.boundsCheck()
	if l.EOF {
		log.Fatal("Parsing terminated inside of head block")
	}

	if l.curr() == '<' && checkHeadBlockEnd(l) {
		(*l).offsetEnd = l.line
		return extractMarkup
	}

	if l.eatClosingTag() {
		l.eatWhitespace()
		return lexHeadBlock
	}

	if l.eatComment() {
		l.eatWhitespace()
		return lexHeadBlock
	}

	// Opening item bracket
	if l.curr() == '<' {
		var item = &HeadBlockItem{}
		l.next()

		for !unicode.IsSpace(rune(l.curr())) && bytes.Compare([]byte{l.curr()}, []byte{'>'}) != 0 {
			item.Kind.Name += string(l.curr())
			l.next()
		}

		(*l).templateDataBuf.headBlock = append((*l).templateDataBuf.headBlock, *item)

		return lexHeadItem
	}

	l.next()

	return lexHeadItem
}

func lexHeadItem(l *TBLexer) parseTemplateStateFn {
	l.eatWhitespace()
	l.boundsCheck()
	if l.EOF {
		log.Fatal("Parsing terminated inside of lex head item")
	}

	item := &HeadBlockItem{}

	if len(l.templateDataBuf.headBlock) > 0 {
		item = &l.templateDataBuf.headBlock[len(l.templateDataBuf.headBlock)-1]
	}

	var attrKey bytes.Buffer
	var attrValue bytes.Buffer

	lexValue := false
	isQuoted := false

	for {
		l.boundsCheck()
		if l.EOF {
			log.Fatal("Parsing terminated inside of lex head item")
		}

		if unicode.IsSpace(rune(l.curr())) {
			l.eatWhitespace()
			if l.curr() == '=' {
				lexValue = true
				l.next()
				l.eatWhitespace()
				continue
			}

			acceptAttr(l.options, item, attrKey, attrValue)
			attrKey = bytes.Buffer{}
			attrValue = bytes.Buffer{}
			lexValue = false
			l.eatWhitespace()
		}

		if l.curr() == '<' && checkHeadBlockEnd(l) {
			(*l).offsetEnd = l.line

			lintAttrs(item)
			return extractMarkup
		}

		// Handle self closing tags
		if l.curr() == '/' && l.peekNext() == '>' {
			acceptAttr(l.options, item, attrKey, attrValue)
			l.next()
			l.next()
			l.eatWhitespace()

			lintAttrs(item)
			return lexHeadBlock
		}

		// Handle closing bracket
		if l.curr() == '>' {
			acceptAttr(l.options, item, attrKey, attrValue)
			lintAttrs(item)
			l.next()
			l.eatWhitespace()

			if l.curr() == '<' && checkHeadBlockEnd(l) {
				(*l).offsetEnd = l.line

				lintAttrs(item)
				return extractMarkup
			}

			for l.curr() != '<' {
				item.Value += string(l.curr())
				l.next()
			}

			lintAttrs(item)
			return lexHeadBlock
		}

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

		if lexValue {
			var delim byte
			if l.curr() == '"' || l.curr() == '\'' || l.curr() == '`' {
				delim = l.curr()
				isQuoted = true
				l.next()
			}

			if isQuoted {
				for l.curr() != delim {
					attrValue.Write([]byte{l.curr()})
					l.next()
				}
			}

			attrValue.Write([]byte{l.curr()})
			isQuoted = false
		} else {
			if l.curr() == '"' {
				l.next()
				continue
			}
			attrKey.Write([]byte{l.curr()})
		}

		l.next()
	}
}

func acceptAttr(opts *options.Options, item *HeadBlockItem, attrKey, attrValue bytes.Buffer) {
	if attrKey.String() == "" || attrKey.String() == "-" {
		return
	}

	// TODO: check correctness of rel has href / script has src

	if bytes.Compare(REL, attrKey.Bytes()) == 0 {
		item.Kind.RelType = string(REL)
		item.Kind.Value = utils.AcceptQuotedText(attrValue.String())
		return
	}

	if bytes.Compare(TYPE, attrKey.Bytes()) == 0 {
		item.Kind.RelType = string(TYPE)
		item.Kind.Value = utils.AcceptQuotedText(attrValue.String())
		return
	}

	if bytes.Compare(HREF, attrKey.Bytes()) == 0 {
		item.Path.PathType = string(HREF)
		item.Path.RelPath = utils.AcceptQuotedText(attrValue.String())
		item.Path.HashedName = createHashedName(item.Path.RelPath)
		return
	}

	if bytes.Compare(SRC, attrKey.Bytes()) == 0 {
		item.Path.PathType = string(SRC)
		item.Path.RelPath = utils.AcceptQuotedText(attrValue.String())
		item.Path.HashedName = createHashedName(item.Path.RelPath)
		return
	}

	if item.Attrs == nil {
		item.Attrs = make(map[string]string)
	}

	item.Attrs[attrKey.String()] = utils.AcceptQuotedText(attrValue.String())
}

func createHashedName(relPath string) string {
	fn := strings.Replace(filepath.Base(relPath), filepath.Ext(relPath), "", 1)
	return fn + utils.ComputeHash(4, 4) + filepath.Ext(relPath)
}

func lintAttrs(item *HeadBlockItem) {
	if bytes.Compare([]byte(item.Kind.Name), LINK) == 0 && bytes.Compare([]byte(item.Path.PathType), HREF) != 0 {
		log.Fatal("Link tags require valid \"href\" attribute")
	}
	if bytes.Compare([]byte(item.Kind.Name), SCRIPT) == 0 && bytes.Compare([]byte(item.Path.PathType), SRC) != 0 {
		log.Fatal("Script tags require valid \"src\" attribute")
	}
}

func checkHeadBlockEnd(l *TBLexer) bool {
	l.next()

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

	if bytes.Compare(l.input[l.pos:l.pos+len(HEAD_BLOCK_DELIM)+1], append(HEAD_BLOCK_DELIM, []byte{'>'}...)) == 0 {
		l.eatToClosingAngleBracket()
		l.next()
		return true
	}

	l.backTrackToOpeningAngleBracket()

	return false
}

func acceptValue(l *TBLexer, item *HeadBlockItem) {
	l.eatWhitespace()

	if l.curr() == '=' {
		l.next()
		l.eatWhitespace()
		item.Kind.Value, _ = l.acceptQuotedStr()
	}
}

func acceptRelPath(l *TBLexer, item *HeadBlockItem) {
	l.eatWhitespace()

	if l.curr() == '=' {
		l.next()
		l.eatWhitespace()
		item.Path.RelPath, _ = l.acceptQuotedStr()
	}
}

func extractScriptBlock(l *TBLexer) parseTemplateStateFn {
	l.boundsCheck()
	if l.EOF {
		log.Fatal("Parsing terminated inside of script block")
	}

	if l.curr() == '<' && checkScriptBlockEnd(l) {
		(*l).offsetEnd = l.line
		return extractMarkup
	}

	(*l).templateDataBuf.scriptBlock.Data.Write([]byte{l.curr()})
	l.next()

	return extractScriptBlock
}

func checkScriptBlockEnd(l *TBLexer) bool {
	l.next()

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

	if bytes.Compare(l.input[l.pos:l.pos+len(SCRIPT_BLOCK_DELIM)+1], append(SCRIPT_BLOCK_DELIM, []byte{'>'}...)) == 0 {
		(*l).pos += len(SCRIPT_BLOCK_DELIM)
		l.next()
		return true
	}
	return false
}

func extractStyleBlock(l *TBLexer) parseTemplateStateFn {
	l.boundsCheck()
	if l.EOF {
		log.Fatal("Parsing terminated inside of style block")
	}

	if l.curr() == '<' && checkStyleBlockEnd(l) {
		(*l).offsetEnd = l.line
		return extractMarkup
	}

	(*l).templateDataBuf.styleBlock.Data.Write([]byte{l.curr()})
	l.next()

	return extractStyleBlock
}

func checkStyleBlockEnd(l *TBLexer) bool {
	l.next()

	if l.curr() == '/' {
		l.next()
	}
	if bytes.Compare(l.input[l.pos:l.pos+len(STYLE_BLOCK_DELIM)+1], append(STYLE_BLOCK_DELIM, []byte{'>'}...)) == 0 {
		(*l).pos += len(STYLE_BLOCK_DELIM)
		l.eatToClosingAngleBracket()
		l.next()
		return true
	}
	return false
}

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

func (l *TBLexer) 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 *TBLexer) curr() byte {
	return l.input[l.pos]
}

func (l *TBLexer) next() {
	l.boundsCheck()
	if l.EOF {
		return
	}
	if l.curr() == '\n' {
		(*l).line++
	}
	_, width := utf8.DecodeRuneInString(string(l.input[l.pos]))
	(*l).pos += width
}

func (l *TBLexer) prev() {
	if (*l).pos == 0 {
		return
	}
	(*l).pos -= 1
}

func (l *TBLexer) peekNext() byte {
	if len(l.input) < l.pos+1 {
		return 0
	}
	return l.input[l.pos+1]
}

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

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

func (l *TBLexer) eatClosingTag() bool {
	if l.curr() == '<' {
		l.next()

		if l.curr() == '/' {
			l.eatToClosingAngleBracket()
			l.next()
			return true
		}

		l.backTrackToOpeningAngleBracket()
	}

	return false
}

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

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

func (l *TBLexer) eatComment() bool {
	if l.curr() == '<' && l.peekNext() == '!' {
		l.eatToClosingAngleBracket()
		l.next()
		return true
	}
	return false
}

func (l *TBLexer) acceptStr() string {
	var buf bytes.Buffer

	for !unicode.IsSpace(rune(l.curr())) {
		buf.Write([]byte{l.curr()})
		l.next()
	}

	bufStr := buf.String()

	if bufStr[0] == '"' || buf.String()[0] == '\'' || buf.String()[0] == '`' {
		bufStr = bufStr[1:]
	}

	if bufStr[len(bufStr)-1] == '"' || bufStr[len(bufStr)-1] == '\'' || bufStr[len(bufStr)-1] == '`' {
		bufStr = bufStr[:len(bufStr)-1]
	}

	return bufStr
}

func (l *TBLexer) acceptQuotedStr() (string, int) {
	var buf bytes.Buffer

	var quoteType byte

	if l.curr() == '"' || l.curr() == '\'' || l.curr() == '`' {
		quoteType = l.curr()
		l.next()
	}

	var count int

	for !unicode.IsSpace(rune(l.curr())) {
		if l.curr() == quoteType && l.peekPrev() != '\\' {
			break
		}
		buf.Write([]byte{l.curr()})
		l.next()
		count++
	}

	bufStr := buf.String()

	if bufStr[0] == '"' || buf.String()[0] == '\'' || buf.String()[0] == '`' {
		bufStr = bufStr[1:]
	}

	if bufStr[len(bufStr)-1] == '"' || bufStr[len(bufStr)-1] == '\'' || bufStr[len(bufStr)-1] == '`' {
		bufStr = bufStr[:len(bufStr)-1]
	}

	return bufStr, count
}

func relativeToProjectRoot(root, osPath string) string {
	osPathSplit := strings.Split(osPath, root+"/")
	return strings.Join(osPathSplit, "/")
}

func relativeToBuildCacheRoot(cachePath, osPath string) string {
	osPathSplit := strings.Split(osPath, cachePath+"/")
	return strings.Join(osPathSplit, "/")
}
