package ast

import (
	"bytes"
	"fmt"
	"io"
	"strings"
	"unicode/utf8"
)

// Print is for debugging. It prints a string representation of parsed
// markdown doc (result of parser.Parse()) to dst.
//
// To make output readable, it shortens text output.
func Print(dst io.Writer, doc Node) {
	PrintWithPrefix(dst, doc, "  ")
}

// PrintWithPrefix is like Print but allows customizing prefix used for
// indentation. By default it's 2 spaces. You can change it to e.g. tab
// by passing "\t"
func PrintWithPrefix(w io.Writer, doc Node, prefix string) {
	// for more compact output, don't print outer Document
	if _, ok := doc.(*Document); ok {
		for _, c := range doc.GetChildren() {
			printRecur(w, c, prefix, 0)
		}
	} else {
		printRecur(w, doc, prefix, 0)
	}
}

// ToString is like Dump but returns result as a string
func ToString(doc Node) string {
	var buf bytes.Buffer
	Print(&buf, doc)
	return buf.String()
}

func contentToString(d1 []byte, d2 []byte) string {
	if d1 != nil {
		return string(d1)
	}
	if d2 != nil {
		return string(d2)
	}
	return ""
}

func getContent(node Node) string {
	if c := node.AsContainer(); c != nil {
		return contentToString(c.Literal, c.Content)
	}
	leaf := node.AsLeaf()
	return contentToString(leaf.Literal, leaf.Content)
}

func shortenString(s string, maxLen int) string {
	// for cleaner, one-line ouput, replace some white-space chars
	// with their escaped version
	s = strings.Replace(s, "\n", `\n`, -1)
	s = strings.Replace(s, "\r", `\r`, -1)
	s = strings.Replace(s, "\t", `\t`, -1)
	if maxLen < 0 {
		return s
	}
	if utf8.RuneCountInString(s) < maxLen {
		return s
	}
	// add "…" to indicate truncation
	return string(append([]rune(s)[:maxLen-3], '…'))
}

// get a short name of the type of v which excludes package name
// and strips "()" from the end
func getNodeType(node Node) string {
	s := fmt.Sprintf("%T", node)
	s = strings.TrimSuffix(s, "()")
	if idx := strings.Index(s, "."); idx != -1 {
		return s[idx+1:]
	}
	return s
}

func printDefault(w io.Writer, indent string, typeName string, content string) {
	content = strings.TrimSpace(content)
	if len(content) > 0 {
		fmt.Fprintf(w, "%s%s '%s'\n", indent, typeName, content)
	} else {
		fmt.Fprintf(w, "%s%s\n", indent, typeName)
	}
}

func getListFlags(f ListType) string {
	var s string
	if f&ListTypeOrdered != 0 {
		s += "ordered "
	}
	if f&ListTypeDefinition != 0 {
		s += "definition "
	}
	if f&ListTypeTerm != 0 {
		s += "term "
	}
	if f&ListItemContainsBlock != 0 {
		s += "has_block "
	}
	if f&ListItemBeginningOfList != 0 {
		s += "start "
	}
	if f&ListItemEndOfList != 0 {
		s += "end "
	}
	s = strings.TrimSpace(s)
	return s
}

func printRecur(w io.Writer, node Node, prefix string, depth int) {
	if node == nil {
		return
	}
	indent := strings.Repeat(prefix, depth)

	content := shortenString(getContent(node), 40)
	typeName := getNodeType(node)
	switch v := node.(type) {
	case *Link:
		content := "url=" + string(v.Destination)
		printDefault(w, indent, typeName, content)
	case *Image:
		content := "url=" + string(v.Destination)
		printDefault(w, indent, typeName, content)
	case *List:
		if v.Start > 1 {
			content += fmt.Sprintf("start=%d ", v.Start)
		}
		if v.Tight {
			content += "tight "
		}
		if v.IsFootnotesList {
			content += "footnotes "
		}
		flags := getListFlags(v.ListFlags)
		if len(flags) > 0 {
			content += "flags=" + flags + " "
		}
		printDefault(w, indent, typeName, content)
	case *ListItem:
		if v.Tight {
			content += "tight "
		}
		if v.IsFootnotesList {
			content += "footnotes "
		}
		flags := getListFlags(v.ListFlags)
		if len(flags) > 0 {
			content += "flags=" + flags + " "
		}
		printDefault(w, indent, typeName, content)
	case *CodeBlock:
		printDefault(w, indent, typeName + ":" + string(v.Info), content)
	default:
		printDefault(w, indent, typeName, content)
	}
	for _, child := range node.GetChildren() {
		printRecur(w, child, prefix, depth+1)
	}
}
