package appshell

import (
	"bytes"
	"encoding/json"
	"esperframework/options"
	"io/ioutil"
	"log"
	"os"
	"path/filepath"
	"unicode"
	"unicode/utf8"
)

const (
	ADJACENT = "adjacent"
	NESTED   = "nested"
)

type positionState string

type BoundaryData struct {
	parent      string
	position    positionState
	open        string
	close       string
	closeOffset int
}

type SlotHTML struct {
	Open  string `json:"open"`
	Close string `json:"close"`
}

type SlotItemData struct {
	Path       string   `json:"path,omitempty"`
	Typ        string   `json:"type"`                 // layout or page template
	Boundaries []string `json:"boundaries,omitempty"` // tuple of the elements that precede and succeed the slot
	HTML       SlotHTML `json:"html,omitempty"`
}

type SlotData struct {
	Path      string         `json:"path,omitempty"`
	SlotItems []SlotItemData `json:"slotItems,omitempty"`
}

type ImportData struct {
	Parent string `json:"parent"`
	Path   string `json:"path"`
	Name   string `json:"name"`
}

type ASTreeBuffer struct {
	beforeHead bytes.Buffer
	headStart  bytes.Buffer
	headEnd    bytes.Buffer
	bodyStart  bytes.Buffer
	bodyEnd    bytes.Buffer
	afterBody  bytes.Buffer
}

type ASTree struct {
	BeforeHead string       `json:"beforeHead"`
	HeadStart  string       `json:"headStart"`
	Styles     SlotData     `json:"styles"`
	Scripts    SlotData     `json:"scripts"`
	Imports    []ImportData `json:"imports"`
	HeadSlot   SlotData     `json:"headSlot"`
	HeadEnd    string       `json:"headEnd"`
	BodyStart  string       `json:"bodyStart"`
	BodySlot   SlotData     `json:"bodySlot"`
	BodyEnd    string       `json:"bodyEnd"`
	AfterBody  string       `json:"afterBody"`
}

type ASLexer struct {
	Input       []byte
	Pos         int
	AppShellAST ASTreeBuffer
	Options     *options.Options
	EOF         bool
}

var (
	HEAD_DELIM = []byte("head")
	HEAD_SLOT  = []byte("%head%")
	BODY_DELIM = []byte("body")
	BODY_SLOT  = []byte("%body%")
	PUBLIC_VAR = []byte("%public%")
	STYLES_VAR = []byte("%styles%")
)

var asTreeBuf = ASTreeBuffer{}
var asTree = ASTree{}

type asStateFn func(*ASLexer) asStateFn

func Parse(l *ASLexer, isTest bool) ASTree {
	l.Input = GetAppShellHTML(l)

	l.run()

	buf := new(bytes.Buffer)
	enc := json.NewEncoder(buf)
	enc.SetEscapeHTML(false)

	asTree.BeforeHead = asTreeBuf.beforeHead.String()
	asTree.HeadStart = asTreeBuf.headStart.String()
	asTree.HeadSlot = SlotData{}
	asTree.HeadEnd = asTreeBuf.headEnd.String()
	asTree.BodyStart = asTreeBuf.bodyStart.String()
	asTree.BodySlot = SlotData{}
	asTree.BodyEnd = asTreeBuf.bodyEnd.String()
	asTree.AfterBody = asTreeBuf.afterBody.String()

	if err := enc.Encode(&asTree); err != nil {
		log.Fatalf("\nFailed to encode AST\n%s\n", err)
	}

	if isTest == false {
		err := ioutil.WriteFile(filepath.Join(l.Options.CachePath, "__app.json"), buf.Bytes(), os.ModePerm)
		if err != nil {
			log.Fatalf("\nFailed to write file at %s\n%s\n", l.Options.CachePath, err)
		}
	}

	return asTree
}

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

func parseBeforeHead(l *ASLexer) asStateFn {
	l.boundsCheck()
	if l.EOF {
		return nil
	}

	buf := &[]byte{}
	l.truncateWhitespace(buf)
	if len(*buf) > 0 {
		asTreeBuf.beforeHead.Write(*buf)
	}

	// Handle head tag
	if l.curr() == '<' {
		l.next()

		if bytes.Compare(l.Input[l.Pos:l.Pos+len(HEAD_DELIM)+1], append(HEAD_DELIM, []byte{'>'}...)) == 0 {
			l.Pos += len(HEAD_DELIM)
			l.next()
			asTreeBuf.headStart.Write([]byte{'<'})
			asTreeBuf.headStart.Write([]byte(HEAD_DELIM))
			asTreeBuf.headStart.Write([]byte{'>'})
			return parseHeadStart
		}

		asTreeBuf.beforeHead.Write([]byte{'<'})
	}

	asTreeBuf.beforeHead.Write([]byte{l.curr()})
	l.next()
	return parseBeforeHead
}

func parseHeadStart(l *ASLexer) asStateFn {
	l.boundsCheck()
	if l.EOF {
		return nil
	}

	buf := &[]byte{}
	l.truncateWhitespace(buf)
	if len(*buf) > 0 {
		asTreeBuf.headStart.Write(*buf)
	}

	if bytes.Compare(l.Input[l.Pos:l.Pos+len(HEAD_SLOT)], HEAD_SLOT) == 0 {
		l.Pos += len(HEAD_SLOT)
		return parseHeadEnd
	}

	// Handle self closing tags
	if l.curr() == '/' && l.peekNext() == '>' || l.curr() == ' ' && l.peekNext() == '>' {
		l.prev()
		l.eatWhitespaceReverse()

		asTreeBuf.headStart.Truncate(len(asTreeBuf.headStart.Bytes()) - 1)
		asTreeBuf.headStart.Write([]byte{'>'})

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

		return parseHeadStart
	}

	if l.curr() == '%' {
		path, length := handleTemplateStringPaths(l.Options, l.Input[l.Pos:])
		asTreeBuf.headStart.Write([]byte(path))
		l.Pos += length
	}

	asTreeBuf.headStart.Write([]byte{l.curr()})
	l.next()
	return parseHeadStart
}

func parseHeadEnd(l *ASLexer) asStateFn {
	l.boundsCheck()
	if l.EOF {
		return nil
	}

	buf := &[]byte{}
	l.truncateWhitespace(buf)
	if len(*buf) > 0 {
		asTreeBuf.headEnd.Write(*buf)
	}

	// Check for head end
	if l.curr() == '<' && l.peekNext() == '/' {
		l.next()
		l.next()

		if bytes.Compare(l.Input[l.Pos:l.Pos+len(HEAD_DELIM)+1], append(HEAD_DELIM, []byte{'>'}...)) == 0 {
			l.eatToClosingAngleBracket()
			l.next()
			asTreeBuf.headEnd.Write([]byte{'<'})
			asTreeBuf.headEnd.Write([]byte{'/'})
			asTreeBuf.headEnd.Write([]byte(HEAD_DELIM))
			asTreeBuf.headEnd.Write([]byte{'>'})
			return parseBodyStart
		}

		asTreeBuf.headEnd.Write([]byte{'<', '/'})
	}

	// Handle self closing tags
	if l.curr() == '/' && l.peekNext() == '>' || l.curr() == ' ' && l.peekNext() == '>' {
		l.prev()
		l.eatWhitespaceReverse()

		asTreeBuf.headEnd.Truncate(len(asTreeBuf.headEnd.Bytes()) - 1)
		asTreeBuf.headEnd.Write([]byte{'>'})

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

		return parseHeadEnd
	}

	if l.curr() == '%' {
		path, length := handleTemplateStringPaths(l.Options, l.Input[l.Pos:])
		asTreeBuf.headEnd.Write([]byte(path))
		l.Pos += length
	}

	asTreeBuf.headEnd.Write([]byte{l.curr()})
	l.next()
	return parseHeadEnd
}

func parseBodyStart(l *ASLexer) asStateFn {
	l.boundsCheck()
	if l.EOF {
		return nil
	}

	buf := &[]byte{}
	l.truncateWhitespace(buf)
	if len(*buf) > 0 {
		asTreeBuf.bodyStart.Write(*buf)
	}

	if bytes.Compare(l.Input[l.Pos:l.Pos+len(BODY_SLOT)], BODY_SLOT) == 0 {
		l.Pos += len(BODY_SLOT)
		return parseBodyEnd
	}

	// Handle self closing tags
	if l.curr() == '/' && l.peekNext() == '>' || l.curr() == ' ' && l.peekNext() == '>' {
		l.prev()
		l.eatWhitespaceReverse()

		asTreeBuf.bodyStart.Truncate(len(asTreeBuf.bodyStart.Bytes()) - 1)
		asTreeBuf.bodyStart.Write([]byte{'>'})

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

		return parseBodyStart
	}

	if l.curr() == '%' {
		path, length := handleTemplateStringPaths(l.Options, l.Input[l.Pos:])
		asTreeBuf.bodyStart.Write([]byte(path))
		l.Pos += length
	}

	asTreeBuf.bodyStart.Write([]byte{l.curr()})
	l.next()
	return parseBodyStart
}

func parseBodyEnd(l *ASLexer) asStateFn {
	l.boundsCheck()
	if l.EOF {
		return nil
	}

	buf := &[]byte{}
	l.truncateWhitespace(buf)
	if len(*buf) > 0 {
		asTreeBuf.bodyEnd.Write(*buf)
	}

	// Check for body end
	if l.curr() == '<' && l.peekNext() == '/' {
		l.next()
		l.next()

		if bytes.Compare(l.Input[l.Pos:l.Pos+len(BODY_DELIM)+1], append(BODY_DELIM, []byte{'>'}...)) == 0 {
			l.eatToClosingAngleBracket()
			l.next()
			asTreeBuf.bodyEnd.Write([]byte{'<'})
			asTreeBuf.bodyEnd.Write([]byte{'/'})
			asTreeBuf.bodyEnd.Write([]byte(BODY_DELIM))
			asTreeBuf.bodyEnd.Write([]byte{'>'})
			return parseAfterBody
		}

		asTreeBuf.bodyEnd.Write([]byte{'<', '/'})
	}

	// Handle self closing tags
	if l.curr() == '/' && l.peekNext() == '>' || l.curr() == ' ' && l.peekNext() == '>' {
		l.prev()
		l.eatWhitespaceReverse()

		asTreeBuf.bodyEnd.Truncate(len(asTreeBuf.bodyEnd.Bytes()) - 1)
		asTreeBuf.bodyEnd.Write([]byte{'>'})

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

		return parseBodyEnd
	}

	if l.curr() == '%' {
		path, length := handleTemplateStringPaths(l.Options, l.Input[l.Pos:])
		asTreeBuf.bodyEnd.Write([]byte(path))
		l.Pos += length
	}

	asTreeBuf.bodyEnd.Write([]byte{l.curr()})
	l.next()
	return parseBodyEnd
}

func parseAfterBody(l *ASLexer) asStateFn {
	l.boundsCheck()
	if l.EOF {
		return nil
	}

	if unicode.IsSpace(rune(l.curr())) {
		l.next()
		return parseAfterBody
	}

	asTreeBuf.afterBody.Write([]byte{l.curr()})

	l.next()

	return parseAfterBody
}

func getElmName(str string) string {
	ns := ""
	for i := 0; i < len(str); i++ {
		if unicode.IsSpace(rune(str[i])) || str[i] == '/' || str[i] == '>' {
			break
		}
		if str[i] == '<' {
			continue
		}
		ns += string(str[i])
	}
	return ns
}

func reverseString(s string) string {
	runes := []rune(s)
	for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
		runes[i], runes[j] = runes[j], runes[i]
	}
	return string(runes)
}

func reverseBytes(bytes []byte) []byte {
	for i, j := 0, len(bytes)-1; i < j; i, j = i+1, j-1 {
		bytes[i], bytes[j] = bytes[j], bytes[i]
	}
	return bytes
}

func rewriteBoundaryPaths(opts *options.Options, str string) string {
	rb := ""
	for i := 0; i < len(str); i++ {
		if str[i] == '%' {
			path, length := handleTemplateStringPaths(opts, []byte(str[i:]))
			rb += path
			i += length - 1
			continue
		}
		rb += string(str[i])
	}
	return rb
}

// TODO: Nonce
func handleTemplateStringPaths(opts *options.Options, str []byte) (string, int) {
	// for i := 0; i < Max(len(STYLES_VAR), len(PUBLIC_VAR)); i++ {

	// 	// Set styles path
	// 	if bytes.Compare(str[i:i+len(STYLES_VAR)], STYLES_VAR) == 0 {
	// 		stylesPath := []string{}

	// 		for i, pi := range strings.Split(opts.StylesPath, "/") {
	// 			if i == 0 {
	// 				continue
	// 			}
	// 			if pi == "styles" {
	// 				continue
	// 			}
	// 			stylesPath = append(stylesPath, pi)
	// 		}

	// 		return "/" + opts.StylesPath + strings.Join(stylesPath, "/"), len(STYLES_VAR)
	// 	}

	// 	// Set public path
	// 	if bytes.Compare(str[i:i+len(PUBLIC_VAR)], PUBLIC_VAR) == 0 {
	// 		publicPath := []string{}

	// 		for i, pi := range strings.Split(opts.PublicPath, "/") {
	// 			if i == 0 {
	// 				continue
	// 			}
	// 			if pi == "public" {
	// 				continue
	// 			}
	// 			publicPath = append(publicPath, pi)
	// 		}

	// 		return strings.Join(publicPath, "/"), len(PUBLIC_VAR)
	// 	}
	// }

	return "", 0
}

// func Max(x, y int) int {
// 	if x < y {
// 		return y
// 	}
// 	return x
// }

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

func (l *ASLexer) 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 *ASLexer) curr() byte {
	return l.Input[l.Pos]
}

func (l *ASLexer) next() {
	l.boundsCheck()
	if l.EOF {
		return
	}
	_, width := utf8.DecodeRuneInString(string(l.Input[l.Pos]))
	l.Pos += width
}

func (l *ASLexer) prev() {
	l.boundsCheck()
	if l.EOF {
		return
	}
	_, width := utf8.DecodeRuneInString(string(l.Input[l.Pos]))
	l.Pos -= width
}

func (l *ASLexer) peekNext() byte {
	return l.Input[l.Pos+1]
}

func (l *ASLexer) peekPrev() byte {
	return l.Input[l.Pos-1]
}

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

func (l *ASLexer) eatWhitespaceReverse() {
	for unicode.IsSpace(rune(l.curr())) {
		l.prev()
	}
}

type lexMethod func(buf bytes.Buffer) lexMethod

func (l *ASLexer) truncateWhitespace(buf *[]byte) lexMethod {
	l.boundsCheck()
	if l.EOF {
		return nil
	}

	for l.curr() == '\r' {
		l.next()
	}

	wsChars := []byte{'\t', '\n', ' '}

	i := 0

	for _, ws := range wsChars {
		for l.curr() == ws && l.peekNext() == ws {
			l.next()
			i++
		}

		if i != 0 || l.curr() == ws {
			*buf = append(*buf, ws)
		}

		i = 0
	}

	if unicode.IsSpace(rune(l.curr())) {
		l.next()
		return l.truncateWhitespace(buf)
	}

	return nil
}

func (l *ASLexer) eatToClosingAngleBracket() {
	for l.curr() != '>' {
		l.next()
	}
}

func (l *ASLexer) backTrackToChar(ch byte) {
	for l.curr() != ch {
		l.prev()
	}
}
