@tool
extends AcceptDialog

const HT_Util = preload("../../util/util.gd")
const HTerrain = preload("../../hterrain.gd")
const HTerrainData = preload("../../hterrain_data.gd")
const HT_Errors = preload("../../util/errors.gd")
const HT_Logger = preload("../../util/logger.gd")
const HT_XYZFormat = preload("../../util/xyz_format.gd")
const HT_Inspector = preload("../inspector/inspector.gd")

signal permanent_change_performed(message)

@onready var _inspector : HT_Inspector = $VBoxContainer/Inspector
@onready var _errors_label : Label = $VBoxContainer/ColorRect/ScrollContainer/VBoxContainer/Errors
@onready var _warnings_label : Label = \
	$VBoxContainer/ColorRect/ScrollContainer/VBoxContainer/Warnings


enum {
	RAW_LITTLE_ENDIAN,
	RAW_BIG_ENDIAN
}

var _terrain : HTerrain = null
var _logger = HT_Logger.get_for(self)


func _init():
	get_ok_button().hide()


func _ready():
	_inspector.set_prototype({
		"heightmap": {
			"type": TYPE_STRING,
			"usage": "file",
			"exts": ["raw", "png", "exr", "xyz"]
		},
		"raw_endianess": {
			"type": TYPE_INT,
			"usage": "enum",
			"enum_items": [[RAW_LITTLE_ENDIAN, "Little Endian"], [RAW_BIG_ENDIAN, "Big Endian"]],
			"enabled": false
		},
		"bit_depth": {
			"type": TYPE_INT,
			"usage": "enum",
			"enum_items": [[HTerrainData.BIT_DEPTH_16, "16-bit"], [HTerrainData.BIT_DEPTH_32, "32-bit"]],
			"enabled": false
		},
		"min_height": {
			"type": TYPE_FLOAT,
			"range": {"min": -2000.0, "max": 2000.0, "step": 0.01},
			"default_value": 0.0
		},
		"max_height": {
			"type": TYPE_FLOAT,
			"range": {"min": -2000.0, "max": 2000.0, "step": 0.01},
			"default_value": 400.0
		},
		"splatmap": {
			"type": TYPE_STRING,
			"usage": "file",
			"exts": ["png"]
		},
		"colormap": {
			"type": TYPE_STRING,
			"usage": "file",
			"exts": ["png"]
		}
	})

	# Testing
#	_errors_label.text = "- Hello World!"
#	_warnings_label.text = "- Yolo Jesus!"


func set_terrain(terrain: HTerrain):
	_terrain = terrain


func _notification(what: int):
	if what == NOTIFICATION_VISIBILITY_CHANGED:
		# Checking a node set in _ready,
		# because visibility can also change between _enter_tree and _ready...
		if visible and _inspector != null:
			_clear_feedback()


static func _format_feedbacks(feed):
	var a := []
	for s in feed:
		a.append("- " + s)
	return "\n".join(PackedStringArray(a))


func _clear_feedback():
	_errors_label.text = ""
	_warnings_label.text = ""


class HT_ErrorCheckReport:
	var errors := []
	var warnings := []


func _show_feedback(res: HT_ErrorCheckReport):
	for e in res.errors:
		_logger.error(e)

	for w in res.warnings:
		_logger.warn(w)
	
	_clear_feedback()
	
	if len(res.errors) > 0:
		_errors_label.text = _format_feedbacks(res.errors)

	if len(res.warnings) > 0:
		_warnings_label.text = _format_feedbacks(res.warnings)


func _on_CheckButton_pressed():
	var res := _validate_form()
	_show_feedback(res)


func _on_ImportButton_pressed():
	assert(_terrain != null and _terrain.get_data() != null)

	# Verify input to inform the user of potential issues
	var res := _validate_form()
	_show_feedback(res)

	if len(res.errors) != 0:
		_logger.debug("Cannot import due to errors, aborting")
		return

	var params := {}

	var heightmap_path = _inspector.get_value("heightmap")
	if heightmap_path != "":
		var endianess = _inspector.get_value("raw_endianess")
		params[HTerrainData.CHANNEL_HEIGHT] = {
			"path": heightmap_path,
			"min_height": _inspector.get_value("min_height"),
			"max_height": _inspector.get_value("max_height"),
			"big_endian": endianess == RAW_BIG_ENDIAN,
			"bit_depth": _inspector.get_value("bit_depth"),
		}

	var colormap_path = _inspector.get_value("colormap")
	if colormap_path != "":
		params[HTerrainData.CHANNEL_COLOR] = {
			"path": colormap_path
		}

	var splatmap_path = _inspector.get_value("splatmap")
	if splatmap_path != "":
		params[HTerrainData.CHANNEL_SPLAT] = {
			"path": splatmap_path
		}

	var data = _terrain.get_data()
	data._edit_import_maps(params)
	emit_signal("permanent_change_performed", "Import maps")
	
	_logger.debug("Terrain import finished")
	hide()


func _on_CancelButton_pressed():
	hide()


func _on_Inspector_property_changed(key: String, value):
	if key == "heightmap":
		var is_raw = value.get_extension().to_lower() == "raw"
		_inspector.set_property_enabled("raw_endianess", is_raw)
		_inspector.set_property_enabled("bit_depth", is_raw)
		if is_raw:
			var bit_depth:int = _estimate_bit_depth_for_raw_file(value)
			if bit_depth == HTerrainData.BIT_DEPTH_UNDEFINED:
				bit_depth = HTerrainData.BIT_DEPTH_16 # fallback depth value
			_inspector.set_value("bit_depth", bit_depth)


# _estimate_bit_depth_for_raw_file returns the file's identified bit depth, or 0.
static func _estimate_bit_depth_for_raw_file(path: String) -> int:
	var ext := path.get_extension().to_lower()
	if ext == "raw":
		var f := FileAccess.open(path, FileAccess.READ)
		if f == null:
			return HTerrainData.BIT_DEPTH_UNDEFINED

		var file_len := f.get_length()
		f = null # close file

		for bit_depth in [HTerrainData.BIT_DEPTH_16, HTerrainData.BIT_DEPTH_32]:
			var file_res := HT_Util.integer_square_root(file_len / (bit_depth/8))
			if file_res > 0:
				return bit_depth

	return HTerrainData.BIT_DEPTH_UNDEFINED


func _validate_form() -> HT_ErrorCheckReport:
	var res := HT_ErrorCheckReport.new()

	var heightmap_path : String = _inspector.get_value("heightmap")
	var splatmap_path : String = _inspector.get_value("splatmap")
	var colormap_path : String = _inspector.get_value("colormap")
	var bit_depth = _inspector.get_value("bit_depth")

	if colormap_path == "" and heightmap_path == "" and splatmap_path == "":
		res.errors.append("No maps specified.")
		return res

	# If a heightmap is specified, it will override the size of the existing terrain.
	# If not specified, maps will have to match the resolution of the existing terrain.
	var heightmap_size := _terrain.get_data().get_resolution()

	if heightmap_path != "":
		var min_height = _inspector.get_value("min_height")
		var max_height = _inspector.get_value("max_height")

		if min_height >= max_height:
			res.errors.append("Minimum height must be lower than maximum height")
			# Returning early because min and max can be slided,
			# so we avoid loading other maps every time to do further checks.
			return res

		var image_size_result = _load_image_size(heightmap_path, _logger, bit_depth)
		if image_size_result.error_code != OK:
			res.errors.append(str("Cannot open heightmap file: ", image_size_result.to_string()))
			return res

		var adjusted_size = HTerrainData.get_adjusted_map_size(
			image_size_result.width, image_size_result.height)

		if adjusted_size != image_size_result.width:
			res.warnings.append(
				"The square resolution deduced from heightmap file size is not power of two + 1.\n" + \
				"The heightmap will be cropped.")

		heightmap_size = adjusted_size

	if splatmap_path != "":
		_check_map_size(splatmap_path, "splatmap", heightmap_size, bit_depth, res, _logger)

	if colormap_path != "":
		_check_map_size(colormap_path, "colormap", heightmap_size, bit_depth, res, _logger)

	return res


static func _check_map_size(path: String, map_name: String, heightmap_size: int, bit_depth: int, 
	res: HT_ErrorCheckReport, logger):
	
	var size_result := _load_image_size(path, logger, bit_depth)
	if size_result.error_code != OK:
		res.errors.append(str("Cannot open splatmap file: ", size_result.to_string()))
		return
	var adjusted_size := HTerrainData.get_adjusted_map_size(size_result.width, size_result.height)
	if adjusted_size != heightmap_size:
		res.errors.append(str(
			"The ", map_name, 
			" must have the same resolution as the heightmap (", heightmap_size, ")"))
	else:
		if adjusted_size != size_result.width:
			res.warnings.append(str(
				"The square resolution deduced from ", map_name, 
				" file size is not power of two + 1.\nThe ", 
				map_name, " will be cropped."))


class HT_ImageSizeResult:
	var width := 0
	var height := 0
	var error_code := OK
	var error_message := ""
	
	func to_string() -> String:
		if error_message != "":
			return error_message
		return HT_Errors.get_message(error_code)


static func _load_image_size(path: String, logger, bit_depth: int) -> HT_ImageSizeResult:
	var ext := path.get_extension().to_lower()
	var result := HT_ImageSizeResult.new()

	if ext == "png" or ext == "exr":
		# Godot can load these formats natively
		var im := Image.new()
		var err := im.load(ProjectSettings.globalize_path(path))
		if err != OK:
			logger.error("An error occurred loading image '{0}', code {1}".format([path, err]))
			result.error_code = err
			return result
		
		result.width = im.get_width()
		result.height = im.get_height()
		return result

	elif ext == "raw":
		var f := FileAccess.open(path, FileAccess.READ)
		if f == null:
			var err := FileAccess.get_open_error()
			logger.error("Error opening file {0}".format([path]))
			result.error_code = err
			return result

		# Assume the raw data is square, so its size is function of file length
		var flen := f.get_length()
		f = null
		var size_px = HT_Util.integer_square_root(flen / (bit_depth/8))
		if size_px == -1:
			result.error_code = ERR_INVALID_DATA
			result.error_message = "RAW image is not square or your bit depth choice is incorrect…"
			return result
		
		logger.debug("Deduced RAW heightmap resolution: {0}*{1}, for a length of {2}" \
			.format([size_px, size_px, flen]))

		result.width = size_px
		result.height = size_px
		return result

	elif ext == "xyz":
		var f := FileAccess.open(path, FileAccess.READ)
		if f == null:
			var err := FileAccess.get_open_error()
			logger.error("Error opening file {0}".format([path]))
			result.error_code = err
			return result

		var bounds := HT_XYZFormat.load_bounds(f)

		result.width = bounds.image_width
		result.height = bounds.image_height
		return result

	else:
		result.error_code = ERR_FILE_UNRECOGNIZED
		return result
