// Copyright 2015 The Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package markdown

import (
	"io"
	"regexp"
	"strconv"
	"strings"

	"gitlab.com/golang-commonmark/html"
)

type Renderer struct {
	w *monadicWriter
}

func NewRenderer(w io.Writer) *Renderer {
	return &Renderer{newMonadicWriter(w)}
}

func (r *Renderer) Render(tokens []Token, options RenderOptions) error {
	for i, tok := range tokens {
		if tok, ok := tok.(*Inline); ok {
			r.renderInline(tok.Children, options)
		} else {
			r.renderToken(tokens, i, options)
		}
	}

	r.w.Flush()

	return r.w.err
}

func (r *Renderer) renderInline(tokens []Token, o RenderOptions) {
	for i := range tokens {
		r.renderToken(tokens, i, o)
	}
}

func (r *Renderer) renderInlineAsText(tokens []Token) {
	for _, tok := range tokens {
		if text, ok := tok.(*Text); ok {
			html.WriteEscapedString(r.w, text.Content)
		} else if img, ok := tok.(*Image); ok {
			r.renderInlineAsText(img.Tokens)
		}
	}
}

var rNotSpace = regexp.MustCompile(`^\S+`)

func (r *Renderer) renderToken(tokens []Token, idx int, options RenderOptions) {
	tok := tokens[idx]

	if idx > 0 && tok.Block() && !tok.Closing() {
		switch t := tokens[idx-1].(type) {
		case *ParagraphOpen:
			if t.Hidden {
				r.w.WriteByte('\n')
			}
		case *ParagraphClose:
			if t.Hidden {
				r.w.WriteByte('\n')
			}
		}
	}

	switch tok := tok.(type) {
	case *BlockquoteClose:
		r.w.WriteString("</blockquote>")

	case *BlockquoteOpen:
		r.w.WriteString("<blockquote>")

	case *BulletListClose:
		r.w.WriteString("</ul>")

	case *BulletListOpen:
		r.w.WriteString("<ul>")

	case *CodeBlock:
		r.w.WriteString("<pre><code>")
		html.WriteEscapedString(r.w, tok.Content)
		r.w.WriteString("</code></pre>")

	case *CodeInline:
		r.w.WriteString("<code>")
		html.WriteEscapedString(r.w, tok.Content)
		r.w.WriteString("</code>")

	case *EmphasisClose:
		r.w.WriteString("</em>")

	case *EmphasisOpen:
		r.w.WriteString("<em>")

	case *Fence:
		r.w.WriteString("<pre><code")
		if tok.Params != "" {
			langName := strings.SplitN(unescapeAll(tok.Params), " ", 2)[0]
			langName = rNotSpace.FindString(langName)
			if langName != "" {
				r.w.WriteString(` class="`)
				r.w.WriteString(options.LangPrefix)
				html.WriteEscapedString(r.w, langName)
				r.w.WriteByte('"')
			}
		}
		r.w.WriteByte('>')
		html.WriteEscapedString(r.w, tok.Content)
		r.w.WriteString("</code></pre>")

	case *Hardbreak:
		if options.XHTML {
			r.w.WriteString("<br />\n")
		} else {
			r.w.WriteString("<br>\n")
		}

	case *HeadingClose:
		r.w.WriteString("</h")
		r.w.WriteByte("0123456789"[tok.HLevel])
		r.w.WriteString(">")

	case *HeadingOpen:
		r.w.WriteString("<h")
		r.w.WriteByte("0123456789"[tok.HLevel])
		r.w.WriteByte('>')

	case *Hr:
		if options.XHTML {
			r.w.WriteString("<hr />")
		} else {
			r.w.WriteString("<hr>")
		}

	case *HTMLBlock:
		r.w.WriteString(tok.Content)
		return // no newline

	case *HTMLInline:
		r.w.WriteString(tok.Content)

	case *Image:
		r.w.WriteString(`<img src="`)
		html.WriteEscapedString(r.w, tok.Src)
		r.w.WriteString(`" alt="`)
		r.renderInlineAsText(tok.Tokens)
		r.w.WriteByte('"')

		if tok.Title != "" {
			r.w.WriteString(` title="`)
			html.WriteEscapedString(r.w, tok.Title)
			r.w.WriteByte('"')
		}
		if options.XHTML {
			r.w.WriteString(" />")
		} else {
			r.w.WriteByte('>')
		}

	case *LinkClose:
		r.w.WriteString("</a>")

	case *LinkOpen:
		r.w.WriteString(`<a href="`)
		html.WriteEscapedString(r.w, tok.Href)
		r.w.WriteByte('"')
		if tok.Title != "" {
			r.w.WriteString(` title="`)
			html.WriteEscapedString(r.w, (tok.Title))
			r.w.WriteByte('"')
		}
		if tok.Target != "" {
			r.w.WriteString(` target="`)
			html.WriteEscapedString(r.w, tok.Target)
			r.w.WriteByte('"')
		}
		if options.Nofollow {
			r.w.WriteString(` rel="nofollow"`)
		}
		r.w.WriteByte('>')

	case *ListItemClose:
		r.w.WriteString("</li>")

	case *ListItemOpen:
		r.w.WriteString("<li>")

	case *OrderedListClose:
		r.w.WriteString("</ol>")

	case *OrderedListOpen:
		if tok.Order != 1 {
			r.w.WriteString(`<ol start="`)
			r.w.WriteString(strconv.Itoa(tok.Order))
			r.w.WriteString(`">`)
		} else {
			r.w.WriteString("<ol>")
		}

	case *ParagraphClose:
		if tok.Hidden {
			return
		}
		if !tok.Tight {
			r.w.WriteString("</p>")
		} else if tokens[idx+1].Closing() {
			return // no newline
		}

	case *ParagraphOpen:
		if tok.Hidden {
			return
		}
		if !tok.Tight {
			r.w.WriteString("<p>")
		}

	case *Softbreak:
		if options.Breaks {
			if options.XHTML {
				r.w.WriteString("<br />\n")
			} else {
				r.w.WriteString("<br>\n")
			}
		} else {
			r.w.WriteByte('\n')
		}
		return

	case *StrongClose:
		r.w.WriteString("</strong>")

	case *StrongOpen:
		r.w.WriteString("<strong>")

	case *StrikethroughClose:
		r.w.WriteString("</s>")

	case *StrikethroughOpen:
		r.w.WriteString("<s>")

	case *TableClose:
		r.w.WriteString("</table>")

	case *TableOpen:
		r.w.WriteString("<table>")

	case *TbodyClose:
		r.w.WriteString("</tbody>")

	case *TbodyOpen:
		r.w.WriteString("<tbody>")

	case *TdClose:
		r.w.WriteString("</td>")

	case *TdOpen:
		if tok.Align != AlignNone {
			r.w.WriteString(`<td style="text-align:`)
			r.w.WriteString(tok.Align.String())
			r.w.WriteString(`">`)
		} else {
			r.w.WriteString("<td>")
		}

	case *Text:
		html.WriteEscapedString(r.w, tok.Content)

	case *TheadClose:
		r.w.WriteString("</thead>")

	case *TheadOpen:
		r.w.WriteString("<thead>")

	case *ThClose:
		r.w.WriteString("</th>")

	case *ThOpen:
		if align := tok.Align; align != AlignNone {
			r.w.WriteString(`<th style="text-align:`)
			r.w.WriteString(align.String())
			r.w.WriteString(`">`)
		} else {
			r.w.WriteString("<th>")
		}

	case *TrClose:
		r.w.WriteString("</tr>")

	case *TrOpen:
		r.w.WriteString("<tr>")

	default:
		panic("unknown token type")
	}

	needLf := false
	if tok.Block() {
		needLf = true

		if tok.Opening() {
			nextTok := tokens[idx+1]
			blockquote := false
			switch nextTok := nextTok.(type) {
			case *Inline:
				needLf = false
			case *ParagraphOpen:
				if nextTok.Tight || nextTok.Hidden {
					needLf = false
				}
			case *ParagraphClose:
				if nextTok.Tight || nextTok.Hidden {
					needLf = false
				}
			case *BlockquoteClose:
				blockquote = true
			}
			if !blockquote && needLf && nextTok.Closing() && nextTok.Tag() == tok.Tag() {
				needLf = false
			}
		}
	}

	if needLf {
		r.w.WriteByte('\n')
	}
}
