@tool
extends EditorPlugin
class_name SS2D_Plugin


## Common Abbreviations
## et = editor transform (viewport's canvas transform)
##
## - Snapping using the build in functionality isn't going to happen
## 	- https://github.com/godotengine/godot/issues/11180
## 	- https://godotengine.org/qa/18051/tool-script-in-3-0

const PROJECT_SETTING_INTERNAL_VERSION := "addons/ss2d/version"

# Icons
# TODO: Change to const and preload when this is resolved:
# https://github.com/godotengine/godot/issues/17483
var ICON_HANDLE: Texture2D = load("res://addons/rmsmartshape/assets/icon_editor_handle.svg")
var ICON_HANDLE_SELECTED: Texture2D = load("res://addons/rmsmartshape/assets/icon_editor_handle_selected.svg")
var ICON_HANDLE_BEZIER: Texture2D = load("res://addons/rmsmartshape/assets/icon_editor_handle_bezier.svg")
var ICON_HANDLE_CONTROL: Texture2D = load("res://addons/rmsmartshape/assets/icon_editor_handle_control.svg")
var ICON_FREEHAND_MODE: Texture2D = load("res://addons/rmsmartshape/assets/icon_editor_freehand.svg")
var ICON_CIRCLE_ERASE: Texture2D = load("res://addons/rmsmartshape/assets/icon_editor_snap.svg")
var ICON_ADD_HANDLE: Texture2D = load("res://addons/rmsmartshape/assets/icon_editor_handle_add.svg")
var ICON_CURVE_EDIT: Texture2D = load("res://addons/rmsmartshape/assets/icon_curve_edit.svg")
var ICON_CURVE_CREATE: Texture2D = load("res://addons/rmsmartshape/assets/icon_curve_create.svg")
var ICON_CURVE_DELETE: Texture2D = load("res://addons/rmsmartshape/assets/icon_curve_delete.svg")
var ICON_PIVOT_POINT: Texture2D = load("res://addons/rmsmartshape/assets/icon_editor_position.svg")
var ICON_CENTER_PIVOT: Texture2D = load("res://addons/rmsmartshape/assets/CenterView.svg")
var ICON_INTERP_LINEAR: Texture2D = load("res://addons/rmsmartshape/assets/InterpLinear.svg")
var ICON_SNAP: Texture2D = load("res://addons/rmsmartshape/assets/icon_editor_snap.svg")
var ICON_IMPORT_CLOSED: Texture2D = load("res://addons/rmsmartshape/assets/closed_shape.png")
var ICON_IMPORT_OPEN: Texture2D = load("res://addons/rmsmartshape/assets/open_shape.png")

enum MODE { EDIT_VERT, EDIT_EDGE, CUT_EDGE, SET_PIVOT, CREATE_VERT, FREEHAND }

enum SNAP_MENU { ID_USE_GRID_SNAP, ID_SNAP_RELATIVE, ID_CONFIGURE_SNAP }
enum OPTIONS_MENU { ID_DEFER_MESH_UPDATES, ID_CHECK_VERSION }

enum ACTION_VERT {
	NONE = 0,
	MOVE_VERT = 1,
	MOVE_CONTROL = 2,
	MOVE_CONTROL_IN = 3,
	MOVE_CONTROL_OUT = 4,
	MOVE_WIDTH_HANDLE = 5
}


# Data related to an action being taken on points
class ActionDataVert:
	#Type of Action from the ACTION_VERT enum
	var type: ACTION_VERT = ACTION_VERT.NONE
	# The affected Verticies and their initial positions
	var keys: PackedInt32Array
	var starting_width: PackedFloat32Array
	var starting_positions: PackedVector2Array = []
	var starting_positions_control_in: PackedVector2Array = []
	var starting_positions_control_out: PackedVector2Array = []

	func _init(
		_keys: PackedInt32Array,
		positions: PackedVector2Array,
		positions_in: PackedVector2Array,
		positions_out: PackedVector2Array,
		width: PackedFloat32Array,
		t: ACTION_VERT
	) -> void:
		type = t
		keys = _keys
		starting_positions = positions
		starting_positions_control_in = positions_in
		starting_positions_control_out = positions_out
		starting_width = width

	func are_verts_selected() -> bool:
		return keys.size() > 0

	func _to_string() -> String:
		return "%s: %s = %s" % [type, keys, starting_positions]

	func is_single_vert_selected() -> bool:
		return keys.size() == 1

	func current_point_key() -> int:
		if not is_single_vert_selected():
			return -1
		return keys[0]

	func current_point_index(s: SS2D_Shape) -> int:
		if not is_single_vert_selected():
			return -1
		return s.get_point_array().get_point_index(keys[0])


# PRELOADS
var GUI_SNAP_POPUP := preload("scenes/SnapPopup.tscn")
var GUI_POINT_INFO_PANEL := preload("scenes/GUI_InfoPanel.tscn")
var GUI_EDGE_INFO_PANEL := preload("scenes/GUI_Edge_InfoPanel.tscn")
var gui_point_info_panel: SS2D_PointInfoPanel = GUI_POINT_INFO_PANEL.instantiate()
var gui_edge_info_panel: SS2D_EdgeInfoPanel = GUI_EDGE_INFO_PANEL.instantiate()
var gui_snap_settings: SS2D_SnapPopup = GUI_SNAP_POPUP.instantiate()

const GUI_POINT_INFO_PANEL_OFFSET := Vector2(256, 130)

# This is the shape node being edited
var shape: SS2D_Shape = null

# Tools
var _tools: Array[SS2D_EditorTool] = [
	SS2D_CollisionEditorTool.new(),
]

# Toolbar Stuff
var tb_hb: HBoxContainer = null
var tb_vert_create: Button = null
var tb_vert_edit: Button = null
var tb_edge_edit: Button = null
var tb_edge_cut: Button = null
var tb_pivot: Button = null
var tb_center_pivot: Button = null
var tb_freehand: Button = null
var tb_button_group: ButtonGroup = null

var tb_snap: MenuButton = null
# The PopupMenu that belongs to tb_snap
var tb_snap_popup: PopupMenu = null

var tb_options: MenuButton = null
var tb_options_popup: PopupMenu = null

var make_unique_dialog: AcceptDialog

# Edge Stuff
var on_edge: bool = false
var edge_point: Vector2
var edge_data: SS2D_Edge = null

# Width Handle Stuff
var on_width_handle: bool = false
const WIDTH_HANDLE_OFFSET: float = 60.0
var closest_key: int
var closest_edge_keys := Vector2i(-1, -1)
var width_scaling: float

# Vertex paint mode stuff
var last_point_position: Vector2
var _mouse_lmb_pressed := false
var _mouse_rmb_pressed := false
var freehand_paint_size := 20.0
var freehand_erase_size := 40.0

# Track our mode of operation
var current_mode: int = MODE.CREATE_VERT
var previous_mode: int = MODE.CREATE_VERT

var current_action := ActionDataVert.new([], [], [], [], [], ACTION_VERT.NONE)
var cached_shape_global_transform: Transform2D

# Action Move Variables
var _mouse_motion_delta_starting_pos := Vector2(0, 0)

# Defining the viewport to get the current zoom/scale
var target_viewport: Viewport
var current_zoom_level : float = 1.0

# Track the property plugin
var plugin: EditorInspectorPlugin

var is_2d_screen_active := false

var _defer_mesh_updates := false

#######
# GUI #
#######


func gui_display_snap_settings() -> void:
	var pos := tb_snap.get_screen_position() + tb_snap.size
	pos.x -= (gui_snap_settings.size.x + tb_snap.size.x) / 2.0
	gui_snap_settings.position = pos
	gui_snap_settings.popup()


func _snapping_item_selected(id: int) -> void:
	if id == SNAP_MENU.ID_USE_GRID_SNAP:
		tb_snap_popup.set_item_checked(id, not tb_snap_popup.is_item_checked(id))
	if id == SNAP_MENU.ID_SNAP_RELATIVE:
		tb_snap_popup.set_item_checked(id, not tb_snap_popup.is_item_checked(id))
	elif id == SNAP_MENU.ID_CONFIGURE_SNAP:
		gui_display_snap_settings()


func _options_item_selected(id: int) -> void:
	if id == OPTIONS_MENU.ID_DEFER_MESH_UPDATES:
		tb_options_popup.set_item_checked(id, not tb_options_popup.is_item_checked(id))
		_defer_mesh_updates = tb_options_popup.is_item_checked(id)
	elif id == OPTIONS_MENU.ID_CHECK_VERSION:
		perform_version_check_and_conversion(true, true)


func _gui_build_toolbar() -> void:
	tb_hb = HBoxContainer.new()
	add_control_to_container(EditorPlugin.CONTAINER_CANVAS_EDITOR_MENU, tb_hb)

	var sep := VSeparator.new()
	tb_hb.add_child(sep)

	tb_button_group = ButtonGroup.new()

	tb_options = MenuButton.new()
	tb_options.tooltip_text = SS2D_Strings.EN_TOOLTIP_MORE_OPTIONS
	tb_options.icon = EditorInterface.get_base_control().get_theme_icon("GuiTabMenuHl", "EditorIcons")
	tb_options_popup = tb_options.get_popup()
	tb_options_popup.add_check_item(SS2D_Strings.EN_OPTIONS_DEFER_MESH_UPDATES, OPTIONS_MENU.ID_DEFER_MESH_UPDATES)
	tb_options_popup.add_item(SS2D_Strings.EN_OPTIONS_CHECK_VERSION, OPTIONS_MENU.ID_CHECK_VERSION)
	tb_options_popup.hide_on_checkable_item_selection = false
	tb_options_popup.connect("id_pressed", self._options_item_selected)

	tb_vert_create = create_tool_button(ICON_CURVE_CREATE, SS2D_Strings.EN_TOOLTIP_CREATE_VERT)
	tb_vert_create.connect(&"pressed", self._enter_mode.bind(MODE.CREATE_VERT))
	tb_vert_create.button_pressed = true

	tb_vert_edit = create_tool_button(ICON_CURVE_EDIT, SS2D_Strings.EN_TOOLTIP_EDIT_VERT)
	tb_vert_edit.connect(&"pressed", self._enter_mode.bind(MODE.EDIT_VERT))

	tb_edge_edit = create_tool_button(ICON_INTERP_LINEAR, SS2D_Strings.EN_TOOLTIP_EDIT_EDGE)
	tb_edge_edit.connect(&"pressed", self._enter_mode.bind(MODE.EDIT_EDGE))

	var edge_cut_icon: Texture2D = EditorInterface.get_base_control().get_theme_icon(&"ActionCut", &"EditorIcons")
	tb_edge_cut = create_tool_button(edge_cut_icon, SS2D_Strings.EN_TOOLTIP_CUT_EDGE)
	tb_edge_cut.connect(&"pressed", _enter_mode.bind(MODE.CUT_EDGE))

	tb_pivot = create_tool_button(ICON_PIVOT_POINT, SS2D_Strings.EN_TOOLTIP_PIVOT)
	tb_pivot.connect(&"pressed", self._enter_mode.bind(MODE.SET_PIVOT))

	tb_center_pivot = create_tool_button(ICON_CENTER_PIVOT, SS2D_Strings.EN_TOOLTIP_CENTER_PIVOT, false)
	tb_center_pivot.connect(&"pressed", self._center_pivot)

	tb_freehand = create_tool_button(ICON_FREEHAND_MODE, SS2D_Strings.EN_TOOLTIP_FREEHAND)
	tb_freehand.connect(&"pressed", self._enter_mode.bind(MODE.FREEHAND))

	for i in _tools:
		i.register(self)

	tb_snap = MenuButton.new()
	tb_snap.tooltip_text = SS2D_Strings.EN_TOOLTIP_SNAP
	tb_snap_popup = tb_snap.get_popup()
	tb_snap.icon = ICON_SNAP
	tb_snap_popup.add_check_item("Use Grid Snap", SNAP_MENU.ID_USE_GRID_SNAP)
	tb_snap_popup.add_check_item("Snap Relative", SNAP_MENU.ID_SNAP_RELATIVE)
	tb_snap_popup.add_separator()
	tb_snap_popup.add_item("Configure Snap...", SNAP_MENU.ID_CONFIGURE_SNAP)
	tb_snap_popup.hide_on_checkable_item_selection = false
	tb_hb.add_child(tb_snap)
	tb_snap_popup.connect("id_pressed", self._snapping_item_selected)

	tb_hb.add_child(tb_options)

# TODO: Move into SS2D_EditorTool eventually
func create_tool_button(icon: Texture2D, tooltip: String, toggle: bool = true) -> Button:
	var tb := Button.new()
	tb.toggle_mode = toggle
	tb.button_group = tb_button_group
	tb.theme_type_variation = "FlatButton"
	tb.focus_mode = Control.FocusMode.FOCUS_NONE
	tb.icon = icon
	tb.tooltip_text = tooltip
	tb_hb.add_child(tb)
	return tb


func _gui_update_vert_info_panel() -> void:
	var idx: int = current_action.current_point_index(shape)
	var key: int = current_action.current_point_key()
	if not is_key_valid(key):
		gui_point_info_panel.visible = false
		return
	gui_point_info_panel.visible = true
	# Shrink panel
	gui_point_info_panel.size = Vector2(1, 1)

	var point := shape.get_point_array().get_point(key)
	gui_point_info_panel.set_idx(idx)
	gui_point_info_panel.set_texture_idx(point.texture_idx)
	gui_point_info_panel.set_width(point.width)
	gui_point_info_panel.set_flip(point.flip)


func _load_config() -> void:
	var conf := ConfigFile.new()
	conf.load(EditorInterface.get_editor_paths().get_project_settings_dir().path_join("ss2d.cfg"))
	_defer_mesh_updates = conf.get_value(SS2D_EditorTool.CONFIG_OPTIONS_SECTION, "defer_mesh_updates", false)
	tb_options_popup.set_item_checked(OPTIONS_MENU.ID_DEFER_MESH_UPDATES, _defer_mesh_updates)
	tb_snap_popup.set_item_checked(SNAP_MENU.ID_USE_GRID_SNAP, conf.get_value(SS2D_EditorTool.CONFIG_OPTIONS_SECTION, "use_grid_snap", false))
	tb_snap_popup.set_item_checked(SNAP_MENU.ID_SNAP_RELATIVE, conf.get_value(SS2D_EditorTool.CONFIG_OPTIONS_SECTION, "snap_relative", false))

	for i in _tools:
		i.load_config(conf)


func _save_config() -> void:
	var conf := ConfigFile.new()
	conf.set_value(SS2D_EditorTool.CONFIG_OPTIONS_SECTION, "defer_mesh_updates", _defer_mesh_updates)
	conf.set_value(SS2D_EditorTool.CONFIG_OPTIONS_SECTION, "use_grid_snap", tb_snap_popup.is_item_checked(SNAP_MENU.ID_USE_GRID_SNAP))
	conf.set_value(SS2D_EditorTool.CONFIG_OPTIONS_SECTION, "snap_relative", tb_snap_popup.is_item_checked(SNAP_MENU.ID_SNAP_RELATIVE))

	for i in _tools:
		i.save_config(conf)

	conf.save(EditorInterface.get_editor_paths().get_project_settings_dir().path_join("ss2d.cfg"))


func _process(_delta: float) -> void:
	if current_mode == MODE.FREEHAND:
		current_zoom_level = get_canvas_scale()


func get_canvas_scale() -> float:
	get_current_viewport()
	if target_viewport:
		return target_viewport.global_canvas_transform.x.x
	else:
		return 1.0


func get_current_viewport() -> void:
	if !get_tree().get_edited_scene_root():
		return
	var editor_viewport: Node = get_tree().get_edited_scene_root().get_parent()

	if editor_viewport is SubViewport:
		target_viewport = editor_viewport
	elif editor_viewport is SubViewportContainer:
		target_viewport = get_tree().get_edited_scene_root()
	else:
		target_viewport = editor_viewport.get_parent()


func _gui_update_edge_info_panel() -> void:
	# Don't update if already visible
	if gui_edge_info_panel.visible:
		return
	var indicies := Vector2i(-1, -1)
	var override: SS2D_Material_Edge_Metadata = null
	var pa := shape.get_point_array()
	if on_edge:
		var t: Transform2D = get_et() * shape.get_global_transform()
		var offset: float = pa.get_closest_offset_straight_edge(t.affine_inverse() * edge_point)
		var keys: Vector2i = _get_edge_point_keys_from_offset(offset, true)
		indicies = Vector2i(pa.get_point_index(keys.x), pa.get_point_index(keys.y))
		if pa.has_material_override(keys):
			override = pa.get_material_override(keys)
	gui_edge_info_panel.set_indicies(indicies)
	if override != null:
		gui_edge_info_panel.set_material_override(true)
		gui_edge_info_panel.load_values_from_meta_material(override)
	else:
		gui_edge_info_panel.set_material_override(false)

	# Shrink panel to minimum size
	gui_edge_info_panel.size = Vector2(1, 1)


func _gui_update_info_panels() -> void:
	if not is_2d_screen_active:
		_gui_hide_info_panels()
		return
	match current_mode:
		MODE.EDIT_VERT:
			_gui_update_vert_info_panel()
			gui_edge_info_panel.visible = false
		MODE.EDIT_EDGE:
			_gui_update_edge_info_panel()
			gui_point_info_panel.visible = false
		_:
			gui_point_info_panel.visible = false
			gui_edge_info_panel.visible = false


func _gui_hide_info_panels() -> void:
	gui_edge_info_panel.visible = false
	gui_point_info_panel.visible = false

#########
# GODOT #
#########


# Called when saving
# https://docs.godotengine.org/en/3.2/classes/class_editorplugin.html?highlight=switch%20scene%20tab
func _apply_changes() -> void:
	gui_point_info_panel.visible = false
	gui_edge_info_panel.visible = false


func _init() -> void:
	pass


func _ready() -> void:
	# Support the undo-redo actions
	_gui_build_toolbar()
	_load_config()
	add_child(gui_point_info_panel)
	gui_point_info_panel.visible = false
	add_child(gui_edge_info_panel)
	gui_edge_info_panel.visible = false
	gui_edge_info_panel.connect("material_override_toggled", self._on_edge_material_override_toggled)
	gui_edge_info_panel.connect("render_toggled", self._on_edge_material_override_render_toggled)
	gui_edge_info_panel.connect("weld_toggled", self._on_edge_material_override_weld_toggled)
	gui_edge_info_panel.connect("z_index_changed", self._on_edge_material_override_z_index_changed)
	gui_edge_info_panel.connect("edge_material_changed", self._on_edge_material_changed)
	add_child(gui_snap_settings)
	gui_snap_settings.hide()

	make_unique_dialog = AcceptDialog.new()
	make_unique_dialog.title = "Make Shape Unique"
	make_unique_dialog.get_label().text = "Make shape point geometry unique (not materials). Proceed?"
	make_unique_dialog.get_ok_button().text = "Proceed"
	make_unique_dialog.add_cancel_button("Cancel")
	make_unique_dialog.theme = EditorInterface.get_base_control().theme
	make_unique_dialog.connect("confirmed", self._shape_make_unique)
	add_child(make_unique_dialog)

	connect("main_screen_changed", self._on_main_screen_changed)

	perform_version_check_and_conversion()


func _enter_tree() -> void:
	@warning_ignore("unsafe_method_access")
	plugin = load("res://addons/rmsmartshape/editors/normal_range_inspector_plugin.gd").new()
	if plugin != null:
		add_inspector_plugin(plugin)

	@warning_ignore("unsafe_method_access")
	var action_plugin: EditorInspectorPlugin = load("res://addons/rmsmartshape/editors/action_property_inspector_plugin.gd").new()
	if action_plugin != null:
		add_inspector_plugin(action_plugin)


func _exit_tree() -> void:
	if (plugin != null):
		remove_inspector_plugin(plugin)

	_save_config()

	gui_point_info_panel.visible = false
	gui_edge_info_panel.visible = false
	remove_control_from_container(EditorPlugin.CONTAINER_CANVAS_EDITOR_MENU, tb_hb)
	tb_hb.queue_free()


func _forward_canvas_gui_input(event: InputEvent) -> bool:
	if not is_shape_valid():
		return false

	# Force update if global transforma has been changed
	if cached_shape_global_transform != shape.get_global_transform():
		shape.set_as_dirty()
		cached_shape_global_transform = shape.get_global_transform()

	var et: Transform2D = get_et()
	var grab_threshold: float = EditorInterface.get_editor_settings().get(
		"editors/polygon_editor/point_grab_radius"
	)

	var key_return_value := false
	if event is InputEventKey:
		key_return_value = _input_handle_keyboard_event(event)

	var mb_return_value := false
	if event is InputEventMouseButton:
		mb_return_value = _input_handle_mouse_button_event(event, et, grab_threshold)

	var mm_return_value := false
	if event is InputEventMouseMotion:
		mb_return_value = _input_handle_mouse_motion_event(event, et, grab_threshold)

	var return_value := key_return_value == true or mb_return_value == true or mm_return_value == true
	_gui_update_info_panels()
	return return_value


func _handles(object: Object) -> bool:
	var hideToolbar: bool = true

	update_overlays()
	gui_point_info_panel.visible = false
	gui_edge_info_panel.visible = false

	var selection: EditorSelection = EditorInterface.get_selection()
	if selection != null:
		if selection.get_selected_nodes().size() == 1:
			if selection.get_selected_nodes()[0] is SS2D_Shape:
				hideToolbar = false

	if hideToolbar == true:
		tb_hb.hide()

	if object is Resource:
		return false

	return object is SS2D_Shape


func _edit(object: Object) -> void:
	on_edge = false
	deselect_verts()
	if is_shape_valid():
		disconnect_shape(shape)

	shape = object

	if not is_shape_valid():
		gui_point_info_panel.visible = false
		gui_edge_info_panel.visible = false
		shape = null
	else:
		connect_shape(shape)

		if shape.get_point_array().get_point_count() == 0:
			_enter_mode(MODE.CREATE_VERT)
		elif current_mode == MODE.CREATE_VERT:
			_enter_mode(MODE.EDIT_VERT)

	update_overlays()


func _make_visible(visible: bool) -> void:
	if visible:
		tb_hb.show()
	else:
		tb_hb.hide()



func _on_main_screen_changed(screen_name: String) -> void:
	is_2d_screen_active = screen_name == "2D"
	if not is_2d_screen_active:
		_gui_hide_info_panels()


############
# SNAPPING #
############
func use_global_snap() -> bool:
	return not tb_snap_popup.is_item_checked(SNAP_MENU.ID_SNAP_RELATIVE)


func use_snap() -> bool:
	return tb_snap_popup.is_item_checked(SNAP_MENU.ID_USE_GRID_SNAP)


func get_snap_offset() -> Vector2:
	return gui_snap_settings.get_snap_offset()


func get_snap_step() -> Vector2:
	return gui_snap_settings.get_snap_step()


func snap(v: Vector2, force: bool = false) -> Vector2:
	if not use_snap() and not force:
		return v
	var step: Vector2 = get_snap_step()
	var offset: Vector2 = get_snap_offset()
	var t := Transform2D.IDENTITY
	if use_global_snap():
		t = shape.get_global_transform()
	return SS2D_PluginFunctionality.snap_position(v, offset, step, t)


##########
# PLUGIN #
##########

func disconnect_shape(s: SS2D_Shape) -> void:
	if s.is_connected("make_unique_pressed", self._on_shape_make_unique):
		s.disconnect("make_unique_pressed", self._on_shape_make_unique)


func connect_shape(s: SS2D_Shape) -> void:
	if not s.is_connected("make_unique_pressed", self._on_shape_make_unique):
		s.connect("make_unique_pressed", self._on_shape_make_unique)


func get_material_override_from_indicies() -> SS2D_Material_Edge_Metadata:
	var keys := shape.get_point_array().get_edge_keys_for_indices(gui_edge_info_panel.indicies)
	return shape.get_point_array().get_material_override(keys)


func _on_edge_material_override_render_toggled(enabled: bool) -> void:
	var override := get_material_override_from_indicies()
	if override != null:
		override.render = enabled


func _on_edge_material_override_weld_toggled(enabled: bool) -> void:
	var override := get_material_override_from_indicies()
	if override != null:
		override.weld = enabled


func _on_edge_material_override_z_index_changed(z: int) -> void:
	var override := get_material_override_from_indicies()
	if override != null:
		override.z_index = z


func _on_edge_material_changed(m: SS2D_Material_Edge) -> void:
	var override := get_material_override_from_indicies()
	if override != null:
		override.edge_material = m


func _on_edge_material_override_toggled(enabled: bool) -> void:
	var indices := gui_edge_info_panel.indicies

	if SS2D_IndexTuple.has(indices, -1):
		return

	var keys := shape.get_point_array().get_edge_keys_for_indices(indices)

	# Get the relevant Override data if any exists
	var override: SS2D_Material_Edge_Metadata = shape.get_point_array().get_material_override(keys)

	if enabled:
		if override == null:
			override = SS2D_Material_Edge_Metadata.new()
			override.edge_material = null
			shape.get_point_array().set_material_override(keys, override)

		# Load override data into the info panel
		gui_edge_info_panel.load_values_from_meta_material(override)
	else:
		if override != null:
			shape.get_point_array().remove_material_override(keys)


func is_shape_valid() -> bool:
	if shape == null:
		return false
	if not is_instance_valid(shape):
		return false
	if not shape.is_inside_tree():
		return false
	return true


func _on_shape_make_unique(_shape: SS2D_Shape) -> void:
	make_unique_dialog.popup_centered()


func _shape_make_unique() -> void:
	perform_action(SS2D_ActionMakeShapeUnique.new(shape))


func get_et() -> Transform2D:
	return EditorInterface.get_edited_scene_root().get_viewport().global_canvas_transform


func is_key_valid(key: int) -> bool:
	if not is_shape_valid():
		return false
	return shape.get_point_array().has_point(key)


func _enter_mode(mode: int) -> void:
	if current_mode == mode:
		return

	for tb: Button in [tb_vert_edit, tb_edge_edit, tb_pivot, tb_center_pivot, tb_vert_create, tb_freehand]:
		tb.button_pressed = false

	previous_mode = current_mode
	current_mode = mode
	match mode:
		MODE.CREATE_VERT:
			tb_vert_create.button_pressed = true
		MODE.EDIT_VERT:
			tb_vert_edit.button_pressed = true
		MODE.EDIT_EDGE:
			tb_edge_edit.button_pressed = true
		MODE.CUT_EDGE:
			tb_edge_cut.button_pressed = true
		MODE.SET_PIVOT:
			tb_pivot.button_pressed = true
		MODE.FREEHAND:
			tb_freehand.button_pressed = true
		_:
			tb_vert_edit.button_pressed = true

	update_overlays()


func _center_pivot() -> void:
	if shape and shape.get_point_array().is_shape_closed():
		# Calculate centroid
		var points: PackedVector2Array = shape.get_point_array().get_tessellated_points()
		var point_count: int = points.size()
		var total_area: float = 0.0
		var center: Vector2 = Vector2.ZERO
		for i in range(point_count):
			var pt1: Vector2 = points[i]
			var pt2: Vector2
			if i == point_count - 1:
				pt2 = points[0]
			else:
				pt2 = points[i + 1]

			var triangle_area: float = pt1.cross(pt2)
			total_area += triangle_area
			center += (pt1 + pt2) * triangle_area

		if total_area != 0.0:
			center /= 3 * total_area

		perform_action(SS2D_ActionSetPivot.new(shape, shape.to_global(center)))

#############
# RENDERING #
#############

func _forward_canvas_draw_over_viewport(overlay: Control) -> void:
	# Something might force a draw which we had no control over,
	# in this case do some updating to be sure
	if not is_shape_valid() or not is_inside_tree():
		return

	match current_mode:
		MODE.CREATE_VERT:
			draw_mode_edit_vert(overlay)
			if Input.is_key_pressed(KEY_ALT) and Input.is_key_pressed(KEY_SHIFT):
				draw_new_shape_preview(overlay)
			elif Input.is_key_pressed(KEY_ALT):
				draw_new_point_close_preview(overlay)
			else:
				draw_new_point_preview(overlay)
		MODE.EDIT_VERT:
			draw_mode_edit_vert(overlay)
			if Input.is_key_pressed(KEY_ALT):
				if Input.is_key_pressed(KEY_SHIFT):
					draw_new_shape_preview(overlay)
				elif not on_edge:
					draw_new_point_close_preview(overlay)
		MODE.EDIT_EDGE:
			draw_mode_edit_edge(overlay, Color.WHITE, Color.YELLOW)
		MODE.CUT_EDGE:
			draw_mode_cut_edge(overlay)
		MODE.FREEHAND:
			if not _mouse_lmb_pressed:
				draw_new_point_close_preview(overlay)
			draw_freehand_circle(overlay)
			draw_mode_edit_vert(overlay, false)


func draw_freehand_circle(overlay: Control) -> void:
	var mouse: Vector2 = overlay.get_local_mouse_position()
	var size: float = freehand_paint_size
	var color := Color.WHITE
	if Input.is_key_pressed(KEY_CTRL):
		color = Color.RED
		size = freehand_erase_size
	color.a = 0.5
	overlay.draw_arc(mouse, size * 2 * current_zoom_level, 0, TAU, 64, color, 1, true)
	color.a = 0.05
	overlay.draw_circle(mouse, size * 2 * current_zoom_level, color)


func draw_mode_edit_edge(overlay: Control, color_normal: Color, color_highlight: Color) -> void:
	var t: Transform2D = get_et() * shape.get_global_transform()
	var pa := shape.get_point_array()
	var verts: PackedVector2Array = pa.get_vertices()

	draw_shape_outline(overlay, t, verts, color_normal)
	draw_vert_handles(overlay, t, verts, false)

	if current_action.type == ACTION_VERT.MOVE_VERT:
		var edge_point_keys := current_action.keys
		var p1: Vector2 = pa.get_point_position(edge_point_keys[0])
		var p2: Vector2 = pa.get_point_position(edge_point_keys[1])
		overlay.draw_line(t * p1, t * p2, Color.BLACK, 8.0, true)
		overlay.draw_line(t * p1, t * p2, color_highlight, 4.0, true)
	elif on_edge:
		var offset: float = pa.get_closest_offset_straight_edge(t.affine_inverse() * edge_point)
		var edge_point_keys := _get_edge_point_keys_from_offset(offset, true)
		var p1: Vector2 = pa.get_point_position(edge_point_keys.x)
		var p2: Vector2 = pa.get_point_position(edge_point_keys.y)
		overlay.draw_line(t * p1, t * p2, Color.BLACK, 8.0, true)
		overlay.draw_line(t * p1, t * p2, color_highlight, 4.0, true)


func draw_mode_cut_edge(overlay: Control) -> void:
	draw_mode_edit_edge(overlay, Color(1.0, 0.25, 0.25, 0.8), Color(1.0, 0.75, 0.75, 1.0))

	if on_edge:
		# Draw "X" marks along the edge that is selected
		var t: Transform2D = get_et() * shape.get_global_transform()
		var pa := shape.get_point_array()
		var offset: float = pa.get_closest_offset_straight_edge(t.affine_inverse() * edge_point)
		var edge_point_keys := _get_edge_point_keys_from_offset(offset, true)
		var from: Vector2 = t * pa.get_point_position(edge_point_keys.x)
		var to: Vector2 = t * pa.get_point_position(edge_point_keys.y)
		var dir: Vector2 = (to - from).normalized()
		var angle: float = dir.angle()
		var length: float = (to - from).length()
		var num_crosses := remap(length, 0.0, 2000.0, 0.0, 10.0)
		@warning_ignore("narrowing_conversion")
		num_crosses = snappedi(num_crosses, 2.0) + 1
		var fraction := 1.0 / (num_crosses + 1)
		for i in num_crosses:
			var pos: Vector2 = from + dir * length * fraction * (i + 1)
			overlay.draw_line(Vector2(8.0, 8.0).rotated(angle) + pos,
					Vector2(-8.0, -8.0).rotated(angle) + pos, Color.RED, 3.0, true)
			overlay.draw_line(Vector2(-8.0, 8.0).rotated(angle) + pos,
					Vector2(8.0, -8.0).rotated(angle) + pos, Color.RED, 3.0, true)


func draw_mode_edit_vert(overlay: Control, show_vert_handles: bool = true) -> void:
	var t: Transform2D = get_et() * shape.get_global_transform()
	var pa := shape.get_point_array()
	var verts: PackedVector2Array = pa.get_vertices()
	var points: PackedVector2Array = pa.get_tessellated_points()
	draw_shape_outline(overlay, t, points, shape.modulate)
	if show_vert_handles:
		draw_vert_handles(overlay, t, verts, true)
	if on_edge:
		overlay.draw_texture(ICON_ADD_HANDLE, edge_point - ICON_ADD_HANDLE.get_size() * 0.5)

	# Draw Highlighted Handle
	if current_action.is_single_vert_selected():
		var tex: Texture2D = ICON_HANDLE_SELECTED
		overlay.draw_texture(
			tex, t * verts[current_action.current_point_index(shape)] - tex.get_size() * 0.5
		)


func draw_shape_outline(
	overlay: Control, t: Transform2D, points: PackedVector2Array, color: Color, width: float = 2.0
) -> void:
	if points.size() >= 2:
		overlay.draw_polyline(t * points, Color.BLACK, width * 1.5, true)
		overlay.draw_polyline(t * points, color, width, true)


func draw_vert_handles(
	overlay: Control, t: Transform2D, verts: PackedVector2Array, control_points: bool
) -> void:
	var transformed_verts := t * verts
	var pa := shape.get_point_array()
	for i in verts.size():
		# Draw Vert handles
		var hp: Vector2 = transformed_verts[i]
		var icon: Texture2D = ICON_HANDLE_BEZIER if (Input.is_key_pressed(KEY_SHIFT) and not current_mode == MODE.FREEHAND) else ICON_HANDLE
		overlay.draw_texture(icon, hp - icon.get_size() * 0.5)

	# Draw Width handle
	var offset: float = WIDTH_HANDLE_OFFSET
	var width_handle_key: int = closest_key
	if (
		Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)
		and current_action.type == ACTION_VERT.MOVE_WIDTH_HANDLE
	):
		offset *= width_scaling
		width_handle_key = current_action.keys[0]

	var point_index: int = pa.get_point_index(width_handle_key)
	if point_index == -1:
		return

	var width_handle_normal: Vector2 = _get_vert_normal(t, verts, point_index)
	var vertex_position: Vector2 = t * pa.get_point_position(width_handle_key)
	var icon_position: Vector2 = vertex_position + width_handle_normal * offset
	var size: Vector2 = Vector2.ONE * 10.0
	var width_handle_color := Color("f53351")
	overlay.draw_line(vertex_position, icon_position, width_handle_color, 1.0)
	overlay.draw_set_transform(icon_position, width_handle_normal.angle(), Vector2.ONE)
	overlay.draw_rect(Rect2(-size / 2.0, size), width_handle_color, true)
	overlay.draw_set_transform(Vector2.ZERO, 0, Vector2.ONE)

	# Draw Control point handles
	if control_points:
		for i in verts.size():
			var key: int = pa.get_point_key_at_index(i)
			var hp: Vector2 = transformed_verts[i]

			# Drawing the point-out for the last point makes no sense, as there's no point ahead of it
			if i < verts.size() - 1:
				var pointout: Vector2 = t * (verts[i] + pa.get_point_out(key))
				if hp != pointout:
					_draw_control_point_line(overlay, hp, pointout, ICON_HANDLE_CONTROL)
			# Drawing the point-in for point 0 makes no sense, as there's no point behind it
			if i > 0:
				var pointin: Vector2 = t * (verts[i] + pa.get_point_in(key))
				if hp != pointin:
					_draw_control_point_line(overlay, hp, pointin, ICON_HANDLE_CONTROL)


func _draw_control_point_line(c: Control, vert: Vector2, cp: Vector2, tex: Texture2D) -> void:
	# Draw the line with a dark and light color to be visible on all backgrounds
	var color_dark := Color(0, 0, 0, 0.3)
	var color_light := Color(1, 1, 1, .5)
	var width := 2.0
	var normal := (cp - vert).normalized()
	c.draw_line(vert + normal * 4 + Vector2.DOWN, cp + Vector2.DOWN, color_dark, width)
	c.draw_line(vert + normal * 4, cp, color_light, width)
	c.draw_texture(tex, cp - tex.get_size() * 0.5)


func draw_new_point_preview(overlay: Control) -> void:
	# Draw lines to where a new point will be added
	var pa := shape.get_point_array()
	var verts: PackedVector2Array = pa.get_vertices()
	var t: Transform2D = get_et() * shape.get_global_transform()
	var color := Color(1, 1, 1, .5)
	var width := 2.0
	var mouse: Vector2 = overlay.get_local_mouse_position()

	if verts.size() > 0:
		var a: Vector2
		if pa.is_shape_closed() and verts.size() > 1:
			a = t * verts[verts.size() - 2]
			overlay.draw_line(mouse, t * verts[0], color,width * .5)
		else:
			a = t * verts[verts.size() - 1]
		overlay.draw_line(mouse, a, color, width)

	overlay.draw_texture(ICON_ADD_HANDLE, mouse - ICON_ADD_HANDLE.get_size() * 0.5)


func draw_new_point_close_preview(overlay: Control) -> void:
	# Draw lines to where a new point will be added
	var t: Transform2D = get_et() * shape.get_global_transform()
	var pa := shape.get_point_array()
	var color := Color(1, 1, 1, .5)
	var width := 2.0

	var mouse: Vector2 = overlay.get_local_mouse_position()
	var a: Vector2 = t * pa.get_point_position(closest_edge_keys[0])
	var b: Vector2 = t * pa.get_point_position(closest_edge_keys[1])
	overlay.draw_line(mouse, a, color, width)
	color.a = 0.1
	overlay.draw_line(mouse, b, color, width)
	overlay.draw_texture(ICON_ADD_HANDLE, mouse - ICON_ADD_HANDLE.get_size() * 0.5)


func draw_new_shape_preview(overlay: Control) -> void:
	# Draw a plus where a new shape will be added
	var mouse: Vector2 = overlay.get_local_mouse_position()
	overlay.draw_texture(ICON_ADD_HANDLE, mouse - ICON_ADD_HANDLE.get_size() * 0.5)


##########
# PLUGIN #
##########
func deselect_verts() -> void:
	current_action = ActionDataVert.new([], [], [], [], [], ACTION_VERT.NONE)


func select_verticies(keys: PackedInt32Array, action: ACTION_VERT) -> ActionDataVert:
	# FIXME: This is likely buggy, not all point properties are stored
	var pa := shape.get_point_array()
	var from_positions := PackedVector2Array()
	var from_positions_c_in := PackedVector2Array()
	var from_positions_c_out := PackedVector2Array()
	var from_widths := PackedFloat32Array()
	for key in keys:
		from_positions.push_back(pa.get_point_position(key))
		from_positions_c_in.push_back(pa.get_point_in(key))
		from_positions_c_out.push_back(pa.get_point_out(key))
		from_widths.push_back(pa.get_point(key).width)
	return ActionDataVert.new(
		keys, from_positions, from_positions_c_in, from_positions_c_out, from_widths, action
	)


func select_vertices_to_move(keys: PackedInt32Array, _mouse_starting_pos_viewport: Vector2) -> void:
	_mouse_motion_delta_starting_pos = _mouse_starting_pos_viewport
	current_action = select_verticies(keys, ACTION_VERT.MOVE_VERT)


func select_control_points_to_move(
	keys: PackedInt32Array, _mouse_starting_pos_viewport: Vector2, action: ACTION_VERT = ACTION_VERT.MOVE_CONTROL
) -> void:
	current_action = select_verticies(keys, action)
	_mouse_motion_delta_starting_pos = _mouse_starting_pos_viewport


func select_width_handle_to_move(keys: PackedInt32Array, _mouse_starting_pos_viewport: Vector2) -> void:
	_mouse_motion_delta_starting_pos = _mouse_starting_pos_viewport
	current_action = select_verticies(keys, ACTION_VERT.MOVE_WIDTH_HANDLE)


# TODO: Move into SS2D_EditorTool eventually
func perform_action(action: SS2D_Action) -> void:
	var undo := get_undo_redo()
	undo.create_action(action.get_name(), UndoRedo.MERGE_DISABLE, shape.get_point_array())
	undo.add_do_method(action, "do")
	undo.add_do_method(self, "update_overlays")
	undo.add_undo_method(action, "undo")
	undo.add_undo_method(self, "update_overlays")
	undo.commit_action()


#########
# INPUT #
#########
func _input_handle_right_click_press(mb_position: Vector2, grab_threshold: float) -> bool:
	if not shape.can_edit:
		return false
	if current_mode == MODE.EDIT_VERT or current_mode == MODE.CREATE_VERT:
		# Mouse over a single vertex?
		if current_action.is_single_vert_selected():
			perform_action(SS2D_ActionDeletePoint.new(shape, current_action.keys[0]))
			deselect_verts()
			return true
		else:
			# Mouse over a control point?
			var et: Transform2D = get_et()
			var points_in: Array = SS2D_PluginFunctionality.get_intersecting_control_point_in(
				shape, et, mb_position, grab_threshold
			)
			var points_out: Array = SS2D_PluginFunctionality.get_intersecting_control_point_out(
				shape, et, mb_position, grab_threshold
			)
			if not points_in.is_empty():
				perform_action(SS2D_ActionDeleteControlPoint.new(shape, points_in[0], SS2D_ActionDeleteControlPoint.PointType.POINT_IN))
				return true
			elif not points_out.is_empty():
				perform_action(SS2D_ActionDeleteControlPoint.new(shape, points_out[0], SS2D_ActionDeleteControlPoint.PointType.POINT_OUT))
				return true
	elif current_mode == MODE.EDIT_EDGE:
		if on_edge:
			gui_edge_info_panel.visible = not gui_edge_info_panel.visible
			gui_edge_info_panel.position = get_window().get_mouse_position()
			return true
	return false


func _input_handle_left_click(
	mb: InputEventMouseButton,
	vp_m_pos: Vector2,
	t: Transform2D,
	et: Transform2D,
	grab_threshold: float
) -> bool:
	# Set Pivot?
	if current_mode == MODE.SET_PIVOT:
		var local_position: Vector2 = et.affine_inverse() * mb.position
		if use_snap():
			local_position = snap(local_position)
		perform_action(SS2D_ActionSetPivot.new(shape, local_position))
		return true

	var pa := shape.get_point_array()

	if current_mode == MODE.EDIT_VERT or current_mode == MODE.CREATE_VERT:
		gui_edge_info_panel.visible = false
		var can_add_point: bool = Input.is_key_pressed(KEY_ALT) or current_mode == MODE.CREATE_VERT
		var is_first_selected: bool = current_action.is_single_vert_selected() and current_action.current_point_key() == pa.get_point_key_at_index(0)

		if _defer_mesh_updates:
			pa.begin_update()

		# Close the shape if the first point is clicked
		if can_add_point and is_first_selected and pa.can_close():
			var close_action := SS2D_ActionCloseShape.new(shape)
			perform_action(close_action)
			if Input.is_key_pressed(KEY_SHIFT):
				select_control_points_to_move([close_action.get_key()], vp_m_pos)
			else:
				select_vertices_to_move([close_action.get_key()], vp_m_pos)
			return true

		# Any nearby control points to move?
		if not Input.is_key_pressed(KEY_ALT):
			if _input_move_control_points(mb, vp_m_pos, grab_threshold):
				return true

			# Highlighting a vert to move or add control points to
			if current_action.is_single_vert_selected():
				if on_width_handle:
					select_width_handle_to_move([current_action.current_point_key()], vp_m_pos)
				elif Input.is_key_pressed(KEY_SHIFT):
					select_control_points_to_move([current_action.current_point_key()], vp_m_pos)
					return true
				else:
					select_vertices_to_move([current_action.current_point_key()], vp_m_pos)
					return true

		# Split the Edge?
		if _input_split_edge(mb, vp_m_pos, t):
			return true

		if not on_edge and can_add_point:
			# Create new point
			var local_position: Vector2 = t.affine_inverse() * mb.position
			if use_snap():
				local_position = snap(local_position)

			var idx: int = -1
			if Input.is_key_pressed(KEY_SHIFT) and Input.is_key_pressed(KEY_ALT):
				# Copy shape with a new single point
				var copy: SS2D_Shape = copy_shape(shape)
				copy.set_point_array(SS2D_Point_Array.new())
				_enter_mode(MODE.CREATE_VERT)
				var selection := EditorInterface.get_selection()
				selection.clear()
				selection.add_node(copy)
				shape = copy
			elif Input.is_key_pressed(KEY_ALT):
				# Add point between start and end points of the closest edge
				idx = pa.get_point_index(closest_edge_keys[1])
			var add_point := SS2D_ActionAddPoint.new(shape, local_position, idx, not _defer_mesh_updates)
			perform_action(add_point)
			if Input.is_key_pressed(KEY_SHIFT) and not Input.is_key_pressed(KEY_ALT):
				select_control_points_to_move([add_point.get_key()], vp_m_pos)
			else:
				select_vertices_to_move([add_point.get_key()], vp_m_pos)
			return true
	elif current_mode == MODE.EDIT_EDGE:
		if gui_edge_info_panel.visible:
			gui_edge_info_panel.visible = false
			return true
		if on_edge:
			# Grab Edge (2 points)
			var offset: float = pa.get_closest_offset_straight_edge(
				t.affine_inverse() * edge_point
			)
			var edge_point_keys := _get_edge_point_keys_from_offset(offset, true)
			select_vertices_to_move([edge_point_keys.x, edge_point_keys.y], vp_m_pos)
			if _defer_mesh_updates:
				pa.begin_update()
		return true
	elif current_mode == MODE.CUT_EDGE:
		if not on_edge:
			return true
		var offset: float = pa.get_closest_offset_straight_edge(t.affine_inverse() * edge_point)
		var edge_keys := _get_edge_point_keys_from_offset(offset, true)
		perform_action(SS2D_ActionCutEdge.new(shape, edge_keys.x, edge_keys.y))
		on_edge = false
		return true
	elif current_mode == MODE.FREEHAND:
		return true
	return false


func _input_handle_mouse_wheel(btn: int) -> bool:
	if current_mode == MODE.FREEHAND:
		if Input.is_key_pressed(KEY_CTRL) and Input.is_key_pressed(KEY_SHIFT):
			var step_multiplier := 1.2 if btn == MOUSE_BUTTON_WHEEL_UP else 0.8
			freehand_erase_size = roundf(clampf(freehand_erase_size * step_multiplier, 5, 400))
			update_overlays()
			return true
		elif Input.is_key_pressed(KEY_SHIFT):
			var step_multiplier := 1.2 if btn == MOUSE_BUTTON_WHEEL_UP else 0.8
			freehand_paint_size = roundf(clampf(freehand_paint_size * step_multiplier, 5, 400))
			update_overlays()
			return true
	elif current_action.is_single_vert_selected():
		if not shape.can_edit:
			return false
		var key: int = current_action.current_point_key()
		var point := shape.get_point_array().get_point(key)
		if Input.is_key_pressed(KEY_SHIFT):
			var width_step := 0.1
			if btn == MOUSE_BUTTON_WHEEL_DOWN:
				width_step *= -1
			point.width = point.width + width_step

		else:
			var texture_idx_step := 1
			if btn == MOUSE_BUTTON_WHEEL_DOWN:
				texture_idx_step *= -1
			point.texture_idx = point.texture_idx + texture_idx_step

		update_overlays()
		_gui_update_info_panels()
		return true

	return false


func _input_handle_keyboard_event(event: InputEventKey) -> bool:
	if not shape.can_edit:
		return false
	var kb: InputEventKey = event
	if _is_valid_keyboard_scancode(kb):
		if current_action.is_single_vert_selected():
			if kb.pressed and kb.keycode == KEY_SPACE:
				var key: int = current_action.current_point_key()
				var point := shape.get_point_array().get_point(key)
				point.flip = not point.flip
				_gui_update_info_panels()

		if kb.pressed and kb.keycode == KEY_ESCAPE:
			# Hide edge_info_panel
			if gui_edge_info_panel.visible:
				gui_edge_info_panel.visible = false

			if current_mode == MODE.CREATE_VERT:
				_enter_mode(MODE.EDIT_VERT)

		if kb.keycode == KEY_CTRL:
			if kb.pressed and not kb.echo:
				on_edge = false
				if closest_key != -1:
					current_action = select_verticies([closest_key], ACTION_VERT.NONE)
			else:
				deselect_verts()
			update_overlays()

		if kb.keycode == KEY_ALT:
			update_overlays()

		return true
	return false


func _is_valid_keyboard_scancode(kb: InputEventKey) -> bool:
	match kb.keycode:
		KEY_ESCAPE:
			return true
		KEY_ENTER:
			return true
		KEY_SPACE:
			return true
		KEY_SHIFT:
			return true
		KEY_ALT:
			return true
		KEY_CTRL:
			return true
	return false


func _input_handle_mouse_button_event(
	event: InputEventMouseButton, et: Transform2D, grab_threshold: float
) -> bool:
	if not shape.can_edit:
		return false
	var t: Transform2D = et * shape.get_global_transform()
	var mb: InputEventMouseButton = event
	var viewport_mouse_position: Vector2 = et.affine_inverse() * mb.position
	var mouse_wheel_spun: bool = (
		mb.pressed
		and (mb.button_index == MOUSE_BUTTON_WHEEL_DOWN or mb.button_index == MOUSE_BUTTON_WHEEL_UP)
	)

	#######################################
	# Left Mouse Button released
	if not mb.pressed and mb.button_index == MOUSE_BUTTON_LEFT:
		_mouse_lmb_pressed = false
		var rslt: bool = false
		var type: ACTION_VERT = current_action.type
		var _in := type == ACTION_VERT.MOVE_CONTROL or type == ACTION_VERT.MOVE_CONTROL_IN
		var _out := type == ACTION_VERT.MOVE_CONTROL or type == ACTION_VERT.MOVE_CONTROL_OUT
		if type == ACTION_VERT.MOVE_VERT:
			perform_action(SS2D_ActionMoveVerticies.new(shape, current_action.keys,
					current_action.starting_positions))
			rslt = true
		elif _in or _out:
			perform_action(SS2D_ActionMoveControlPoints.new(
				shape,
				current_action.keys,
				current_action.starting_positions_control_in,
				current_action.starting_positions_control_out
			))
			rslt = true
		elif current_mode == MODE.FREEHAND:
			if _defer_mesh_updates:
				shape.get_point_array().end_update()
		deselect_verts()
		return rslt

	#######################################
	# Right Mouse Button released
	if not mb.pressed and mb.button_index == MOUSE_BUTTON_RIGHT:
		_mouse_rmb_pressed = false

	#########################################
	# Mouse Wheel on valid point
	elif mouse_wheel_spun:
		return _input_handle_mouse_wheel(mb.button_index)

	#########################################
	# Mouse left click
	elif mb.pressed and mb.button_index == MOUSE_BUTTON_LEFT:
		_mouse_lmb_pressed = true
		return _input_handle_left_click(mb, viewport_mouse_position, t, et, grab_threshold)

	#########################################
	# Mouse right click
	elif mb.pressed and mb.button_index == MOUSE_BUTTON_RIGHT:
		_mouse_rmb_pressed = true
		return _input_handle_right_click_press(mb.position, grab_threshold)

	return false


func _input_split_edge(mb: InputEventMouseButton, vp_m_pos: Vector2, t: Transform2D) -> bool:
	if not on_edge:
		return false
	var gpoint: Vector2 = mb.position
	var insertion_point: int = -1
	var pa := shape.get_point_array()
	var mb_offset: float = pa.get_closest_offset(t.affine_inverse() * gpoint)

	insertion_point = pa.get_point_index(_get_edge_point_keys_from_offset(mb_offset)[1])

	if insertion_point == -1:
		insertion_point = pa.get_point_count() - 1

	var split_curve := SS2D_ActionSplitCurve.new(shape, insertion_point, gpoint, t, not _defer_mesh_updates)
	perform_action(split_curve)
	select_vertices_to_move([split_curve.get_key()], vp_m_pos)
	on_edge = false

	if _defer_mesh_updates:
		pa.begin_update()

	return true


func _input_move_control_points(mb: InputEventMouseButton, vp_m_pos: Vector2, grab_threshold: float) -> bool:
	var points_in := SS2D_PluginFunctionality.get_intersecting_control_point_in(
		shape, get_et(), mb.position, grab_threshold
	)
	var points_out := SS2D_PluginFunctionality.get_intersecting_control_point_out(
		shape, get_et(), mb.position, grab_threshold
	)
	if not points_in.is_empty():
		select_control_points_to_move([points_in[0]], vp_m_pos, ACTION_VERT.MOVE_CONTROL_IN)
		return true
	elif not points_out.is_empty():
		select_control_points_to_move([points_out[0]], vp_m_pos, ACTION_VERT.MOVE_CONTROL_OUT)
		return true
	return false


func _get_edge_point_keys_from_offset(
	offset: float, straight: bool = false, _position := Vector2(0, 0)
) -> Vector2i:
	var pa := shape.get_point_array()
	for i in range(0, pa.get_point_count() - 1, 1):
		var key: int = pa.get_point_key_at_index(i)
		var key_next: int = pa.get_point_key_at_index(i + 1)
		var this_offset := 0.0
		var next_offset := 0.0
		if straight:
			this_offset = pa.get_closest_offset_straight_edge(pa.get_point_position(key))
			next_offset = pa.get_closest_offset_straight_edge(pa.get_point_position(key_next))
		else:
			this_offset = pa.get_closest_offset(pa.get_point_position(key))
			next_offset = pa.get_closest_offset(pa.get_point_position(key_next))

		if offset >= this_offset and offset <= next_offset:
			return Vector2i(key, key_next)
		# for when the shape is closed and the final point has an offset of 0
		if next_offset == 0 and offset >= this_offset:
			return Vector2i(key, key_next)
	return Vector2i(-1, -1)


func _input_motion_is_on_edge(mm: InputEventMouseMotion, grab_threshold: float) -> bool:
	var xform: Transform2D = get_et() * shape.get_global_transform()
	var pa := shape.get_point_array()

	if pa.get_point_count() < 2:
		return false

	# Find edge
	var closest_point: Vector2
	if current_mode == MODE.EDIT_EDGE or current_mode == MODE.CUT_EDGE:
		closest_point = pa.get_closest_point_straight_edge(
			xform.affine_inverse() * mm.position
		)
	else:
		closest_point = pa.get_closest_point(xform.affine_inverse() * mm.position)
	edge_point = xform * closest_point
	if edge_point.distance_to(mm.position) <= grab_threshold:
		return true
	return false


func _input_find_closest_edge_keys(mm: InputEventMouseMotion) -> void:
	var pa := shape.get_point_array()

	if pa.get_point_count() < 2:
		return

	# Find edge
	var xform: Transform2D = get_et() * shape.get_global_transform()
	var closest_point: Vector2 = pa.get_closest_point_straight_edge(xform.affine_inverse() * mm.position)
	var edge_p: Vector2 = xform * closest_point
	var offset: float = pa.get_closest_offset_straight_edge(xform.affine_inverse() * edge_p)
	closest_edge_keys = _get_edge_point_keys_from_offset(offset, true, xform.affine_inverse() * mm.position)


func get_mouse_over_vert_key(mm: InputEventMouseMotion, grab_threshold: float) -> int:
	var xform: Transform2D = get_et() * shape.get_global_transform()
	var pa := shape.get_point_array()
	# However, if near a control point or one of its handles then we are not on the edge
	for k in pa.get_all_point_keys():
		var pp: Vector2 = pa.get_point_position(k)
		var p: Vector2 = xform * pp
		if p.distance_to(mm.position) <= grab_threshold:
			return k
	return -1


func get_mouse_over_width_handle(mm: InputEventMouseMotion, grab_threshold: float) -> int:
	var xform: Transform2D = get_et() * shape.get_global_transform()
	var pa := shape.get_point_array()
	for k in pa.get_all_point_keys():
		var pp: Vector2 = pa.get_point_position(k)
		var normal: Vector2 = _get_vert_normal(
			xform, pa.get_vertices(), pa.get_point_index(k)
		)
		var p: Vector2 = xform * pp + normal * WIDTH_HANDLE_OFFSET
		if p.distance_to(mm.position) <= grab_threshold:
			return k
	return -1


func _input_motion_move_control_points(delta: Vector2, _in: bool, _out: bool) -> bool:
	var rslt := false
	var pa := shape.get_point_array()
	for i in range(0, current_action.keys.size(), 1):
		var key: int = current_action.keys[i]
		var out_multiplier := 1
		# Invert the delta for position_out if moving both at once
		if _out and _in:
			out_multiplier = -1

		var from_in: Vector2 = shape.to_global(current_action.starting_positions_control_in[i])
		var new_position_in: Vector2 = shape.global_transform.affine_inverse() * (
			delta + from_in
		)

		var from_out: Vector2 = shape.to_global(current_action.starting_positions_control_out[i])
		var new_position_out: Vector2 = shape.global_transform.affine_inverse() * (
			(delta * out_multiplier)
			+ from_out
		)

		if use_snap():
			new_position_in = snap(new_position_in)
			new_position_out = snap(new_position_out)
		if _in:
			pa.set_point_in(key, new_position_in)
			rslt = true
		if _out:
			pa.set_point_out(key, new_position_out)
			rslt = true
		update_overlays()

	return rslt


func _input_motion_move_verts(delta: Vector2) -> bool:
	for i in range(0, current_action.keys.size(), 1):
		var key: int = current_action.keys[i]
		var from: Vector2 = shape.to_global(current_action.starting_positions[i])
		var new_position: Vector2 = shape.global_transform.affine_inverse() * (from + delta)
		if use_snap():
			new_position = snap(new_position)
		shape.get_point_array().set_point_position(key, new_position)
		update_overlays()
	return true


func _input_motion_move_width_handle(mouse_position: Vector2, scale: Vector2) -> bool:
	for i in range(0, current_action.keys.size(), 1):
		var key: int = current_action.keys[i]
		var from_width: float = current_action.starting_width[i]
		var from_position: Vector2 = current_action.starting_positions[i]
		width_scaling = from_position.distance_to(mouse_position) / WIDTH_HANDLE_OFFSET * scale.x
		shape.get_point_array().get_point(key).width = roundf(from_width * width_scaling * 10.0) / 10.0
		update_overlays()
	return true


## Will return index of closest vert to point.
func get_closest_vert_to_point(pa: SS2D_Point_Array, p: Vector2) -> int:
	var gt: Transform2D = shape.get_global_transform()
	var verts: PackedVector2Array = pa.get_vertices()
	var transformed_point: Vector2 = gt.affine_inverse() * p
	var idx: int = -1
	var closest_distance: float = -1.0
	for i in verts.size():
		var distance: float = verts[i].distance_to(transformed_point)
		if distance < closest_distance or closest_distance == -1.0:
			idx = pa.get_point_key_at_index(i)
			closest_distance = distance
	return idx


func _input_handle_mouse_motion_event(
	event: InputEventMouseMotion, et: Transform2D, grab_threshold: float
) -> bool:
	var pa := shape.get_point_array()
	var t: Transform2D = et * shape.get_global_transform()
	var mm: InputEventMouseMotion = event
	var delta_current_pos: Vector2 = et.affine_inverse() * mm.position
	gui_point_info_panel.position = mm.position + GUI_POINT_INFO_PANEL_OFFSET
	var delta: Vector2 = delta_current_pos - _mouse_motion_delta_starting_pos

	closest_key = get_closest_vert_to_point(pa, delta_current_pos)

	if current_mode == MODE.EDIT_VERT or current_mode == MODE.CREATE_VERT:
		var type: ACTION_VERT = current_action.type
		var _in := type == ACTION_VERT.MOVE_CONTROL or type == ACTION_VERT.MOVE_CONTROL_IN
		var _out := type == ACTION_VERT.MOVE_CONTROL or type == ACTION_VERT.MOVE_CONTROL_OUT

		if type == ACTION_VERT.MOVE_VERT:
			return _input_motion_move_verts(delta)
		elif _in or _out:
			return _input_motion_move_control_points(delta, _in, _out)
		elif type == ACTION_VERT.MOVE_WIDTH_HANDLE:
			return _input_motion_move_width_handle(
				et.affine_inverse() * mm.position, et.get_scale()
			)
		var mouse_over_key: int = get_mouse_over_vert_key(event, grab_threshold)
		var mouse_over_width_handle: int = get_mouse_over_width_handle(event, grab_threshold)

		# Make the closest key grabable while holding down Control
		if (
			Input.is_key_pressed(KEY_CTRL)
			and not Input.is_key_pressed(KEY_ALT)
			and mouse_over_width_handle == -1
			and mouse_over_key == -1
		):
			mouse_over_key = closest_key

		on_width_handle = false
		if mouse_over_key != -1:
			on_edge = false
			current_action = select_verticies([mouse_over_key], ACTION_VERT.NONE)
		elif mouse_over_width_handle != -1:
			on_edge = false
			on_width_handle = true
			current_action = select_verticies([mouse_over_width_handle], ACTION_VERT.NONE)
		elif Input.is_key_pressed(KEY_ALT):
			_input_find_closest_edge_keys(mm)
		else:
			deselect_verts()
			on_edge = _input_motion_is_on_edge(mm, grab_threshold)

	elif current_mode == MODE.EDIT_EDGE or current_mode == MODE.CUT_EDGE:
		# Don't update if edge panel is visible
		if gui_edge_info_panel.visible:
			return false
		var type: ACTION_VERT = current_action.type
		if type == ACTION_VERT.MOVE_VERT:
			return _input_motion_move_verts(delta)
		else:
			deselect_verts()
		on_edge = _input_motion_is_on_edge(mm, grab_threshold)

	elif current_mode == MODE.FREEHAND:
		if _mouse_lmb_pressed:
			if not Input.is_key_pressed(KEY_CTRL):
				var local_position: Vector2 = t.affine_inverse() * mm.position
				if last_point_position.distance_to(local_position) >= freehand_paint_size * 2:
					last_point_position = local_position
					if use_snap():
						local_position = snap(local_position)
					var idx: int = pa.get_point_index(closest_edge_keys[1]) if pa.is_shape_closed() else -1
					perform_action(SS2D_ActionAddPoint.new(shape, local_position, idx, not _defer_mesh_updates))
				update_overlays()
				return true
			else:
				var xform: Transform2D = get_et() * shape.get_global_transform()
				var closest_ss2d_point: SS2D_Point = pa.get_point(closest_key)
				if closest_ss2d_point != null:
					var closest_point: Vector2 = closest_ss2d_point.position
					closest_point = xform * closest_point
					if closest_point.distance_to(mm.position) / current_zoom_level <= freehand_erase_size * 2:
						var delete_point: int = get_mouse_over_vert_key(event, grab_threshold)
						delete_point = closest_key
						on_width_handle = false
						if delete_point != -1:
							perform_action(SS2D_ActionDeletePoint.new(shape, delete_point, not _defer_mesh_updates))
							last_point_position = Vector2.ZERO
							update_overlays()
							return true
		else:
			_input_find_closest_edge_keys(mm)

	update_overlays()
	return false


func _get_vert_normal(t: Transform2D, verts: PackedVector2Array, i: int) -> Vector2:
	var point: Vector2 = t * verts[i]
	var prev_point: Vector2 = t * (verts[(i - 1) % verts.size()])
	var next_point: Vector2 = t * (verts[(i + 1) % verts.size()])
	return ((prev_point - point).normalized().rotated(PI / 2) + (point - next_point).normalized().rotated(PI / 2)).normalized()


func copy_shape(s: SS2D_Shape) -> SS2D_Shape:
	var copy: SS2D_Shape = s.clone(false)

	var undo := get_undo_redo()
	undo.create_action("Add Shape Node")

	undo.add_do_method(s.get_parent(), "add_child", copy, true)
	undo.add_do_method(copy, "set_owner", get_tree().get_edited_scene_root())
	undo.add_do_reference(copy)
	undo.add_undo_method(copy, "set_owner", null)
	undo.add_undo_method(s.get_parent(), "remove_child", copy)

	var collision_polygon_original := s.get_collision_polygon_node()

	if collision_polygon_original:
		var collision_polygon_new := CollisionPolygon2D.new()
		collision_polygon_new.visible = collision_polygon_original.visible

		undo.add_do_method(collision_polygon_original.get_parent(), "add_child", collision_polygon_new, true)
		undo.add_do_method(collision_polygon_new, "set_owner", get_tree().get_edited_scene_root())
		undo.add_do_reference(collision_polygon_new)

		undo.add_undo_method(collision_polygon_original.get_parent(), "remove_child", collision_polygon_new)

		undo.commit_action()

		copy.collision_polygon_node_path = copy.get_path_to(collision_polygon_new)
	else:
		undo.commit_action()

	return copy


#########
# DEBUG #
#########
func _debug_mouse_positions(mm: InputEventMouseMotion, t: Transform2D) -> void:
	print("========================================")
	print("MouseDelta:%s" % str(_mouse_motion_delta_starting_pos))
	print("= MousePositions =")
	print("Position:  %s" % str(mm.position))
	print("Relative:  %s" % str(mm.relative))
	print("= Transforms =")
	print("Transform3D: %s" % str(t))
	print("Inverse:   %s" % str(t.affine_inverse()))
	print("= Transformed Mouse positions =")
	print("Position:  %s" % str(t.affine_inverse() * mm.position))
	print("Relative:  %s" % str(t.affine_inverse() * mm.relative))
	print("MouseDelta:%s" % str(t.affine_inverse() * _mouse_motion_delta_starting_pos))


####################
# Version Checking #
####################
func perform_version_check_and_conversion(force: bool = false, show_dialog_when_no_conversion_needed: bool = false) -> void:
	# Only perform check if version changed or after initial install to reduce project startup time.
	if not force:
		var last_version: String = ProjectSettings.get_setting(PROJECT_SETTING_INTERNAL_VERSION, "")

		# This should suffice for most cases.
		# TODO: Consider more sophisticated version checking, e.g. opening a newer project with an older version.
		if last_version == get_plugin_version():
			return

		print("SS2D: Version change detected: ", last_version, " != ", get_plugin_version())

	print("SS2D: Performing version check...")

	var converters: Array[SS2D_VersionTransition.IVersionConverter] = [
		# SS2D_VersionTransition.ShapeNodeTypeConverter.new("Node2D", "MeshInstance2D"),
		# Insert more converters for future changes here
	]

	for i in converters:
		i.init()

	for i in converters:
		if not i.needs_conversion():
			continue

		print("SS2D: Version conversion required")
		var dialog: ConfirmationDialog = ConfirmationDialog.new()
		add_child(dialog)
		var original_ok_button_text := dialog.ok_button_text
		dialog.ok_button_text += " (3s)"
		dialog.dialog_close_on_escape = false
		dialog.title = "SmartShape2D - Version Check"
		dialog.dialog_text = """
		SmartShape2D detected scenes that were saved with an older version of SmartShape2D.
		These scenes need to be converted to work as before.

		We highly recommend making a backup of your project before proceeding.

		Do you want to proceed?""".dedent().strip_edges()
		dialog.get_ok_button().disabled = true
		dialog.confirmed.connect(_on_dialog_confirm_conversion.bind(converters))
		dialog.confirmed.connect(_free_dialog.bind(dialog))
		dialog.get_cancel_button().pressed.connect(_free_dialog.bind(dialog))
		dialog.popup_centered()

		await get_tree().create_timer(3).timeout

		# Dialog could be closed already
		if is_instance_valid(dialog):
			dialog.get_ok_button().disabled = false
			dialog.ok_button_text = original_ok_button_text

		return

	# No conversion needed
	print("SS2D: No conversion needed")
	_write_version_info_to_project()

	if show_dialog_when_no_conversion_needed:
		var dialog := AcceptDialog.new()
		add_child(dialog)
		dialog.title = "Info"
		dialog.dialog_text = "No conversion needed."
		dialog.confirmed.connect(_free_dialog.bind(dialog))
		dialog.popup_centered()


func _on_dialog_confirm_conversion(converters: Array[SS2D_VersionTransition.IVersionConverter]) -> void:
	var success := true

	for i in converters.size():
		if not converters[i].needs_conversion():
			continue

		if converters[i].convert():
			print("SS2D: Conversion step ", i + 1, " successful")
		else:
			push_error("SS2D: Conversion step ", i + 1, " failed -> Aborting")
			success = false
			break

	var summary := AcceptDialog.new()
	add_child(summary)

	if success:
		summary.title = "Conversion Successful"
		summary.dialog_text = "Conversion successful!"
		_write_version_info_to_project()
	else:
		summary.title = "Conversion Failed"
		summary.dialog_text = "An unexpected error occurred.\nSee log output for further information."

	summary.confirmed.connect(_free_dialog.bind(summary))
	summary.popup_centered()


func _free_dialog(dialog: Node) -> void:
	dialog.queue_free()


func _write_version_info_to_project() -> void:
	ProjectSettings.set_setting(PROJECT_SETTING_INTERNAL_VERSION, get_plugin_version())
	ProjectSettings.set_as_internal(PROJECT_SETTING_INTERNAL_VERSION, true)
	ProjectSettings.save()
