package compiler

import (
	"bytes"
	"esperframework/compiler/appshell"
	"esperframework/compiler/parser"
	"esperframework/options"
	"fmt"
	"log"
	"path/filepath"
	"strings"
)

type Node struct {
	idx        []int
	typ        string
	html       string
	next       *Node
	prev       *Node
	closingTag *Node
}

type MarkupList struct {
	idx         *[]int
	head        *Node
	length      int
	writeToOpen bool
}

func aggregateMarkup(opts *options.Options, td *parser.TemplateData, dg *parser.DepGraph, templateBodyData *[]appshell.SlotItemData, templatesSeen *map[string][]appshell.SlotItemData) {
	if _, ok := (*templatesSeen)[td.MetaData.MetaDataPath]; ok {
		return
	}

	for _, path := range (*dg)[td.MetaData.MetaDataPath].LayoutPaths {
		if _, ok := (*templatesSeen)[path]; ok {
			continue
		}

		templateData := getTemplateData(opts, path)
		ml := &MarkupList{}
		ml.init(&templateData.Markup.HTML)
		aggregateDeps(opts, &templateData, dg, templateBodyData, templatesSeen)
		appendBodyItems(ml, &templateData, templateBodyData, parser.TemplateKind[parser.Layout])

		(*templatesSeen)[templateData.MetaData.MetaDataPath] = *templateBodyData
	}

	ml := &MarkupList{}
	ml.init(&td.Markup.HTML)
	aggregateDeps(opts, td, dg, templateBodyData, templatesSeen)
	appendBodyItems(ml, td, templateBodyData, parser.TemplateKind[parser.Page])

	(*templatesSeen)[td.MetaData.MetaDataPath] = *templateBodyData
}

func (ml *MarkupList) init(ast *parser.MarkupItem) {
	initIdx := make([]int, 1)
	ml.idx = &initIdx
	(*ml.idx)[0] = 0

	if len(ast.Children) <= (*ml.idx)[0] {
		return
	}

	ml.walk(&ast.Children, &ast.Children[(*ml.idx)[0]])
}

func (ml *MarkupList) walk(ast *[]parser.MarkupItem, astNode *parser.MarkupItem) {
	curr, status := eatUnnecessaryNodes(ast, astNode, ml.idx)
	if status == "NIL" {
		return
	}

	astNode = &curr

	openNode, closeNode := transformNode(astNode)
	openNode.idx = ml.getIdx()

	if ml.head == nil {
		ml.insertListHead(ast, astNode, &openNode, &closeNode)
		return
	}

	if len((*astNode).Children) > 0 {
		ml.insert(&openNode, &closeNode)

		*ml.idx = append(*ml.idx, 0)

		ml.walk(ast, &(*astNode).Children[0])
		return
	}

	// Below here is for elements with no child nodes
	// fmt.Printf("%+v\n", "********NO CHILD NODES********")

	ml.insert(&openNode, &closeNode)

	parentSlice := []parser.MarkupItem{}
	currNode := &parser.MarkupItem{}
	currNodeChildren := *ast
	reparent := false
	count := 0

	i := 0
	for {
		// we have recursed all the way to the root
		if len(*ml.idx) == 0 {
			currNode = nil
			break
		}

		if i >= len(*ml.idx) {
			break
		}

		if (*ml.idx)[i] > len(currNodeChildren)-1 {
			currNode = nil
			break
		}

		if len(currNodeChildren) > 0 {
			parentSlice = currNodeChildren
			*currNode = currNodeChildren[(*ml.idx)[i]]
			currNodeChildren = (*currNode).Children
		}

		// Runs on leaf nodes
		if len(currNodeChildren) == 0 {

			mlLen := len(*(*ml).idx)

			if (*ml.idx)[len(*ml.idx)-1] >= len(parentSlice)-1 && len(parentSlice) <= 1 {
				*ml.idx = (*ml.idx)[:len(*ml.idx)-1]
			}

			if count > 0 && currNode.SelfClosing == true || count > 0 && len(currNode.Children) == 0 {
				ml.walk(ast, currNode)
				return
			}

			if (*ml.idx)[len(*ml.idx)-1] == len(parentSlice)-1 && mlLen == len(*(*ml).idx) {
				break
			}

			(*ml.idx)[len(*ml.idx)-1]++

			i = 0
			count++
			currNodeChildren = *ast
			reparent = true

			continue
		}

		i++
	}

	// below here runs on leaf nodes
	if reparent && currNode != nil {
		ml.walk(ast, currNode)
		return
	}

	// we have recursed all the way to the root
	if len(*(*ml).idx) == 0 {
		return
	}

	hs := *ast
	node := parser.MarkupItem{}

	i = 0
	reparented := false

	for {
		if !reparented && len(*(*ml).idx) > 0 {
			if i == len(*(*ml).idx)-1 {

				*(*ml).idx = (*(*ml).idx)[:len(*(*ml).idx)-1]

				// we have recursed all the way to the root
				if len(*(*ml).idx) == 0 {
					break
				}

				(*(*ml).idx)[len(*(*ml).idx)-1]++

				i = 0
				hs = *ast
				node = parser.MarkupItem{}
				reparented = true
				continue
			}
		}

		// This case will be true if the node has been reparented
		if i == len(*(*ml).idx) {
			// fmt.Println("********REPARENTED********")

			if i == len(*(*ml).idx)-1 {
				*(*ml).idx = (*(*ml).idx)[:len(*(*ml).idx)-1]

				if len(*(*ml).idx) == 0 {
					return
				}

				(*(*ml).idx)[len(*(*ml).idx)-1]++
			}
			break
		}

		if i >= len(*(*ml).idx) || (*(*ml).idx)[i] > len(hs)-1 {
			return
		}

		node = hs[(*(*ml).idx)[i]]
		hs = node.Children

		i++
	}

	if node.Typ != "" {
		ml.walk(ast, &node)
	}
}

func aggregateDeps(opts *options.Options, td *parser.TemplateData, dg *parser.DepGraph, templateBodyData *[]appshell.SlotItemData, templatesSeen *map[string][]appshell.SlotItemData) {
	for _, dep := range (*dg)[td.MetaData.MetaDataPath].Deps {
		if dep.Kind.Value != "import" {
			continue
		}

		depth := ""
		for i := 0; i < td.MetaData.AdditionalDepth; i++ {
			depth += "../"
		}

		metaDataPath := filepath.Join(strings.Split(td.MetaData.MetaDataPath, filepath.Base(td.MetaData.MetaDataPath))[0], depth+strings.Split(dep.Path.RelPath, filepath.Ext(dep.Path.RelPath))[0]+".json")

		if _, ok := (*templatesSeen)[metaDataPath]; ok {
			continue
		}

		templateData := getTemplateData(opts, metaDataPath)

		ml := &MarkupList{}

		ml.init(&templateData.Markup.HTML)
		appendBodyItems(ml, &templateData, templateBodyData, dep.Attrs["name"])
		aggregateDeps(opts, &templateData, dg, templateBodyData, templatesSeen)

		(*templatesSeen)[metaDataPath] = *templateBodyData
	}
}

func appendBodyItems(ml *MarkupList, td *parser.TemplateData, templateBodyData *[]appshell.SlotItemData, loc string) {
	html := ml.printList()

	item := appshell.SlotItemData{}
	item.Path = td.MetaData.MetaDataPath
	item.Typ = loc

	if len(item.Boundaries) == 0 {
		item.Boundaries = make([]string, 2)
	}

	// hash := "WF" + utils.ComputeHash(2, 4)

	// if len(hi.Attrs) == 0 {
	// 	(*hi).Attrs = map[string]string{}
	// } else {
	// 	for key := range (*hi).Attrs {
	// 		if key[:2] == "WF" {
	// 			hash = key
	// 		}
	// 	}
	// }

	idx := indexOf(*templateBodyData, "Path", td.MetaData.MetaDataPath)

	// Handle first item
	if idx == -1 {
		// item.Boundaries[0] = "body " + hash
		// hi.Attrs[hash] = ""

		item.HTML.Open = html.Open
		item.HTML.Close = html.Close
		*templateBodyData = append(*templateBodyData, item)
		return
	}

	// // Generate hash attr for last item
	// if lastItem {
	// 	(*templateBodyData)[idx].Boundaries[1] = "head " + hash
	// 	hi.Attrs[hash] = ""
	// }

	(*templateBodyData)[idx].HTML.Open += html.Open
	(*templateBodyData)[idx].HTML.Close += html.Close
}

func (ml *MarkupList) printList() appshell.SlotHTML {
	ml.writeToOpen = true

	var openBuf *bytes.Buffer = bytes.NewBuffer([]byte{})
	var closeBuf *bytes.Buffer = bytes.NewBuffer([]byte{})

	currNode := ml.head

	if currNode == nil {
		return appshell.SlotHTML{}
	}

	if (*currNode).next == nil {
		ml.writeNode(currNode, openBuf, closeBuf)

		return appshell.SlotHTML{
			Open:  openBuf.String(),
			Close: closeBuf.String(),
		}
	}

	for (*currNode).next != nil {
		ml.writeNode(currNode, openBuf, closeBuf)
		currNode = currNode.next
	}

	ml.writeNode(currNode, openBuf, closeBuf)

	return appshell.SlotHTML{
		Open:  openBuf.String(),
		Close: closeBuf.String(),
	}
}

func (ml *MarkupList) writeNode(currNode *Node, openBuf, closeBuf *bytes.Buffer) {
	if (*currNode).typ == parser.MarkupItemType[parser.MUSlot] {
		ml.writeToOpen = false
		return
	}

	if ml.writeToOpen {
		(*openBuf).WriteString((*currNode).html)
	} else {
		(*closeBuf).WriteString((*currNode).html)
	}
}

func (ml *MarkupList) insert(on *Node, cn *Node) {
	currNode := ml.head
	var parent *Node = nil
	var prevSibling *Node = nil
	hasSiblings := false

	i := 0
	for {
		if i > (*ml).length-1 {
			break
		}

		if currNode == nil {
			break
		}

		// Parent
		if len((*currNode).idx) == len(*(ml).idx)-1 {
			parent = currNode
		}

		// Siblings
		if len((*currNode).idx) == len(*(ml).idx) {
			if (*(*ml).idx)[len((*(*ml).idx))-1] > (*currNode).idx[len((*currNode).idx)-1] {
				hasSiblings = true
				prevSibling = currNode

				if (*(*ml).idx)[len((*(*ml).idx))-1] == (*currNode).idx[len((*currNode).idx)-1]-1 {
					break
				}
			}
		}

		currNode = currNode.next

		i++
	}

	if hasSiblings {
		// fmt.Printf("%+v\n", "********HAS PREV SIBLINGS********")

		if cn.html == "" {
			if parent != nil {
				on.next = parent.closingTag

				if parent.closingTag != nil {
					parent.closingTag.prev = on
				}
			}

			if (*prevSibling).closingTag == nil {
				on.prev = prevSibling
				(*prevSibling).next = on
			} else {
				on.prev = prevSibling.closingTag
				(*prevSibling).closingTag.next = on
			}

			(*ml).length += 1

			return
		}

		cn.prev = on

		if parent != nil {
			cn.next = parent.closingTag
		}

		on.next = cn
		on.closingTag = cn

		if (*prevSibling).closingTag == nil {
			on.prev = prevSibling
			(*prevSibling).next = on
		} else {
			on.prev = prevSibling.closingTag
			(*prevSibling).closingTag.next = on
		}

		(*ml).length += 2

		return
	}

	// fmt.Printf("%+v\n", "********HAS NO PREV SIBLINGS********")

	if parent != nil {
		on.prev = parent
		(*parent).next = on
	}

	if cn.html == "" {
		if parent != nil {
			if parent.closingTag != nil {
				on.prev = parent.closingTag
				on.next = parent.closingTag

				parent.closingTag.prev = on
			}

			parent.next = on
		}

		(*ml).length += 1
		return
	}

	cn.prev = on

	if parent != nil {
		cn.next = parent.closingTag
	}

	on.next = cn
	on.closingTag = cn

	if parent != nil && parent.closingTag != nil {
		(*parent).closingTag.prev = cn
	}

	(*ml).length += 2
}

func (ml *MarkupList) insertListHead(ast *[]parser.MarkupItem, astNode *parser.MarkupItem, openNode, closeNode *Node) {
	ml.head = openNode
	ml.head.idx = ml.getIdx()
	ml.head.typ = astNode.Typ

	if len(closeNode.html) == 0 {
		ml.length = 1
	} else {
		ml.head.next = closeNode
		ml.length = 2
	}

	if len((*astNode).Children) > 0 {
		(*ml.idx) = append(*ml.idx, 0)
		ml.walk(ast, &(*astNode).Children[0])
		return
	}

	(*ml.idx)[len(*ml.idx)-1]++

	if (*ml.idx)[len(*ml.idx)-1] > len(*ast)-1 {
		return
	}

	ml.walk(ast, &(*ast)[(*ml.idx)[len(*ml.idx)-1]])
}

func (ml *MarkupList) getIdx() []int {
	nIdx := make([]int, len(*ml.idx))

	for i, in := range *ml.idx {
		nIdx[i] = in
	}

	return nIdx
}

// Returns the current node (if it is not a comment) and an empty string
// or a nil parser item and the string "NIL" to denote that all nodes have been iterated through
func eatUnnecessaryNodes(ast *[]parser.MarkupItem, node *parser.MarkupItem, idx *[]int) (parser.MarkupItem, string) {
	currNode := parser.MarkupItem{}
	status := ""

	for {
		currNode, status = eatCommentNodes(ast, idx)
		if status == "NIL" {
			return parser.MarkupItem{}, "NIL"
		}

		if currNode.Typ != parser.MarkupItemType[parser.MUComment] {
			break
		}
	}

	return currNode, ""
}

func eatCommentNodes(ast *[]parser.MarkupItem, idx *[]int) (parser.MarkupItem, string) {
	parent := (*ast)
	currNode := parser.MarkupItem{}

	i := 0
	for {
		if i >= len(*idx)-1 {
			currNode = parent[(*idx)[len(*idx)-1]]

			if currNode.Name != "" {
				return currNode, ""
			}

			if currNode.Typ != parser.MarkupItemType[parser.MUComment] {
				return currNode, ""
			} else {
				if (*idx)[len(*idx)-1] >= len(parent)-1 {
					return currNode, "NIL"
				}
			}

			(*idx)[len(*idx)-1]++

			currNode = parent[(*idx)[len(*idx)-1]]

			continue
		}

		currNode = parent[(*idx)[i]]
		parent = currNode.Children

		i++
	}
}

func transformNode(item *parser.MarkupItem) (Node, Node) {
	var html bytes.Buffer

	// Item is text node
	if (*item).Data != "" {
		truncateWhitespace(&html, item)

		return Node{
			html: html.String(),
			typ:  (*item).Typ,
		}, Node{}
	}

	// Item is element like
	return transformElement(item)

}

func transformElement(item *parser.MarkupItem) (Node, Node) {
	elmLikeNodes := []string{parser.MarkupItemType[parser.MUElement], parser.MarkupItemType[parser.MUInlineComponent], parser.MarkupItemType[parser.MUSlot]}

	if !contains(elmLikeNodes, (*item).Typ) {
		log.Fatalf("Transform element requires an \"element like\" node type. %s is not an element like node.", item.Typ)
	}

	var op bytes.Buffer
	selfClosing := false

	if len((*item).Children) == 0 || (*item).SelfClosing {
		selfClosing = true
	}

	openToken := "<"
	closeToken := ">"

	isSelfClosingIC := (*item).Typ == parser.MarkupItemType[parser.MUInlineComponent] && len((*item).Children) == 0

	if isSelfClosingIC {
		openToken = "${"
		closeToken = "}"
	}

	// NOTE: This will cause issues when using props or attributes on inline components
	if isSelfClosingIC || len((*item).Attributes) == 0 {
		op.WriteString(fmt.Sprintf("%s%s%s", openToken, (*item).Name, closeToken))
	}

	// NOTE: This will cause issues when using props or attributes on inline components
	if !isSelfClosingIC && len((*item).Attributes) > 0 {
		op.WriteString(fmt.Sprintf("%s%s %s%s", openToken, (*item).Name, transformElementAttrs((*item).Attributes), closeToken))
	}

	// Is nested inline component
	if (*item).Typ == parser.MarkupItemType[parser.MUInlineComponent] && len((*item).Children) > 0 {
		open := Node{
			html: fmt.Sprintf("${%s-open}", (*item).Name),
			typ:  (*item).Typ,
		}

		close := Node{
			html: fmt.Sprintf("${%s-close}", (*item).Name),
			typ:  (*item).Typ,
		}

		return open, close
	}

	open := Node{
		html: op.String(),
		typ:  (*item).Typ,
	}

	close := Node{}

	// https://html.spec.whatwg.org/multipage/syntax.html#elements-2
	voidElements := []string{"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "track", "wbr"}

	if contains(voidElements, item.Name) {
		return open, close
	}

	if selfClosing && item.Typ != parser.MarkupItemType[parser.MUElement] {
		return open, close
	}

	if (*item).Name != "" {
		close.html = fmt.Sprintf("</%s>", (*item).Name)
		close.typ = parser.MarkupItemType[parser.MUClosingTag]
		open.closingTag = &close
	}

	return open, close
}

func transformElementAttrs(attrs []parser.MarkupItem) string {
	var attrStr bytes.Buffer

	for i, attr := range attrs {
		attrStr.WriteString(attr.Name)

		if len(attr.Value) > 0 {
			attrStr.WriteString("=\"")

			for _, av := range attr.Value {
				attrStr.WriteString(av.Data)
			}

			attrStr.WriteString("\"")
		}

		if i == len(attrs) {
			attrStr.WriteString(" ")
		}
	}

	return attrStr.String()
}

func truncateWhitespace(html *bytes.Buffer, item *parser.MarkupItem) {
	var prev rune

	for i := 0; i < len((*item).Data); i++ {
		ch := rune(item.Data[i])

		if prev == rune('\n') && ch == '\n' {
			continue
		}

		if prev == rune('\n') && ch == '\t' {
			continue
		}

		if prev == rune('\n') && ch == ' ' {
			continue
		}

		if prev == rune('\t') && ch == '\t' {
			continue
		}

		if prev == rune(' ') && ch == ' ' {
			continue
		}

		if ch == '\r' {
			continue
		}

		prev = ch

		html.WriteRune(ch)
	}
}

func contains(s []string, e string) bool {
	for _, a := range s {
		if a == e {
			return true
		}
	}
	return false
}
