package ui;

import flixel.math.FlxPoint;
import flixel.FlxG;
import flixel.FlxSprite;
import flixel.effects.FlxFlicker;
import flixel.group.FlxGroup;
import flixel.util.FlxSignal;

class MenuTypedList<T:MenuItem> extends FlxTypedGroup<T>
{
	public var selectedIndex(default, null) = 0;
	public var selectedItem(get, never):T;
	/** Called when a new item is highlighted */
	public var onChange(default, null) = new FlxTypedSignal<T->Void>();
	/** Called when an item is accepted */
	public var onAcceptPress(default, null) = new FlxTypedSignal<T->Void>();
	/** The navigation control scheme to use */
	public var navControls:NavControls;
	/** Set to false to disable nav control */
	public var enabled:Bool = true;
	/**  */
	public var wrapMode:WrapMode = Both;
	
	var byName = new Map<String, T>();
	/** Set to true, internally to disable controls, without affecting vars like `enabled` */
	public var busy(default, null):Bool = false;
	// bit awkward because BACK is also a menu control and this doesn't affect that
	
	public function new (navControls:NavControls = Vertical, ?wrapMode:WrapMode)
	{
		this.navControls = navControls;
		
		if (wrapMode != null)
			this.wrapMode = wrapMode;
		else
			this.wrapMode = switch (navControls)
			{
				case Horizontal: Horizontal;
				case Vertical: Vertical;
				default: Both;
			}
		super();
	}
	
	public function addItem(name:String, item:T):T
	{
		if (length == selectedIndex)
			item.select();
		
		byName[name] = item;
		return add(item);
	}
	
	public function resetItem(oldName:String, newName:String, ?callback:Void->Void):T
	{
		if (!byName.exists(oldName))
			throw "No item named:" + oldName;
		
		var item = byName[oldName];
		byName.remove(oldName);
		byName[newName] = item;
		item.setItem(newName, callback);
		
		return item;
	}
	
	override function update(elapsed:Float)
	{
		super.update(elapsed);
		
		if (enabled && !busy)
			updateControls();
	}
	
	inline function updateControls()
	{
		var controls = PlayerSettings.player1.controls;
		
		var wrapX = wrapMode.match(Horizontal | Both);
		var wrapY = wrapMode.match(Vertical | Both);
		var newIndex = switch(navControls)
		{
			case Vertical    : navList(controls.UI_UP_P  , controls.UI_DOWN_P, wrapY);
			case Horizontal  : navList(controls.UI_LEFT_P, controls.UI_RIGHT_P, wrapX);
			case Both        : navList(controls.UI_LEFT_P || controls.UI_UP_P, controls.UI_RIGHT_P || controls.UI_DOWN_P, !wrapMode.match(None));
			
			case Columns(num): navGrid(num, controls.UI_LEFT_P, controls.UI_RIGHT_P, wrapX, controls.UI_UP_P  , controls.UI_DOWN_P , wrapY);
			case Rows   (num): navGrid(num, controls.UI_UP_P  , controls.UI_DOWN_P , wrapY, controls.UI_LEFT_P, controls.UI_RIGHT_P, wrapX);
		}
		
		if (newIndex != selectedIndex)
		{
			FlxG.sound.play(Paths.sound('scrollMenu'));
			selectItem(newIndex);
		}
		
		//Todo: bypass popup blocker on firefox
		if (controls.ACCEPT)
			accept();
	}
	
	function navAxis(index:Int, size:Int, prev:Bool, next:Bool, allowWrap:Bool):Int
	{
		if (prev == next)
			return index;
		
		if (prev)
		{
			if (index > 0)
				index--;
			else if (allowWrap)
				index = size - 1;
		}
		else
		{
			if (index < size - 1)
				index++;
			else if (allowWrap)
				index = 0;
		}
		
		return index;
	}
	
	/**
	 * Controls navigation on a linear list of items such as Vertical.
	 * @param prev 
	 * @param next 
	 * @param allowWrap 
	 */
	inline function navList(prev:Bool, next:Bool, allowWrap:Bool)
	{
		return navAxis(selectedIndex, length, prev, next, allowWrap);
	}
	
	/**
	 * Controls navigation on a grid
	 * @param latSize   The size of the fixed axis of the grid, or the "lateral axis"
	 * @param latPrev   Whether the 'prev' key is pressed along the fixed-lengthed axis. eg: "left" in Column mode
	 * @param latNext   Whether the 'next' key is pressed along the fixed-lengthed axis. eg: "right" in Column mode
	 * @param prev      Whether the 'prev' key is pressed along the variable-lengthed axis. eg: "up" in Column mode
	 * @param next      Whether the 'next' key is pressed along the variable-lengthed axis. eg: "down" in Column mode
	 * @param allowWrap unused
	 */
	function navGrid(latSize:Int, latPrev:Bool, latNext:Bool, latAllowWrap:Bool, prev:Bool, next:Bool, allowWrap:Bool):Int
	{
		// The grid lenth along the variable-length axis
		var size = Math.ceil(length / latSize);
		// The selected position along the variable-length axis
		var index = Math.floor(selectedIndex / latSize);
		// The selected position along the fixed axis
		var latIndex = selectedIndex % latSize;
		
		latIndex = navAxis(latIndex, latSize, latPrev, latNext, latAllowWrap);
		index = navAxis(index, size, prev, next, allowWrap);
		
		return Std.int(Math.min(length - 1, index * latSize + latIndex));
	}
	
	public function accept()
	{
		var selected = members[selectedIndex];
		onAcceptPress.dispatch(selected);
		
		if (selected.fireInstantly)
			selected.callback();
		else
		{
			busy = true;
			FlxG.sound.play(Paths.sound('confirmMenu'));
			FlxFlicker.flicker(selected, 1, 0.06, true, false, function(_)
			{
				busy = false;
				selected.callback();
			});
		}
	}
	
	public function selectItem(index:Int)
	{
		members[selectedIndex].idle();
		
		selectedIndex = index;
		
		var selected = members[selectedIndex];
		selected.select();
		onChange.dispatch(selected);
	}
	
	public function has(name:String)
	{
		return byName.exists(name);
	}
	
	public function getItem(name:String)
	{
		return byName[name];
	}
	
	override function destroy()
	{
		super.destroy();
		byName.clear();
		onChange.removeAll();
		onAcceptPress.removeAll();
	}
	
	inline function get_selectedItem():T
	{
		return members[selectedIndex];
	}
}

class MenuItem extends FlxSprite
{
	public var callback:Void->Void;
	public var name:String;
	/**
	 * Set to true for things like opening URLs otherwise, it may it get blocked.
	 */
	public var fireInstantly = false;
	public var selected(get, never):Bool;
	function get_selected() return alpha == 1.0;
	
	public function new (x = 0.0, y = 0.0, name:String, callback)
	{
		super(x, y);
		
		antialiasing = true;
		setData(name, callback);
		idle();
	}
	
	function setData(name:String, ?callback:Void->Void)
	{
		this.name = name;
		
		if (callback != null)
			this.callback = callback;
	}
	
	/**
	 * Calls setData and resets/redraws the state of the item
	 * @param name      the label.
	 * @param callback  Unchanged if null.
	 */
	public function setItem(name:String, ?callback:Void->Void)
	{
		setData(name, callback);
		
		if (selected)
			select();
		else
			idle();
	}
	
	public function idle()
	{
		alpha = 0.6;
	}
	
	public function select()
	{
		alpha = 1.0;
	}
}

class MenuTypedItem<T:FlxSprite> extends MenuItem
{
	public var label(default, set):T;
	
	public function new (x = 0.0, y = 0.0, label:T, name:String, callback)
	{
		super(x, y, name, callback);
		// set label after super otherwise setters fuck up
		this.label = label;
	}
	
	/**
	 * Use this when you only want to show the label
	 */
	function setEmptyBackground()
	{
		var oldWidth = width;
		var oldHeight = height;
		makeGraphic(1, 1, 0x0);
		width = oldWidth;
		height = oldHeight;
	}
	
	function set_label(value:T)
	{
		if (value != null)
		{
			value.x = x;
			value.y = y;
			value.alpha = alpha;
		}
		return this.label = value;
	}
	
	override function update(elapsed:Float)
	{
		super.update(elapsed);
		if (label != null)
			label.update(elapsed);
	}
	
	override function draw()
	{
		super.draw();
		if (label != null)
		{
			label.cameras = cameras;
			label.scrollFactor.copyFrom(scrollFactor);
			label.draw();
		}
	}

	override function set_alpha(value:Float):Float
	{
		super.set_alpha(value);
		
		if (label != null)
			label.alpha = alpha;
		
		return alpha;
	}

	override function set_x(value:Float):Float
	{
		super.set_x(value);
		
		if (label != null)
			label.x = x;
		
		return x;
	}

	override function set_y(Value:Float):Float
	{
		super.set_y(Value);
		
		if (label != null)
			label.y = y;
		
		return y;
	}
}

enum NavControls
{
	Horizontal;
	Vertical;
	Both;
	Columns(num:Int);
	Rows(num:Int);
}

enum WrapMode
{
	Horizontal;
	Vertical;
	Both;
	None;
}