extends RefCounted

const FONT_H1 := 32
const FONT_H2 := 28
const FONT_H3 := 24
const FONT_H4 := 20
const FONT_H5 := 16
const FONT_H6 := 12

const HORIZONTAL_RULE := "[img=4000x2]res://addons/gdUnit4/src/update/assets/horizontal-line2.png[/img]\n"
const HEADER_RULE := "[font_size=%d]$1[/font_size]\n"
const HEADER_CENTERED_RULE := "[font_size=%d][center]$1[/center][/font_size]\n"

const image_download_folder := "res://addons/gdUnit4/tmp-update/"

const exclude_font_size := "\b(?!(?:(font_size))\b)"

var md_replace_patterns := [
	# horizontal rules
	[regex("(?m)^[ ]{0,3}---$"), HORIZONTAL_RULE],
	[regex("(?m)^[ ]{0,3}___$"), HORIZONTAL_RULE],
	[regex("(?m)^[ ]{0,3}\\*\\*\\*$"), HORIZONTAL_RULE],

	# headers
	[regex("(?m)^###### (.*)"), HEADER_RULE % FONT_H6],
	[regex("(?m)^##### (.*)"), HEADER_RULE % FONT_H5],
	[regex("(?m)^#### (.*)"), HEADER_RULE % FONT_H4],
	[regex("(?m)^### (.*)"), HEADER_RULE % FONT_H3],
	[regex("(?m)^## (.*)"), (HEADER_RULE + HORIZONTAL_RULE) % FONT_H2],
	[regex("(?m)^# (.*)"), (HEADER_RULE + HORIZONTAL_RULE) % FONT_H1],
	[regex("(?m)^(.+)=={2,}$"), HEADER_RULE % FONT_H1],
	[regex("(?m)^(.+)--{2,}$"), HEADER_RULE % FONT_H2],
	# html headers
	[regex("<h1>((.*?\\R?)+)<\\/h1>"), (HEADER_RULE + HORIZONTAL_RULE) % FONT_H1],
	[regex("<h1[ ]*align[ ]*=[ ]*\"center\">((.*?\\R?)+)<\\/h1>"), (HEADER_CENTERED_RULE + HORIZONTAL_RULE) % FONT_H1],
	[regex("<h2>((.*?\\R?)+)<\\/h2>"), (HEADER_RULE + HORIZONTAL_RULE) % FONT_H2],
	[regex("<h2[ ]*align[ ]*=[ ]*\"center\">((.*?\\R?)+)<\\/h2>"), (HEADER_CENTERED_RULE + HORIZONTAL_RULE) % FONT_H1],
	[regex("<h3>((.*?\\R?)+)<\\/h3>"), HEADER_RULE % FONT_H3],
	[regex("<h3[ ]*align[ ]*=[ ]*\"center\">((.*?\\R?)+)<\\/h3>"), HEADER_CENTERED_RULE % FONT_H3],
	[regex("<h4>((.*?\\R?)+)<\\/h4>"), HEADER_RULE % FONT_H4],
	[regex("<h4[ ]*align[ ]*=[ ]*\"center\">((.*?\\R?)+)<\\/h4>"), HEADER_CENTERED_RULE % FONT_H4],
	[regex("<h5>((.*?\\R?)+)<\\/h5>"), HEADER_RULE % FONT_H5],
	[regex("<h5[ ]*align[ ]*=[ ]*\"center\">((.*?\\R?)+)<\\/h5>"), HEADER_CENTERED_RULE % FONT_H5],
	[regex("<h6>((.*?\\R?)+)<\\/h6>"), HEADER_RULE % FONT_H6],
	[regex("<h6[ ]*align[ ]*=[ ]*\"center\">((.*?\\R?)+)<\\/h6>"), HEADER_CENTERED_RULE % FONT_H6],

	# asterics
	#[regex("(\\*)"), "xxx$1xxx"],

	# extract/compile image references
	[regex("!\\[(.*?)\\]\\[(.*?)\\]"), Callable(self, "process_image_references")],
	# extract images with path and optional tool tip
	[regex("!\\[(.*?)\\]\\((.*?)(( )+(.*?))?\\)"), Callable(self, "process_image")],

	# links
	[regex("([!]|)\\[(.+)\\]\\(([^ ]+?)\\)"),  "[url={\"url\":\"$3\"}]$2[/url]"],
	# links with tool tip
	[regex("([!]|)\\[(.+)\\]\\(([^ ]+?)( \"(.+)\")?\\)"),  "[url={\"url\":\"$3\", \"tool_tip\":\"$5\"}]$2[/url]"],

	# embeded text
	[regex("(?m)^[ ]{0,3}>(.*?)$"), "[img=50x14]res://addons/gdUnit4/src/update/assets/embedded.png[/img][i]$1[/i]"],

	# italic + bold font
	[regex("[_]{3}(.*?)[_]{3}"), "[i][b]$1[/b][/i]"],
	[regex("[\\*]{3}(.*?)[\\*]{3}"), "[i][b]$1[/b][/i]"],
	# bold font
	[regex("<b>(.*?)<\\/b>"), "[b]$1[/b]"],
	[regex("[_]{2}(.*?)[_]{2}"), "[b]$1[/b]"],
	[regex("[\\*]{2}(.*?)[\\*]{2}"), "[b]$1[/b]"],
	# italic font
	[regex("<i>(.*?)<\\/i>"), "[i]$1[/i]"],
	[regex(exclude_font_size+"_(.*?)_"), "[i]$1[/i]"],
	[regex("\\*(.*?)\\*"), "[i]$1[/i]"],

	# strikethrough font
	[regex("<s>(.*?)</s>"), "[s]$1[/s]"],
	[regex("~~(.*?)~~"), "[s]$1[/s]"],
	[regex("~(.*?)~"), "[s]$1[/s]"],

	# handling lists
	# using an image for dots as workaroud because list is not supported checked Godot 3.x
	[regex("(?m)^[ ]{0,1}[*\\-+] (.*)$"), list_replace(0)],
	[regex("(?m)^[ ]{2,3}[*\\-+] (.*)$"), list_replace(1)],
	[regex("(?m)^[ ]{4,5}[*\\-+] (.*)$"), list_replace(2)],
	[regex("(?m)^[ ]{6,7}[*\\-+] (.*)$"), list_replace(3)],
	[regex("(?m)^[ ]{8,9}[*\\-+] (.*)$"), list_replace(4)],

	# code blocks, code blocks looks not like code blocks in richtext
	[regex("```(javascript|python|shell|gdscript)([\\s\\S]*?\n)```"), code_block("$2", true)],
	[regex("``([\\s\\S]*?)``"), code_block("$1")],
	[regex("`([\\s\\S]*?)`{1,2}"), code_block("$1")],
]

var _img_replace_regex := RegEx.new()
var _image_urls := PackedStringArray()
var _on_table_tag := false
var _client


func regex(pattern :String) -> RegEx:
	var regex_ := RegEx.new()
	var err = regex_.compile(pattern)
	if err != OK:
		push_error("error '%s' checked pattern '%s'" % [err, pattern])
		return null
	return regex_


func _init():
	_img_replace_regex.compile("\\[img\\]((.*?))\\[/img\\]")


func set_http_client(client) -> void:
	_client = client


func _notification(what):
	if what == NOTIFICATION_PREDELETE:
		# finally remove_at the downloaded images
		for image in _image_urls:
			DirAccess.remove_absolute(image)
			DirAccess.remove_absolute(image + ".import")


func list_replace(indent :int) -> String:
	var replace_pattern := "[img=12x12]res://addons/gdUnit4/src/update/assets/dot2.png[/img]" if indent %2 else "[img=12x12]res://addons/gdUnit4/src/update/assets/dot1.png[/img]"
	replace_pattern += " $1"

	for index in indent:
		replace_pattern = replace_pattern.insert(0, "   ")
	return replace_pattern


func code_block(replace :String, border :bool = false) -> String:
	var cb := "[code][color=aqua][font_size=16]%s[/font_size][/color][/code]" % replace
	if border:
		return "[img=1400x14]res://addons/gdUnit4/src/update/assets/border_top.png[/img]"\
			+ "[indent]" + cb + "[/indent]"\
			+ "[img=1400x14]res://addons/gdUnit4/src/update/assets/border_bottom.png[/img]\n"
	return cb


func to_bbcode(input :String) -> String:
	input = process_tables(input)

	for pattern in md_replace_patterns:
		var regex_ :RegEx = pattern[0]
		var bb_replace = pattern[1]
		if bb_replace is Callable:
			input = await bb_replace.call(regex_, input)
		else:
			input = regex_.sub(input, bb_replace, true)
	return input + "\n"


func process_tables(input :String) -> String:
	var bbcode := Array()
	var lines := Array(input.split("\n"))
	while not lines.is_empty():
		if is_table(lines[0]):
			bbcode.append_array(parse_table(lines))
			continue
		bbcode.append(lines.pop_front())
	return "\n".join(PackedStringArray(bbcode))


class Table:
	var _columns :int
	var _rows := Array()

	class Row:
		var _cells := PackedStringArray()

		func _init(cells :PackedStringArray,columns :int):
			_cells = cells
			for i in range(_cells.size(), columns):
				_cells.append("")

		func to_bbcode(cell_sizes :PackedInt32Array, bold :bool) -> String:
			var cells := PackedStringArray()
			for cell_index in _cells.size():
				var cell :String = _cells[cell_index]
				if cell.strip_edges() == "--":
					cell = create_line(cell_sizes[cell_index])
				if bold:
					cell = "[b]%s[/b]" % cell
				cells.append("[cell]%s[/cell]" % cell)
			return "|".join(cells)

		func create_line(length :int) -> String:
			var line := ""
			for i in length:
				line += "-"
			return line

	func _init(columns :int):
		_columns = columns

	func parse_row(line :String) -> bool:
		# is line containing cells?
		if line.find("|") == -1:
			return false
		_rows.append(Row.new(line.split("|"), _columns))
		return true

	func calculate_max_cell_sizes() -> PackedInt32Array:
		var cells_size := PackedInt32Array()
		for column in _columns:
			cells_size.append(0)

		for row_index in _rows.size():
			var row :Row = _rows[row_index]
			for cell_index in row._cells.size():
				var cell_size :int = cells_size[cell_index]
				var size := row._cells[cell_index].length()
				if size > cell_size:
					cells_size[cell_index] = size
		return cells_size

	func to_bbcode() -> PackedStringArray:
		var cell_sizes := calculate_max_cell_sizes()
		var bb_code := PackedStringArray()

		bb_code.append("[table=%d]" % _columns)
		for row_index in _rows.size():
			bb_code.append(_rows[row_index].to_bbcode(cell_sizes, row_index==0))
		bb_code.append("[/table]\n")
		return bb_code


func parse_table(lines :Array) -> PackedStringArray:
	var line :String = lines[0]
	var table := Table.new(line.count("|") + 1)
	while not lines.is_empty():
		line = lines.pop_front()
		if not table.parse_row(line):
			break
	return table.to_bbcode()


func is_table(line :String) -> bool:
	return line.find("|") != -1


func open_table(line :String) -> String:
	_on_table_tag = true
	return "[table=%d]" % (line.count("|") + 1)


func close_table() -> String:
	_on_table_tag = false
	return "[/table]"


func extract_cells(line :String, bold := false) -> String:
	var cells := ""
	for cell in line.split("|"):
		if bold:
			cell = "[b]%s[/b]" % cell
		cells += "[cell]%s[/cell]" % cell
	return cells


func process_image_references(p_regex :RegEx, p_input :String) -> String:
	# exists references?
	var matches := p_regex.search_all(p_input)
	if matches.is_empty():
		return p_input
	# collect image references and remove_at it
	var references := Dictionary()
	var link_regex := regex("\\[(\\S+)\\]:(\\S+)([ ]\"(.*)\")?")
	# create copy of original source to replace checked it
	var input := p_input.replace("\r", "")
	var extracted_references :=  p_input.replace("\r", "")
	for reg_match in link_regex.search_all(input):
		var line = reg_match.get_string(0) + "\n"
		var ref = reg_match.get_string(1)
		#var topl_tip = reg_match.get_string(4)
		# collect reference and url
		references[ref] = reg_match.get_string(2)
		extracted_references = extracted_references.replace(line, "")

	# replace image references by collected url's
	for reference_key in references.keys():
		var regex_key := regex("\\](\\[%s\\])" % reference_key)
		for reg_match in regex_key.search_all(extracted_references):
			var ref :String = reg_match.get_string(0)
			var image_url :String = "](%s)" % references.get(reference_key)
			extracted_references = extracted_references.replace(ref, image_url)
	return extracted_references


func process_image(p_regex :RegEx, p_input :String) -> String:
	var to_replace := PackedStringArray()
	var tool_tips :=  PackedStringArray()
	# find all matches
	var matches := p_regex.search_all(p_input)
	if matches.is_empty():
		return p_input
	for reg_match in matches:
		# grap the parts to replace and store temporay because a direct replace will distort the offsets
		to_replace.append(p_input.substr(reg_match.get_start(0), reg_match.get_end(0)))
		# grap optional tool tips
		tool_tips.append(reg_match.get_string(5))
	# finally replace all findings
	for replace in to_replace:
		var re := p_regex.sub(replace, "[img]$2[/img]")
		p_input = p_input.replace(replace, re)
	return await _process_external_image_resources(p_input)


func _process_external_image_resources(input :String) -> String:
	DirAccess.make_dir_recursive_absolute(image_download_folder)
	# scan all img for external resources and download it
	for value in _img_replace_regex.search_all(input):
		if value.get_group_count() >= 1:
			var image_url :String = value.get_string(1)
			# if not a local resource we need to download it
			if image_url.begins_with("http"):
				if OS.is_stdout_verbose():
					prints("download image:", image_url)
				var response = await _client.request_image(image_url)
				if response.code() == 200:
					var image = Image.new()
					var error = image.load_png_from_buffer(response.body())
					if error != OK:
						prints("Error creating image from response", error)
					# replace characters where format characters
					var new_url := image_download_folder + image_url.get_file().replace("_", "-")
					if new_url.get_extension() != 'png':
						new_url = new_url + '.png'
					var err := image.save_png(new_url)
					if err:
						push_error("Can't save image to '%s'. Error: %s" % [new_url, error_string(err)])
					_image_urls.append(new_url)
					input = input.replace(image_url, new_url)
	return input
