package parser

import (
	"fmt"
	"strings"

	"github.com/yuin/goldmark/ast"
	"github.com/yuin/goldmark/text"
	"github.com/yuin/goldmark/util"
)

var linkLabelStateKey = NewContextKey()

type linkLabelState struct {
	ast.BaseInline

	Segment text.Segment

	IsImage bool

	Prev *linkLabelState

	Next *linkLabelState

	First *linkLabelState

	Last *linkLabelState
}

func newLinkLabelState(segment text.Segment, isImage bool) *linkLabelState {
	return &linkLabelState{
		Segment: segment,
		IsImage: isImage,
	}
}

func (s *linkLabelState) Text(source []byte) []byte {
	return s.Segment.Value(source)
}

func (s *linkLabelState) Dump(source []byte, level int) {
	fmt.Printf("%slinkLabelState: \"%s\"\n", strings.Repeat("    ", level), s.Text(source))
}

var kindLinkLabelState = ast.NewNodeKind("LinkLabelState")

func (s *linkLabelState) Kind() ast.NodeKind {
	return kindLinkLabelState
}

func linkLabelStateLength(v *linkLabelState) int {
	if v == nil || v.Last == nil || v.First == nil {
		return 0
	}
	return v.Last.Segment.Stop - v.First.Segment.Start
}

func pushLinkLabelState(pc Context, v *linkLabelState) {
	tlist := pc.Get(linkLabelStateKey)
	var list *linkLabelState
	if tlist == nil {
		list = v
		v.First = v
		v.Last = v
		pc.Set(linkLabelStateKey, list)
	} else {
		list = tlist.(*linkLabelState)
		l := list.Last
		list.Last = v
		l.Next = v
		v.Prev = l
	}
}

func removeLinkLabelState(pc Context, d *linkLabelState) {
	tlist := pc.Get(linkLabelStateKey)
	var list *linkLabelState
	if tlist == nil {
		return
	}
	list = tlist.(*linkLabelState)

	if d.Prev == nil {
		list = d.Next
		if list != nil {
			list.First = d
			list.Last = d.Last
			list.Prev = nil
			pc.Set(linkLabelStateKey, list)
		} else {
			pc.Set(linkLabelStateKey, nil)
		}
	} else {
		d.Prev.Next = d.Next
		if d.Next != nil {
			d.Next.Prev = d.Prev
		}
	}
	if list != nil && d.Next == nil {
		list.Last = d.Prev
	}
	d.Next = nil
	d.Prev = nil
	d.First = nil
	d.Last = nil
}

type linkParser struct {
}

var defaultLinkParser = &linkParser{}

// NewLinkParser return a new InlineParser that parses links.
func NewLinkParser() InlineParser {
	return defaultLinkParser
}

func (s *linkParser) Trigger() []byte {
	return []byte{'!', '[', ']'}
}

var linkBottom = NewContextKey()

func (s *linkParser) Parse(parent ast.Node, block text.Reader, pc Context) ast.Node {
	line, segment := block.PeekLine()
	if line[0] == '!' {
		if len(line) > 1 && line[1] == '[' {
			block.Advance(1)
			pushLinkBottom(pc)
			return processLinkLabelOpen(block, segment.Start+1, true, pc)
		}
		return nil
	}
	if line[0] == '[' {
		pushLinkBottom(pc)
		return processLinkLabelOpen(block, segment.Start, false, pc)
	}

	// line[0] == ']'
	tlist := pc.Get(linkLabelStateKey)
	if tlist == nil {
		return nil
	}
	last := tlist.(*linkLabelState).Last
	if last == nil {
		_ = popLinkBottom(pc)
		return nil
	}
	block.Advance(1)
	removeLinkLabelState(pc, last)
	// CommonMark spec says:
	//  > A link label can have at most 999 characters inside the square brackets.
	if linkLabelStateLength(tlist.(*linkLabelState)) > 998 {
		ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment)
		_ = popLinkBottom(pc)
		return nil
	}

	if !last.IsImage && s.containsLink(last) { // a link in a link text is not allowed
		ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment)
		_ = popLinkBottom(pc)
		return nil
	}

	c := block.Peek()
	l, pos := block.Position()
	var link *ast.Link
	var hasValue bool
	switch c {
	case '(':
		link = s.parseLink(parent, last, block, pc)
	case '[':
		link, hasValue = s.parseReferenceLink(parent, last, block, pc)
		if link == nil && hasValue {
			ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment)
			_ = popLinkBottom(pc)
			return nil
		}
	}

	if link == nil {
		// maybe shortcut reference link
		block.SetPosition(l, pos)
		ssegment := text.NewSegment(last.Segment.Stop, segment.Start)
		maybeReference := block.Value(ssegment)
		// CommonMark spec says:
		//  > A link label can have at most 999 characters inside the square brackets.
		if len(maybeReference) > 999 {
			ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment)
			_ = popLinkBottom(pc)
			return nil
		}

		ref, ok := pc.Reference(util.ToLinkReference(maybeReference))
		if !ok {
			ast.MergeOrReplaceTextSegment(last.Parent(), last, last.Segment)
			_ = popLinkBottom(pc)
			return nil
		}
		link = ast.NewLink()
		s.processLinkLabel(parent, link, last, pc)
		link.Title = ref.Title()
		link.Destination = ref.Destination()
		link.Reference = ast.NewReferenceLink(ast.ReferenceLinkShortcut, maybeReference)
	}
	var n ast.Node
	if last.IsImage {
		last.Parent().RemoveChild(last.Parent(), last)
		n = ast.NewImage(link)
	} else {
		last.Parent().RemoveChild(last.Parent(), last)
		n = link
	}
	n.(interface{ SetPos(int) }).SetPos(last.Segment.Start)
	return n
}

func (s *linkParser) containsLink(n ast.Node) bool {
	if n == nil {
		return false
	}
	for c := n; c != nil; c = c.NextSibling() {
		if _, ok := c.(*ast.Link); ok {
			return true
		}
		if s.containsLink(c.FirstChild()) {
			return true
		}
	}
	return false
}

func processLinkLabelOpen(block text.Reader, pos int, isImage bool, pc Context) *linkLabelState {
	start := pos
	if isImage {
		start--
	}
	state := newLinkLabelState(text.NewSegment(start, pos+1), isImage)
	pushLinkLabelState(pc, state)
	block.Advance(1)
	return state
}

func (s *linkParser) processLinkLabel(parent ast.Node, link *ast.Link, last *linkLabelState, pc Context) {
	bottom := popLinkBottom(pc)
	ProcessDelimiters(bottom, pc)
	for c := last.NextSibling(); c != nil; {
		next := c.NextSibling()
		parent.RemoveChild(parent, c)
		link.AppendChild(link, c)
		c = next
	}
}

var linkFindClosureOptions text.FindClosureOptions = text.FindClosureOptions{
	Nesting: false,
	Newline: true,
	Advance: true,
}

func (s *linkParser) parseReferenceLink(parent ast.Node, last *linkLabelState,
	block text.Reader, pc Context) (*ast.Link, bool) {
	_, orgpos := block.Position()
	block.Advance(1) // skip '['
	segments, found := block.FindClosure('[', ']', linkFindClosureOptions)
	if !found {
		return nil, false
	}

	var maybeReference []byte
	refType := ast.ReferenceLinkFull
	if segments.Len() == 1 { // avoid allocate a new byte slice
		maybeReference = block.Value(segments.At(0))
	} else {
		maybeReference = []byte{}
		for i := range segments.Len() {
			s := segments.At(i)
			maybeReference = append(maybeReference, block.Value(s)...)
		}
	}
	if util.IsBlank(maybeReference) { // collapsed reference link
		s := text.NewSegment(last.Segment.Stop, orgpos.Start-1)
		maybeReference = block.Value(s)
		refType = ast.ReferenceLinkCollapsed
	}
	// CommonMark spec says:
	//  > A link label can have at most 999 characters inside the square brackets.
	if len(maybeReference) > 999 {
		return nil, true
	}

	ref, ok := pc.Reference(util.ToLinkReference(maybeReference))
	if !ok {
		return nil, true
	}

	link := ast.NewLink()
	s.processLinkLabel(parent, link, last, pc)
	link.Title = ref.Title()
	link.Destination = ref.Destination()
	link.Reference = ast.NewReferenceLink(refType, maybeReference)
	return link, true
}

func (s *linkParser) parseLink(parent ast.Node, last *linkLabelState, block text.Reader, pc Context) *ast.Link {
	block.Advance(1) // skip '('
	block.SkipSpaces()
	var title []byte
	var destination []byte
	var ok bool
	if block.Peek() == ')' { // empty link like '[link]()'
		block.Advance(1)
	} else {
		destination, ok = parseLinkDestination(block)
		if !ok {
			return nil
		}
		block.SkipSpaces()
		if block.Peek() == ')' {
			block.Advance(1)
		} else {
			title, ok = parseLinkTitle(block)
			if !ok {
				return nil
			}
			block.SkipSpaces()
			if block.Peek() == ')' {
				block.Advance(1)
			} else {
				return nil
			}
		}
	}

	link := ast.NewLink()
	s.processLinkLabel(parent, link, last, pc)
	link.Destination = destination
	link.Title = title
	return link
}

func parseLinkDestination(block text.Reader) ([]byte, bool) {
	block.SkipSpaces()
	line, _ := block.PeekLine()
	if block.Peek() == '<' {
		i := 1
		for i < len(line) {
			c := line[i]
			if c == '\\' && i < len(line)-1 && util.IsPunct(line[i+1]) {
				i += 2
				continue
			} else if c == '>' {
				block.Advance(i + 1)
				return line[1:i], true
			}
			i++
		}
		return nil, false
	}
	opened := 0
	i := 0
	for i < len(line) {
		c := line[i]
		if c == '\\' && i < len(line)-1 && util.IsPunct(line[i+1]) {
			i += 2
			continue
		} else if c == '(' {
			opened++
		} else if c == ')' {
			opened--
			if opened < 0 {
				break
			}
		} else if util.IsSpace(c) {
			break
		}
		i++
	}
	block.Advance(i)
	return line[:i], len(line[:i]) != 0
}

func parseLinkTitle(block text.Reader) ([]byte, bool) {
	block.SkipSpaces()
	opener := block.Peek()
	if opener != '"' && opener != '\'' && opener != '(' {
		return nil, false
	}
	closer := opener
	if opener == '(' {
		closer = ')'
	}
	block.Advance(1)
	segments, found := block.FindClosure(opener, closer, linkFindClosureOptions)
	if found {
		if segments.Len() == 1 {
			return block.Value(segments.At(0)), true
		}
		var title []byte
		for i := range segments.Len() {
			s := segments.At(i)
			title = append(title, block.Value(s)...)
		}
		return title, true
	}
	return nil, false
}

func pushLinkBottom(pc Context) {
	bottoms := pc.Get(linkBottom)
	b := pc.LastDelimiter()
	if bottoms == nil {
		pc.Set(linkBottom, b)
		return
	}
	if s, ok := bottoms.([]ast.Node); ok {
		pc.Set(linkBottom, append(s, b))
		return
	}
	pc.Set(linkBottom, []ast.Node{bottoms.(ast.Node), b})
}

func popLinkBottom(pc Context) ast.Node {
	bottoms := pc.Get(linkBottom)
	if bottoms == nil {
		return nil
	}
	if v, ok := bottoms.(ast.Node); ok {
		pc.Set(linkBottom, nil)
		return v
	}
	s := bottoms.([]ast.Node)
	v := s[len(s)-1]
	n := s[0 : len(s)-1]
	switch len(n) {
	case 0:
		pc.Set(linkBottom, nil)
	case 1:
		pc.Set(linkBottom, n[0])
	default:
		pc.Set(linkBottom, s[0:len(s)-1])
	}
	return v
}

func (s *linkParser) CloseBlock(parent ast.Node, block text.Reader, pc Context) {
	pc.Set(linkBottom, nil)
	tlist := pc.Get(linkLabelStateKey)
	if tlist == nil {
		return
	}
	for s := tlist.(*linkLabelState); s != nil; {
		next := s.Next
		removeLinkLabelState(pc, s)
		s.Parent().ReplaceChild(s.Parent(), s, ast.NewTextSegment(s.Segment))
		s = next
	}
}
