package parser

import "../mime"
import "../token"
import "core:strings"

StyleToggle :: struct {
	amount: int,
	type:   token.TokenType,
}

parse_inline_style :: proc(tok: ^token.Token, symbol: byte, toggles: []StyleToggle) {
	if tok.children != nil {
		for i := 0; i < len(tok.children); i += 1 {
			parse_inline_style(tok.children[i], symbol, toggles)
		}
	}

	if tok.type == .Text {
		str, ok := tok.value.(string)
		if !ok {
			return
		}

		symbol_found := false
		for i := 0; i < len(str); i += 1 {
			if str[i] == symbol {
				symbol_found = true
				break
			}
		}
		if !symbol_found {
			return
		}

		tok.value = nil

		active_styles: [256]bool
		current_node := tok

		i := 0
		start := 0

		for i < len(str) {
			if str[i] == symbol {
				if i > start {
					push_token(current_node, token.TokenType.Text, str[start:i])
				}

				count := 0
				symbol_start := i
				for i < len(str) && str[i] == symbol {
					count += 1
					i += 1
				}

				preceded_by_space := symbol_start > 0 && is_space_byte(str[symbol_start - 1])
				followed_by_space := i < len(str) && is_space_byte(str[i])
				if preceded_by_space && followed_by_space {
					push_token(current_node, token.TokenType.Text, str[symbol_start:i])
					start = i
					continue
				}

				for count > 0 {
					matched := false
					for toggle in toggles {
						if count >= toggle.amount {
							active_styles[int(toggle.type)] = !active_styles[int(toggle.type)]
							count -= toggle.amount
							matched = true
							break
						}
					}
					if !matched {
						push_token(
							current_node,
							token.TokenType.Text,
							str[symbol_start:symbol_start + count],
						)
						count = 0
						break
					}
				}

				current_node = tok
				for toggle in toggles {
					if active_styles[int(toggle.type)] {
						last :=
							current_node.children[len(current_node.children) - 1] if len(current_node.children) > 0 else nil
						current_node =
							last if last != nil && last.type == toggle.type else push_token(current_node, toggle.type, nil)
					}
				}

				start = i
			} else {
				i += 1
			}
		}

		if i > start {
			push_token(current_node, token.TokenType.Text, str[start:i])
		}
	}
}

parse_inline_code :: proc(tok: ^token.Token) {
	if tok.type == .CodeBlock || tok.type == .InlineCode {
		return
	}
	if tok.children != nil {
		for i := 0; i < len(tok.children); i += 1 {
			parse_inline_code(tok.children[i])
		}
	}

	if tok.type == .Text {
		str, ok := tok.value.(string)
		if !ok {
			return
		}

		start_idx := strings.index_byte(str, '`')
		if start_idx == -1 {
			return
		}

		end_idx := strings.index_byte(str[start_idx + 1:], '`')
		if end_idx == -1 {
			return
		}
		end_idx += start_idx + 1

		code := str[start_idx + 1:end_idx]

		tok.value = nil

		if start_idx > 0 {
			push_token(tok, .Text, str[:start_idx])
		}
		push_token(tok, .InlineCode, code)

		if end_idx + 1 < len(str) {
			rem := push_token(tok, .Text, str[end_idx + 1:])
			parse_inline_code(rem)
		}
	}
}

parse_inline_footnotes :: proc(tok: ^token.Token) {
	if tok.type == .CodeBlock || tok.type == .InlineCode {
		return
	}
	if tok.children != nil {
		for i := 0; i < len(tok.children); i += 1 {
			parse_inline_footnotes(tok.children[i])
		}
	}

	if tok.type == .Text {
		str, ok := tok.value.(string)
		if !ok {
			return
		}

		start_idx := strings.index(str, "[&")
		if start_idx == -1 {
			return
		}

		end_idx := strings.index_byte(str[start_idx:], ']')
		if end_idx == -1 {
			return
		}
		end_idx += start_idx

		id := str[start_idx + 2:end_idx]

		tok.value = nil

		if start_idx > 0 {
			push_token(tok, .Text, str[:start_idx])
		}
		push_token(tok, .FootnoteRef, id)

		if end_idx + 1 < len(str) {
			rem := push_token(tok, .Text, str[end_idx + 1:])
			parse_inline_footnotes(rem)
		}
	}
}

match_pair :: proc(s: string, open1, close1, open2, close2: byte) -> (int, string, string, bool) {
	if len(s) == 0 || s[0] != open1 {
		return 0, "", "", false
	}

	depth := 1
	close1_idx := -1
	for i := 1; i < len(s); i += 1 {
		if s[i] == open1 {
			depth += 1
		} else if s[i] == close1 {
			depth -= 1
			if depth == 0 {
				close1_idx = i
				break
			}
		}
	}
	if close1_idx == -1 {
		return 0, "", "", false
	}

	if close1_idx + 1 < len(s) && s[close1_idx + 1] == open2 {
		depth = 1
		close2_idx := -1
		for i := close1_idx + 2; i < len(s); i += 1 {
			if s[i] == open2 {
				depth += 1
			} else if s[i] == close2 {
				depth -= 1
				if depth == 0 {
					close2_idx = i
					break
				}
			}
		}
		if close2_idx != -1 {
			return close2_idx + 1, s[1:close1_idx], s[close1_idx + 2:close2_idx], true
		}
	}
	return 0, "", "", false
}

match_single :: proc(s: string, open, close: byte) -> (int, string, bool) {
	if len(s) == 0 || s[0] != open {
		return 0, "", false
	}
	close_idx := strings.index_byte(s, close)
	if close_idx == -1 {
		return 0, "", false
	}
	return close_idx + 1, s[1:close_idx], true
}

find_link :: proc(
	str: string,
) -> (
	start_idx, end_idx: int,
	is_link: bool,
	url: string,
	alt: string,
	is_header: bool,
) {
	for i := 0; i < len(str); i += 1 {
		if str[i] == '#' && i + 1 < len(str) {
			if str[i + 1] == '[' {
				if end, alt_text, url_text, ok := match_pair(str[i + 1:], '[', ']', '(', ')'); ok {
					return i, i + 1 + end, true, url_text, alt_text, false
				}
				if end, inner, ok := match_single(str[i + 1:], '[', ']'); ok {
					return i, i + 1 + end, true, inner, inner, false
				}
			} else if str[i + 1] == '(' {
				if end, url_text, alt_text, ok := match_pair(str[i + 1:], '(', ')', '[', ']'); ok {
					return i, i + 1 + end, true, url_text, alt_text, false
				}
				if end, inner, ok := match_single(str[i + 1:], '(', ')'); ok {
					return i, i + 1 + end, true, inner, inner, false
				}
			}
		}

		if str[i] == '[' {
			if end, alt_text, url_text, ok := match_pair(str[i:], '[', ']', '(', ')'); ok {
				if strings.has_prefix(url_text, "#") {
					return i, i + end, true, url_text[1:], alt_text, true
				}
			}
			if end, inner, ok := match_single(str[i:], '[', ']'); ok {
				if strings.has_prefix(inner, "#") {
					return i, i + end, true, inner[1:], inner[1:], true
				}
			}
		} else if str[i] == '(' {
			if end, url_text, alt_text, ok := match_pair(str[i:], '(', ')', '[', ']'); ok {
				if strings.has_prefix(url_text, "#") {
					return i, i + end, true, url_text[1:], alt_text, true
				}
			}
			if end, inner, ok := match_single(str[i:], '(', ')'); ok {
				if strings.has_prefix(inner, "#") {
					return i, i + end, true, inner[1:], inner[1:], true
				}
			}
		}
	}

	return -1, -1, false, "", "", false
}

parse_links :: proc(tok: ^token.Token) {
	if tok.type == .CodeBlock || tok.type == .InlineCode {
		return
	}
	if tok.children != nil {
		for i := 0; i < len(tok.children); i += 1 {
			parse_links(tok.children[i])
		}
	}

	if tok.type == .Text {
		str, ok := tok.value.(string)
		if !ok {
			return
		}

		start_idx, end_idx, is_link, url, alt, is_header := find_link(str)
		if !is_link {
			return
		}

		tok.value = nil

		if start_idx > 0 {
			push_token(tok, .Text, str[:start_idx])
		}

		link_token := push_token(tok, .HeaderLink if is_header else .Link, url)
		push_token(link_token, .Text, alt)

		if end_idx < len(str) {
			rem := push_token(tok, .Text, str[end_idx:])
			parse_links(rem)
		}
	}
}

find_variable :: proc(
	str: string,
) -> (
	start_idx, end_idx: int,
	is_var: bool,
	name: string,
	val: string,
	is_def: bool,
) {
	for i := 0; i < len(str); i += 1 {
		if str[i] == '$' && i + 1 < len(str) && str[i + 1] == '(' {
			if end, inner, ok := match_single(str[i + 1:], '(', ')'); ok {
				eq := strings.index_byte(inner, '=')
				if eq != -1 {
					return i,
						i + 1 + end,
						true,
						strings.trim_space(inner[:eq]),
						strings.trim_space(inner[eq + 1:]),
						true
				} else if mime.is_url(inner) {
					continue
				} else {
					return i, i + 1 + end, true, strings.trim_space(inner), "", false
				}
			}
		}
	}

	return -1, -1, false, "", "", false
}

parse_variables :: proc(tok: ^token.Token) {
	if tok.type == .CodeBlock || tok.type == .InlineCode {
		return
	}
	if tok.children != nil {
		for i := 0; i < len(tok.children); i += 1 {
			parse_variables(tok.children[i])
		}
	}

	if tok.type == .Text {
		str, ok := tok.value.(string)
		if !ok {
			return
		}

		start_idx, end_idx, is_var, name, val, is_def := find_variable(str)
		if !is_var {
			return
		}

		tok.value = nil

		if start_idx > 0 {
			push_token(tok, .Text, str[:start_idx])
		}

		if is_def {
			def_token := push_token(tok, .VariableDef, name)
			push_token(def_token, .Text, val)
		} else {
			push_token(tok, .Variable, name)
		}

		if end_idx < len(str) {
			rem := push_token(tok, .Text, str[end_idx:])
			parse_variables(rem)
		}
	}
}

resolve_variables :: proc(tok: ^token.Token, vars: ^map[string]string) {
	if tok.type == .VariableDef {
		if name, ok := tok.value.(string); ok {
			if len(tok.children) > 0 {
				if val, vok := tok.children[0].value.(string); vok {
					vars[name] = val
				}
			}
		}
		return
	}
	if tok.type == .Variable {
		if name, ok := tok.value.(string); ok {
			if val, found := vars[name]; found {
				if tok.children == nil {
					push_token(tok, .Text, val)
				}
			}
		}
		return
	}
	if tok.type == .Link || tok.type == .HeaderLink {
		if url, ok := tok.value.(string); ok {
			for i := 0; i < len(url); i += 1 {
				if url[i] == '$' && i + 1 < len(url) && url[i + 1] == '(' {
					if end, inner, ok := match_single(url[i + 1:], '(', ')'); ok {
						var_name := strings.trim_space(inner)
						if val, found := vars[var_name]; found {
							b := strings.builder_make(context.temp_allocator)
							strings.write_string(&b, url[:i])
							strings.write_string(&b, val)
							strings.write_string(&b, url[i + 1 + end:])
							tok.value = strings.to_string(b)
							break
						}
					}
				}
			}
		}
	}
	if tok.children != nil {
		for child in tok.children {
			resolve_variables(child, vars)
		}
	}
}

pass_through_children :: proc(tok: ^token.Token) {
	if tok.type == .CodeBlock || tok.type == .InlineCode {
		return
	}
	parse_inline_style(tok, '*', []StyleToggle{{2, .Bold}, {1, .Italics}})
	parse_inline_style(tok, '~', []StyleToggle{{2, .Strikethrough}})
	parse_inline_style(tok, '-', []StyleToggle{{2, .Strikethrough}})
	parse_inline_style(tok, '^', []StyleToggle{{1, .Superscript}})
	parse_inline_style(tok, '_', []StyleToggle{{2, .Underline}, {1, .Subscript}})
}
