package parsing

import (
	"bytes"
	"fmt"
	"regexp"

	"git.handmade.network/hmn/hmn/src/hmnurl"
	"github.com/yuin/goldmark"
	"github.com/yuin/goldmark/ast"
	"github.com/yuin/goldmark/parser"
	"github.com/yuin/goldmark/renderer"
	"github.com/yuin/goldmark/renderer/html"
	"github.com/yuin/goldmark/text"
	"github.com/yuin/goldmark/util"
)

// NOTE(ben): ggcode is my cute name for our custom extension syntax because I got fed up with
// bbcode. It's designed to be a more natural fit for Goldmark's method of parsing, while still
// being a general-purpose tag-like syntax that's easy for us to add instances of without writing
// new Goldmark parsers.
//
// Inline ggcode is delimited by two exclamation marks. Block ggcode is delimited by three. Inline
// ggcode uses parentheses to delimit the start and end of the affected content. Block ggcode is
// like a fenced code block and ends with !!!. ggcode sections can optionally have named string
// arguments inside braces. Quotes around the value are mandatory.
//
// Inline example:
//
//     See our article on !!glossary{slug="tcp"}(TCP) for more details.
//
// Block example:
//
//     !!!resource{name="Beej's Guide to Network Programming" url="https://beej.us/guide/bgnet/html/"}
//     This is a _fantastic_ resource on network programming, suitable for beginners.
//     !!!
//

var ggcodeTags = map[string]ggcodeTag{
	"glossary": {
		Filter: ggcodeFilterEdu,
		Renderer: func(c ggcodeRendererContext, n *ggcodeNode, entering bool) error {
			if entering {
				term := n.Args["term"]
				c.W.WriteString(fmt.Sprintf(
					`<a href="%s" class="glossary-term" data-term="%s">`,
					hmnurl.BuildEducationGlossary(term),
					term,
				))
			} else {
				c.W.WriteString("</a>")
			}
			return nil
		},
	},
	"note": {
		Filter: ggcodeFilterEdu,
		Renderer: func(c ggcodeRendererContext, n *ggcodeNode, entering bool) error {
			if entering {
				c.W.WriteString(`<span class="note">`)
			} else {
				c.W.WriteString(`</span>`)
			}
			return nil
		},
	},
	"figure": {
		Filter: ggcodeFilterEdu,
		Renderer: func(c ggcodeRendererContext, n *ggcodeNode, entering bool) error {
			if entering {
				c.W.WriteString(`<figure>`)

				src := n.Args["src"]
				alt := n.Args["alt"]
				if embedHTML, _, canEmbed := htmlForURLEmbed(src, c.Opts.Previews); canEmbed {
					c.W.WriteString(embedHTML)
				} else {
					var srcAttr, altAttr string
					if src != "" {
						srcAttr = fmt.Sprintf(` src="%s"`, src)
					}
					if alt != "" {
						altAttr = fmt.Sprintf(` alt="%s"`, alt)
					}
					c.W.WriteString(fmt.Sprintf(`<img%s%s>`, srcAttr, altAttr))
				}

				c.W.WriteString(`<figcaption>`)
			} else {
				c.W.WriteString(`</figcaption>`)
				c.W.WriteString(`</figure>`)
			}
			return nil
		},
	},
}

// ----------------------
// Types
// ----------------------

type ggcodeRendererContext struct {
	W      util.BufWriter
	Source []byte
	Opts   MarkdownOptions
}

type ggcodeTagFilter func(opts MarkdownOptions) bool
type ggcodeRenderer func(c ggcodeRendererContext, n *ggcodeNode, entering bool) error

type ggcodeTag struct {
	Filter   ggcodeTagFilter
	Renderer ggcodeRenderer
}

var ggcodeFilterEdu ggcodeTagFilter = func(opts MarkdownOptions) bool {
	return opts.Education
}

// ----------------------
// Parsers and delimiters
// ----------------------

var reGGCodeBlockOpen = regexp.MustCompile(`^!!!(?P<name>[a-zA-Z0-9-_]+)(\{(?P<args>.*?)\})?$`)
var reGGCodeInline = regexp.MustCompile(`^!!(?P<name>[a-zA-Z0-9-_]+)(\{(?P<args>.*?)\})?(\((?P<content>.*?)\))?`)
var reGGCodeArgs = regexp.MustCompile(`(?P<arg>[a-zA-Z0-9-_]+)="(?P<val>.*?)"`)

// Block parser stuff

type ggcodeBlockParser struct{}

var _ parser.BlockParser = ggcodeBlockParser{}

func (s ggcodeBlockParser) Trigger() []byte {
	return []byte("!")
}

func (s ggcodeBlockParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) {
	restOfLine, _ := reader.PeekLine()

	if match := extractMap(reGGCodeBlockOpen, bytes.TrimSpace(restOfLine)); match != nil {
		name := string(match["name"])
		var args map[string]string
		if argsMatch := extractAllMap(reGGCodeArgs, match["args"]); argsMatch != nil {
			args = make(map[string]string)
			for i := range argsMatch["arg"] {
				arg := string(argsMatch["arg"][i])
				val := string(argsMatch["val"][i])
				args[arg] = val
			}
		}

		reader.Advance(len(restOfLine))
		return &ggcodeNode{
			Name: name,
			Args: args,
		}, parser.Continue | parser.HasChildren
	} else {
		return nil, parser.NoChildren
	}
}

func (s ggcodeBlockParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State {
	line, _ := reader.PeekLine()
	if string(bytes.TrimSpace(line)) == "!!!" {
		reader.Advance(3)
		return parser.Close
	}
	return parser.Continue | parser.HasChildren
}

func (s ggcodeBlockParser) Close(node ast.Node, reader text.Reader, pc parser.Context) {}

func (s ggcodeBlockParser) CanInterruptParagraph() bool {
	return false
}

func (s ggcodeBlockParser) CanAcceptIndentedLine() bool {
	return false
}

// Inline parser stuff

type ggcodeInlineParser struct{}

var _ parser.InlineParser = ggcodeInlineParser{}

func (s ggcodeInlineParser) Trigger() []byte {
	return []byte("!()")
}

func (s ggcodeInlineParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
	restOfLine, segment := block.PeekLine() // Gets the rest of the line (starting at the current parser cursor index), and the segment representing the indices in the source text.
	if match := extractMap(reGGCodeInline, restOfLine); match != nil {
		name := string(match["name"])
		var args map[string]string
		if argsMatch := extractAllMap(reGGCodeArgs, match["args"]); argsMatch != nil {
			args = make(map[string]string)
			for i := range argsMatch["arg"] {
				arg := string(argsMatch["arg"][i])
				val := string(argsMatch["val"][i])
				args[arg] = val
			}
		}

		node := &ggcodeNode{
			Name: name,
			Args: args,
		}
		contentLength := len(match["content"])
		if contentLength > 0 {
			contentSegmentStart := segment.Start + len(match["all"]) - (contentLength + 1) // the 1 is for the end parenthesis
			contentSegmentEnd := contentSegmentStart + contentLength
			contentSegment := text.NewSegment(contentSegmentStart, contentSegmentEnd)
			node.AppendChild(node, ast.NewTextSegment(contentSegment))
		}

		block.Advance(len(match["all"]))
		return node
	} else {
		return nil
	}
}

type ggcodeDelimiterParser struct {
	Node *ggcodeNode // We need to pass this through 🙄
}

func (p ggcodeDelimiterParser) IsDelimiter(b byte) bool {
	return b == '(' || b == ')'
}

func (p ggcodeDelimiterParser) CanOpenCloser(opener, closer *parser.Delimiter) bool {
	return opener.Char == '(' && closer.Char == ')'
}

func (p ggcodeDelimiterParser) OnMatch(consumes int) ast.Node {
	return p.Node
}

// ----------------------
// AST node
// ----------------------

type ggcodeNode struct {
	ast.BaseBlock
	Name string
	Args map[string]string
}

var _ ast.Node = &ggcodeNode{}

func (n *ggcodeNode) Dump(source []byte, level int) {
	ast.DumpHelper(n, source, level, n.Args, nil)
}

var kindGGCode = ast.NewNodeKind("ggcode")

func (n *ggcodeNode) Kind() ast.NodeKind {
	return kindGGCode
}

// ----------------------
// Renderer
// ----------------------

type ggcodeHTMLRenderer struct {
	html.Config
	Opts MarkdownOptions
}

func newGGCodeHTMLRenderer(markdownOpts MarkdownOptions, opts ...html.Option) renderer.NodeRenderer {
	r := &ggcodeHTMLRenderer{
		Opts:   markdownOpts,
		Config: html.NewConfig(),
	}
	for _, opt := range opts {
		opt.SetHTMLOption(&r.Config)
	}
	return r
}

func (r *ggcodeHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
	reg.Register(kindGGCode, r.render)
}

func (r *ggcodeHTMLRenderer) render(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
	node := n.(*ggcodeNode)
	var renderer ggcodeRenderer = defaultGGCodeRenderer
	if tag, ok := ggcodeTags[node.Name]; ok {
		if tag.Filter == nil || tag.Filter(r.Opts) {
			renderer = tag.Renderer
		}
	}
	err := renderer(ggcodeRendererContext{
		W:      w,
		Source: source,
		Opts:   r.Opts,
	}, node, entering)
	return ast.WalkContinue, err
}

func defaultGGCodeRenderer(c ggcodeRendererContext, n *ggcodeNode, entering bool) error {
	if entering {
		c.W.WriteString("[unknown ggcode tag]")
	}
	return nil
}

// ----------------------
// Extension
// ----------------------

type ggcodeExtension struct {
	Opts MarkdownOptions
}

func (e ggcodeExtension) Extend(m goldmark.Markdown) {
	m.Parser().AddOptions(parser.WithBlockParsers(
		util.Prioritized(ggcodeBlockParser{}, 500),
	))
	m.Parser().AddOptions(parser.WithInlineParsers(
		util.Prioritized(ggcodeInlineParser{}, 500),
	))
	m.Renderer().AddOptions(renderer.WithNodeRenderers(
		util.Prioritized(newGGCodeHTMLRenderer(e.Opts), 500),
	))
}
