package parser

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"net/http"
	"path/filepath"
	"strings"
)

type Parser struct {
	scanner      *scanner
	filename     string
	fs           http.FileSystem
	currenttoken *token
	namedBlocks  map[string]*NamedBlock
	parent       *Parser
	result       *Block
}

func newParser(rdr io.Reader) *Parser {
	p := new(Parser)
	p.scanner = newScanner(rdr)
	p.namedBlocks = make(map[string]*NamedBlock)
	return p
}

func StringParser(input string) (*Parser, error) {
	return newParser(bytes.NewReader([]byte(input))), nil
}

func ByteParser(input []byte) (*Parser, error) {
	return newParser(bytes.NewReader(input)), nil
}

func (p *Parser) SetFilename(filename string) {
	p.filename = filename
}

func (p *Parser) SetVirtualFilesystem(fs http.FileSystem) {
	p.fs = fs
}

func FileParser(filename string) (*Parser, error) {
	data, err := ioutil.ReadFile(filename)

	if err != nil {
		return nil, err
	}

	parser := newParser(bytes.NewReader(data))
	parser.filename = filename
	return parser, nil
}

func VirtualFileParser(filename string, fs http.FileSystem) (*Parser, error) {
	file, err := fs.Open(filename)
	if err != nil {
		return nil, err
	}

	data, err := ioutil.ReadAll(file)
	if err != nil {
		return nil, err
	}

	parser := newParser(bytes.NewReader(data))
	parser.filename = filename
	parser.fs = fs
	return parser, nil
}

func (p *Parser) Parse() *Block {
	if p.result != nil {
		return p.result
	}

	defer func() {
		if r := recover(); r != nil {
			if rs, ok := r.(string); ok && rs[:len("Amber Error")] == "Amber Error" {
				panic(r)
			}

			pos := p.pos()

			if len(pos.Filename) > 0 {
				panic(fmt.Sprintf("Amber Error in <%s>: %v - Line: %d, Column: %d, Length: %d", pos.Filename, r, pos.LineNum, pos.ColNum, pos.TokenLength))
			} else {
				panic(fmt.Sprintf("Amber Error: %v - Line: %d, Column: %d, Length: %d", r, pos.LineNum, pos.ColNum, pos.TokenLength))
			}
		}
	}()

	block := newBlock()
	p.advance()

	for {
		if p.currenttoken == nil || p.currenttoken.Kind == tokEOF {
			break
		}

		if p.currenttoken.Kind == tokBlank {
			p.advance()
			continue
		}

		block.push(p.parse())
	}

	if p.parent != nil {
		p.parent.Parse()

		for _, prev := range p.parent.namedBlocks {
			ours := p.namedBlocks[prev.Name]

			if ours == nil {
				// Put a copy of the named block into current context, so that sub-templates can use the block
				p.namedBlocks[prev.Name] = prev
				continue
			}

			top := findTopmostParentWithNamedBlock(p, prev.Name)
			nb := top.namedBlocks[prev.Name]
			switch ours.Modifier {
			case NamedBlockAppend:
				for i := 0; i < len(ours.Children); i++ {
					nb.push(ours.Children[i])
				}
			case NamedBlockPrepend:
				for i := len(ours.Children) - 1; i >= 0; i-- {
					nb.pushFront(ours.Children[i])
				}
			default:
				nb.Children = ours.Children
			}
		}

		block = p.parent.result
	}

	p.result = block
	return block
}

func (p *Parser) pos() SourcePosition {
	pos := p.scanner.Pos()
	pos.Filename = p.filename
	return pos
}

func (p *Parser) parseRelativeFile(filename string) *Parser {
	if len(p.filename) == 0 {
		panic("Unable to import or extend " + filename + " in a non filesystem based parser.")
	}

	filename = filepath.Join(filepath.Dir(p.filename), filename)

	if strings.IndexRune(filepath.Base(filename), '.') < 0 {
		filename = filename + ".amber"
	}

	parser, err := FileParser(filename)
	if err != nil && p.fs != nil {
		parser, err = VirtualFileParser(filename, p.fs)
	}
	if err != nil {
		panic("Unable to read " + filename + ", Error: " + string(err.Error()))
	}

	return parser
}

func (p *Parser) parse() Node {
	switch p.currenttoken.Kind {
	case tokDoctype:
		return p.parseDoctype()
	case tokComment:
		return p.parseComment()
	case tokText:
		return p.parseText()
	case tokIf:
		return p.parseIf()
	case tokEach:
		return p.parseEach()
	case tokImport:
		return p.parseImport()
	case tokTag:
		return p.parseTag()
	case tokAssignment:
		return p.parseAssignment()
	case tokNamedBlock:
		return p.parseNamedBlock()
	case tokExtends:
		return p.parseExtends()
	case tokIndent:
		return p.parseBlock(nil)
	case tokMixin:
		return p.parseMixin()
	case tokMixinCall:
		return p.parseMixinCall()
	}

	panic(fmt.Sprintf("Unexpected token: %d", p.currenttoken.Kind))
}

func (p *Parser) expect(typ rune) *token {
	if p.currenttoken.Kind != typ {
		panic("Unexpected token!")
	}
	curtok := p.currenttoken
	p.advance()
	return curtok
}

func (p *Parser) advance() {
	p.currenttoken = p.scanner.Next()
}

func (p *Parser) parseExtends() *Block {
	if p.parent != nil {
		panic("Unable to extend multiple parent templates.")
	}

	tok := p.expect(tokExtends)
	parser := p.parseRelativeFile(tok.Value)
	parser.Parse()
	p.parent = parser
	return newBlock()
}

func (p *Parser) parseBlock(parent Node) *Block {
	p.expect(tokIndent)
	block := newBlock()
	block.SourcePosition = p.pos()

	for {
		if p.currenttoken == nil || p.currenttoken.Kind == tokEOF || p.currenttoken.Kind == tokOutdent {
			break
		}

		if p.currenttoken.Kind == tokBlank {
			p.advance()
			continue
		}

		if p.currenttoken.Kind == tokId ||
			p.currenttoken.Kind == tokClassName ||
			p.currenttoken.Kind == tokAttribute {

			if tag, ok := parent.(*Tag); ok {
				attr := p.expect(p.currenttoken.Kind)
				cond := attr.Data["Condition"]

				switch attr.Kind {
				case tokId:
					tag.Attributes = append(tag.Attributes, Attribute{p.pos(), "id", attr.Value, true, cond})
				case tokClassName:
					tag.Attributes = append(tag.Attributes, Attribute{p.pos(), "class", attr.Value, true, cond})
				case tokAttribute:
					tag.Attributes = append(tag.Attributes, Attribute{p.pos(), attr.Value, attr.Data["Content"], attr.Data["Mode"] == "raw", cond})
				}

				continue
			} else {
				panic("Conditional attributes must be placed immediately within a parent tag.")
			}
		}

		block.push(p.parse())
	}

	p.expect(tokOutdent)

	return block
}

func (p *Parser) parseIf() *Condition {
	tok := p.expect(tokIf)
	cnd := newCondition(tok.Value)
	cnd.SourcePosition = p.pos()

readmore:
	switch p.currenttoken.Kind {
	case tokIndent:
		cnd.Positive = p.parseBlock(cnd)
		goto readmore
	case tokElse:
		p.expect(tokElse)
		if p.currenttoken.Kind == tokIf {
			cnd.Negative = newBlock()
			cnd.Negative.push(p.parseIf())
		} else if p.currenttoken.Kind == tokIndent {
			cnd.Negative = p.parseBlock(cnd)
		} else {
			panic("Unexpected token!")
		}
		goto readmore
	}

	return cnd
}

func (p *Parser) parseEach() *Each {
	tok := p.expect(tokEach)
	ech := newEach(tok.Value)
	ech.SourcePosition = p.pos()
	ech.X = tok.Data["X"]
	ech.Y = tok.Data["Y"]

	if p.currenttoken.Kind == tokIndent {
		ech.Block = p.parseBlock(ech)
	}

	return ech
}

func (p *Parser) parseImport() *Block {
	tok := p.expect(tokImport)
	node := p.parseRelativeFile(tok.Value).Parse()
	node.SourcePosition = p.pos()
	return node
}

func (p *Parser) parseNamedBlock() *Block {
	tok := p.expect(tokNamedBlock)

	if p.namedBlocks[tok.Value] != nil {
		panic("Multiple definitions of named blocks are not permitted. Block " + tok.Value + " has been re defined.")
	}

	block := newNamedBlock(tok.Value)
	block.SourcePosition = p.pos()

	if tok.Data["Modifier"] == "append" {
		block.Modifier = NamedBlockAppend
	} else if tok.Data["Modifier"] == "prepend" {
		block.Modifier = NamedBlockPrepend
	}

	if p.currenttoken.Kind == tokIndent {
		block.Block = *(p.parseBlock(nil))
	}

	p.namedBlocks[block.Name] = block

	if block.Modifier == NamedBlockDefault {
		return &block.Block
	}

	return newBlock()
}

func (p *Parser) parseDoctype() *Doctype {
	tok := p.expect(tokDoctype)
	node := newDoctype(tok.Value)
	node.SourcePosition = p.pos()
	return node
}

func (p *Parser) parseComment() *Comment {
	tok := p.expect(tokComment)
	cmnt := newComment(tok.Value)
	cmnt.SourcePosition = p.pos()
	cmnt.Silent = tok.Data["Mode"] == "silent"

	if p.currenttoken.Kind == tokIndent {
		cmnt.Block = p.parseBlock(cmnt)
	}

	return cmnt
}

func (p *Parser) parseText() *Text {
	tok := p.expect(tokText)
	node := newText(tok.Value, tok.Data["Mode"] == "raw")
	node.SourcePosition = p.pos()
	return node
}

func (p *Parser) parseAssignment() *Assignment {
	tok := p.expect(tokAssignment)
	node := newAssignment(tok.Data["X"], tok.Value)
	node.SourcePosition = p.pos()
	return node
}

func (p *Parser) parseTag() *Tag {
	tok := p.expect(tokTag)
	tag := newTag(tok.Value)
	tag.SourcePosition = p.pos()

	ensureBlock := func() {
		if tag.Block == nil {
			tag.Block = newBlock()
		}
	}

readmore:
	switch p.currenttoken.Kind {
	case tokIndent:
		if tag.IsRawText() {
			p.scanner.readRaw = true
		}

		block := p.parseBlock(tag)
		if tag.Block == nil {
			tag.Block = block
		} else {
			for _, c := range block.Children {
				tag.Block.push(c)
			}
		}
	case tokId:
		id := p.expect(tokId)
		if len(id.Data["Condition"]) > 0 {
			panic("Conditional attributes must be placed in a block within a tag.")
		}
		tag.Attributes = append(tag.Attributes, Attribute{p.pos(), "id", id.Value, true, ""})
		goto readmore
	case tokClassName:
		cls := p.expect(tokClassName)
		if len(cls.Data["Condition"]) > 0 {
			panic("Conditional attributes must be placed in a block within a tag.")
		}
		tag.Attributes = append(tag.Attributes, Attribute{p.pos(), "class", cls.Value, true, ""})
		goto readmore
	case tokAttribute:
		attr := p.expect(tokAttribute)
		if len(attr.Data["Condition"]) > 0 {
			panic("Conditional attributes must be placed in a block within a tag.")
		}
		tag.Attributes = append(tag.Attributes, Attribute{p.pos(), attr.Value, attr.Data["Content"], attr.Data["Mode"] == "raw", ""})
		goto readmore
	case tokText:
		if p.currenttoken.Data["Mode"] != "piped" {
			ensureBlock()
			tag.Block.pushFront(p.parseText())
			goto readmore
		}
	}

	return tag
}

func (p *Parser) parseMixin() *Mixin {
	tok := p.expect(tokMixin)
	mixin := newMixin(tok.Value, tok.Data["Args"])
	mixin.SourcePosition = p.pos()

	if p.currenttoken.Kind == tokIndent {
		mixin.Block = p.parseBlock(mixin)
	}

	return mixin
}

func (p *Parser) parseMixinCall() *MixinCall {
	tok := p.expect(tokMixinCall)
	mixinCall := newMixinCall(tok.Value, tok.Data["Args"])
	mixinCall.SourcePosition = p.pos()
	return mixinCall
}

func findTopmostParentWithNamedBlock(p *Parser, name string) *Parser {
	top := p

	for {
		if top.namedBlocks[name] == nil {
			return nil
		}
		if top.parent == nil {
			return top
		}
		if top.parent.namedBlocks[name] != nil {
			top = top.parent
		} else {
			return top
		}
	}
}
