// 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 (
	"strings"
	"unicode"
	"unicode/utf8"
)

func nextQuoteIndex(s []rune, from int) int {
	for i := from; i < len(s); i++ {
		r := s[i]
		if r == '\'' || r == '"' {
			return i
		}
	}
	return -1
}

func firstRune(s string) rune {
	for _, r := range s {
		return r
	}
	return utf8.RuneError
}

func replaceQuotes(tokens []Token, s *StateCore) {
	type stackItem struct {
		token  int
		text   []rune
		pos    int
		single bool
		level  int
	}
	var stack []stackItem
	var changed map[int][]rune

	for i, tok := range tokens {
		thisLevel := tok.Level()

		j := len(stack) - 1
		for j >= 0 {
			if stack[j].level <= thisLevel {
				break
			}
			j--
		}
		stack = stack[:j+1]

		tok, ok := tok.(*Text)
		if !ok || !strings.ContainsAny(tok.Content, `"'`) {
			continue
		}

		text := []rune(tok.Content)
		pos := 0
		max := len(text)

	loop:
		for pos < max {
			index := nextQuoteIndex(text, pos)
			if index < 0 {
				break
			}

			canOpen := true
			canClose := true
			pos = index + 1
			isSingle := text[index] == '\''

			lastChar := ' '
			if index-1 > 0 {
				lastChar = text[index-1]
			} else {
			loop1:
				for j := i - 1; j >= 0; j-- {
					switch tok := tokens[j].(type) {
					case *Softbreak:
						break loop1
					case *Hardbreak:
						break loop1
					case *Text:
						lastChar, _ = utf8.DecodeLastRuneInString(tok.Content)
						break loop1
					default:
						continue
					}
				}
			}

			nextChar := ' '
			if pos < max {
				nextChar = text[pos]
			} else {
			loop2:
				for j := i + 1; j < len(tokens); j++ {
					switch tok := tokens[j].(type) {
					case *Softbreak:
						break loop2
					case *Hardbreak:
						break loop2
					case *Text:
						nextChar, _ = utf8.DecodeRuneInString(tok.Content)
						break loop2
					default:
						continue
					}
				}
			}

			isLastPunct := isMdAsciiPunct(lastChar) || unicode.IsPunct(lastChar)
			isNextPunct := isMdAsciiPunct(nextChar) || unicode.IsPunct(nextChar)
			isLastWhiteSpace := unicode.IsSpace(lastChar)
			isNextWhiteSpace := unicode.IsSpace(nextChar)

			if isNextWhiteSpace {
				canOpen = false
			} else if isNextPunct {
				if !(isLastWhiteSpace || isLastPunct) {
					canOpen = false
				}
			}

			if isLastWhiteSpace {
				canClose = false
			} else if isLastPunct {
				if !(isNextWhiteSpace || isNextPunct) {
					canClose = false
				}
			}

			if nextChar == '"' && text[index] == '"' {
				if lastChar >= '0' && lastChar <= '9' {
					canClose = false
					canOpen = false
				}
			}

			if canOpen && canClose {
				canOpen = false
				canClose = isNextPunct
			}

			if !canOpen && !canClose {
				if isSingle {
					text[index] = '’'
					if changed == nil {
						changed = make(map[int][]rune)
					}
					changed[i] = text
				}
				continue
			}

			if canClose {
				for j := len(stack) - 1; j >= 0; j-- {
					item := stack[j]
					if item.level < thisLevel {
						break
					}
					if item.single == isSingle && item.level == thisLevel {
						if changed == nil {
							changed = make(map[int][]rune)
						}

						var q1, q2 string
						if isSingle {
							q1 = s.Md.options.Quotes[2]
							q2 = s.Md.options.Quotes[3]
						} else {
							q1 = s.Md.options.Quotes[0]
							q2 = s.Md.options.Quotes[1]
						}

						if utf8.RuneCountInString(q1) == 1 && utf8.RuneCountInString(q2) == 1 {
							item.text[item.pos] = firstRune(q1)
							text[index] = firstRune(q2)
						} else if tok == tokens[item.token] {
							newText := make([]rune, 0, len(text)-2+len(q1)+len(q2))
							newText = append(newText, text[:item.pos]...)
							newText = append(newText, []rune(q1)...)
							newText = append(newText, text[item.pos+1:index]...)
							newText = append(newText, []rune(q2)...)
							newText = append(newText, text[index+1:]...)

							text = newText
							item.text = newText
						} else {
							newText := make([]rune, 0, len(item.text)-1+len(q1))
							newText = append(newText, item.text[:item.pos]...)
							newText = append(newText, []rune(q1)...)
							newText = append(newText, item.text[item.pos+1:]...)
							item.text = newText

							newText = make([]rune, 0, len(text)-1+len(q2))
							newText = append(newText, text[:index]...)
							newText = append(newText, []rune(q2)...)
							newText = append(newText, text[index+1:]...)

							text = newText
						}

						max = len(text)

						if changed == nil {
							changed = make(map[int][]rune)
						}
						changed[i] = text
						changed[item.token] = item.text
						stack = stack[:j]
						continue loop
					}
				}
			}

			if canOpen {
				stack = append(stack, stackItem{
					token:  i,
					text:   text,
					pos:    index,
					single: isSingle,
					level:  thisLevel,
				})
			} else if canClose && isSingle {
				text[index] = '’'
				if changed == nil {
					changed = make(map[int][]rune)
				}
				changed[i] = text
			}
		}
	}

	if changed != nil {
		for i, text := range changed {
			tokens[i].(*Text).Content = string(text)
		}
	}
}

func ruleSmartQuotes(s *StateCore) {
	if !s.Md.Typographer {
		return
	}

	tokens := s.Tokens
	for i := len(tokens) - 1; i >= 0; i-- {
		tok := tokens[i]
		if tok, ok := tok.(*Inline); ok {
			replaceQuotes(tok.Children, s)
		}
	}
}