package renderdocs

import (
	"fmt"
	"strings"

	"github.com/antchfx/htmlquery"
	"golang.org/x/net/html"
)

type Package struct {
	Name        string
	Description string

	Type       string
	Default    string
	Example    string
	DeclaredBy []string
}

// Parse parses HTML generated by nixos-render-docs
func Parse(doc *html.Node) (map[string]Package, error) {
	// Some options might have nested "dl"s with class=variablelist,
	// so it's important to find the first one and perform
	// subsequent searches from it
	dl := htmlquery.FindOne(doc, `//dl[@class="variablelist"]`)
	nameNodes := htmlquery.Find(dl, `/dt`)
	contentNodes := htmlquery.Find(dl, `/dd`)

	if len(nameNodes) != len(contentNodes) {
		return nil, fmt.Errorf("len(%d) != len(%d)", len(nameNodes), len(contentNodes))
	}

	pkgs := map[string]Package{}
	for i, nameNode := range nameNodes {
		pkg := Package{}

		span := htmlquery.FindOne(nameNode, `/span[@class="term"]`)
		pkgName := htmlquery.InnerText(span)
		pkgName = strings.TrimSpace(pkgName)
		pkg.Name = pkgName

		contentNode := contentNodes[i]
		props := extractProperties(contentNode)

		pkg.Type = NormProp(props.Type)
		pkg.Default = NormProp(props.Default)
		pkg.Example = NormProp(props.Example)
		for _, decl := range props.DeclaredBy {
			pkg.DeclaredBy = append(pkg.DeclaredBy, NormProp(decl))
		}

		pkg.Description = htmlquery.OutputHTML(contentNode, true)

		pkgs[pkgName] = pkg
	}

	return pkgs, nil
}

func NormProp(prop string) string {
	prop = strings.TrimSpace(prop)
	if len(prop) > 1 && prop[0] == '`' && prop[len(prop)-1] == '`' {
		prop = prop[1 : len(prop)-1]
	}
	return replaceQuotes(prop)
}

func replaceQuotes(text string) string {
	const doubleQuote = `"`
	const singleQuote = `'`

	return strings.NewReplacer(
		"“", doubleQuote,
		"”", doubleQuote,
		"‘", singleQuote,
		"’", singleQuote,
		"–", "--",
		"…", "..",
	).Replace(text)
}

const (
	PropType       = "Type:"
	PropDefault    = "Default:"
	PropExample    = "Example:"
	PropDeclaredBy = "Declared by:"
)

var knownProps = map[string]bool{
	PropType:       true,
	PropDefault:    true,
	PropExample:    true,
	PropDeclaredBy: true,
}

// extractProperties finds, removes from the node and returns
// known package properties such as default value, example, etc.
//
// Known cases are:
//
// == 1 ==
//
//	<p>
//	  <span><em> PROP_NAME <em></span>
//	  <code> PROP_VALUE </code>
//	</p>
//
// =======
//
// == 2 ==
//
//	 <p>
//			<span><em> PROP_NAME </em></span>
//		</p>
//		<pre><code> PROP_VALUE </code></pre>
//
// =======
//
// == 3 ==
//
//	 <p> PROP_VALUE </p>
//			<span><em> PROP_NAME </em></span>
//
// =======
//
// == 4 ==
//
//	 <p> PROP_VALUE </p>
//			<span><em> PROP_NAME </em></span>
//	 <table>
//			<tr><td><...> DECLARATION </td></tr>
//			...
//	 </table>
//
// =======
func extractProperties(pkgContent *html.Node) Package {
	props := Package{}
	emphs := htmlquery.Find(pkgContent, `/p/span[@class="emphasis"]/em`)

	for _, emph := range emphs {
		propName := htmlquery.InnerText(emph)
		if !knownProps[propName] {
			continue
		}

		span := htmlquery.FindOne(emph, "parent::span")
		p := htmlquery.FindOne(span, "parent::p")

		pre := htmlquery.FindOne(p, "following-sibling::*[1][self::pre]")
		if pre != nil {
			setProp(&props, propName, innerText(pre))

			cp := p.Parent
			cp.RemoveChild(p)
			cp.RemoveChild(pre)

			continue
		}

		table := htmlquery.FindOne(p, "following-sibling::*[1][self::table]")
		if table != nil {
			links := htmlquery.Find(table, "//tr/td//a/@href")
			for _, link := range links {
				setProp(&props, propName, innerText(link))
			}

			cp := p.Parent
			cp.RemoveChild(p)
			cp.RemoveChild(table)

			continue
		}

		span.RemoveChild(emph)
		setProp(&props, propName, innerText(p))

		p.Parent.RemoveChild(p)
	}

	return props
}

func setProp(props *Package, name, value string) {
	switch name {
	case PropType:
		props.Type = value

	case PropDefault:
		props.Default = value

	case PropExample:
		props.Example = value

	case PropDeclaredBy:
		props.DeclaredBy = append(props.DeclaredBy, value)
	}
}

func innerText(n *html.Node) string {
	var output func(*strings.Builder, *html.Node)
	output = func(b *strings.Builder, n *html.Node) {
		switch n.Type {
		case html.ElementNode:
			switch n.Data {
			case "code":
				class := htmlquery.SelectAttr(n, "class")
				if class == "literal" {
					text := htmlquery.InnerText(n)
					text = fmt.Sprintf("`%s`", text)
					b.WriteString(text)
					return
				}
			}

		case html.TextNode:
			b.WriteString(n.Data)
			return
		case html.CommentNode:
			return
		}
		for child := n.FirstChild; child != nil; child = child.NextSibling {
			output(b, child)
		}
	}

	var b strings.Builder
	output(&b, n)
	return b.String()
}
