/*
 * opsu! - an open-source osu! client
 * Copyright (C) 2014, 2015 Jeffrey Han
 *
 * opsu! is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * opsu! is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with opsu!.  If not, see <http://www.gnu.org/licenses/>.
 */

package itdelatrisu.opsu.ui;

import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;

import org.lwjgl.input.Keyboard;
import org.newdawn.slick.Color;
import org.newdawn.slick.Font;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.UnicodeFont;
import yugecin.opsudance.core.components.Component;
import yugecin.opsudance.core.input.*;

import static itdelatrisu.opsu.GameImage.*;
import static yugecin.opsudance.core.InstanceContainer.*;

public class DropdownMenu<E> extends Component
{
	private static final float PADDING_Y = 0.1f, CHEVRON_X = 0.03f;

	public final E[] items;
	private String[] itemNames;
	private int selectedItemIndex;
	private boolean expanded;

	private AnimatedValue expandProgress = new AnimatedValue(300, 0f, 1f, AnimationEquation.LINEAR);

	private int baseHeight;
	private float offsetY;

	private Color textColor = Color.white;
	private Color backgroundColor = Color.black;
	private Color highlightColor = Colors.BLUE_DIVIDER;
	private Color borderColor = Colors.BLUE_DIVIDER;
	private Color chevronDownColor = textColor;
	private Color chevronRightColor = backgroundColor;

	private UnicodeFont fontNormal = Fonts.MEDIUM;
	private UnicodeFont fontSelected = Fonts.MEDIUMBOLD;

	private Image chevronDown;
	private Image chevronRight;

	public DropdownMenu(E[] items, int x, int y, int width)
	{
		this.items = items;
		init(x, y, width);
	}

	public void setWidth(int width) {
		this.width = width;
	}

	public int getHeight() {
		if (expanded) {
			return height;
		}
		return baseHeight;
	}

	public boolean isOpen() {
		return expanded;
	}

	/**
	 * Selects the item at the given index.
	 * @param index the list item index
	 * @throws IllegalArgumentException if {@code index} is negative or greater than or equal to size
	 */
	public void setSelectedIndex(int index) {
		if (index < 0 || items.length <= index) {
			throw new IllegalArgumentException();
		}
		this.selectedItemIndex = index;
	}

	private int getMaxItemWidth() {
		int maxWidth = 0;
		for (String itemName : itemNames) {
			int w = fontSelected.getWidth(itemName);
			if (w > maxWidth) {
				maxWidth = w;
			}
		}
		return maxWidth;
	}

	private void init(int x, int y, int width) {
		this.itemNames = new String[items.length];
		for (int i = 0; i < itemNames.length; i++) {
			itemNames[i] = items[i].toString();
		}
		this.x = x;
		this.y = y;
		this.baseHeight = fontNormal.getLineHeight();
		this.baseHeight -= 2; // meh.. :/
		this.offsetY = baseHeight + baseHeight * PADDING_Y;
		this.height = (int) (offsetY * (items.length + 1));
		int downChevronSize = baseHeight * 4 / 5;
		this.chevronDown = CHEVRON_DOWN.getScaledImage(downChevronSize, downChevronSize);
		int rightChevronSize = baseHeight * 2 / 3;
		//noinspection SuspiciousNameCombination
		this.chevronRight = CHEVRON_RIGHT.getScaledImage(rightChevronSize, rightChevronSize);
		int maxItemWidth = getMaxItemWidth();
		int minWidth = maxItemWidth + chevronRight.getWidth() * 2;
		this.width = Math.max(width, minWidth);
	}

	@Override
	public void updateHover(int x, int y)
	{
		if (displayContainer.suppressHover) {
			this.hovered = false;
			return;
		}
		displayContainer.suppressHover = this.hovered =
			this.x <= x && x <= this.x + width && this.y <= y && y <= this.y +
			(expanded ? height : baseHeight);
	}

	public boolean baseContains(int x, int y)
	{
		return (x > this.x && x < this.x + width && y > this.y && y < this.y + baseHeight);
	}

	public boolean isClosing()
	{
		return !expanded && expandProgress.getValue() >= 0.0001f;
	}

	@Override
	public void render(Graphics g) {
		int delta = renderDelta;

		// update animation
		expandProgress.update((expanded) ? delta : -delta * 2);

		// get parameters
		int idx = getIndexAt(mouseY);
		float t = expandProgress.getValue();
		if (expanded) {
			t = AnimationEquation.OUT_CUBIC.calc(t);
		}

		// background and border
		Color oldGColor = g.getColor();
		float oldLineWidth = g.getLineWidth();
		final int cornerRadius = 6;
		g.setLineWidth(1f);
		g.setColor((idx == -1) ? highlightColor : backgroundColor);
		g.fillRoundRect(x, y, width, baseHeight, cornerRadius);
		g.setColor(borderColor);
		g.drawRoundRect(x, y, width, baseHeight, cornerRadius);
		if (expanded || t >= 0.0001) {
			float oldBackgroundAlpha = backgroundColor.a;
			backgroundColor.a *= t;
			g.setColor(backgroundColor);
			g.fillRoundRect(x, y + offsetY, width, (height - offsetY) * t, cornerRadius);
			backgroundColor.a = oldBackgroundAlpha;
		}
		if (idx >= 0 && t >= 0.9999) {
			g.setColor(highlightColor);
			float yPos = y + offsetY + (offsetY * idx);
			int yOff = 0, hOff = 0;
			if (idx == 0 || idx == items.length - 1) {
				g.fillRoundRect(x, yPos, width, offsetY, cornerRadius);
				if (idx == 0)
					yOff = cornerRadius;
				hOff = cornerRadius;
			}
			g.fillRect(x, yPos + yOff, width, offsetY - hOff);
		}
		g.setColor(oldGColor);
		g.setLineWidth(oldLineWidth);

		// text
		chevronDown.draw(x + width - chevronDown.getWidth() - width * CHEVRON_X, y + (baseHeight - chevronDown.getHeight()) / 2f, chevronDownColor);
		fontNormal.drawString(x + (width * 0.03f), y + (fontNormal.getPaddingTop() + fontNormal.getPaddingBottom()) / 2f, itemNames[selectedItemIndex], textColor);
		float oldTextAlpha = textColor.a;
		textColor.a *= t;
		if (expanded || t >= 0.0001) {
			for (int i = 0; i < itemNames.length; i++) {
				Font f = (i == selectedItemIndex) ? fontSelected : fontNormal;
				if (i == idx && t >= 0.999)
					chevronRight.draw(x, y + offsetY + (offsetY * i) + (offsetY - chevronRight.getHeight()) / 2f, chevronRightColor);
				f.drawString(x + chevronRight.getWidth(), y + offsetY + (offsetY * i * t), itemNames[i], textColor);
			}
		}
		textColor.a = oldTextAlpha;
	}

	/**
	 * Returns the index of the item at the given location, -1 for the base item,
	 * and -2 if there is no item at the location.
	 * @param y the y coordinate
	 */
	private int getIndexAt(int y) {
		if (!hovered) {
			return -2;
		}
		if (y <= this.y + baseHeight) {
			return -1;
		}
		if (!expanded) {
			return -2;
		}
		return (int) ((y - (this.y + offsetY)) / offsetY);
	}

	public void reset() {
		this.expanded = false;
		expandProgress.setTime(0);
	}

	/**
	 * only call when not in dispatched input event or when it's being consumed
	 */
	public void openGrabFocus()
	{
		this.setFocused(true);
		input.addListener(this);
	}

	/**
	 * only call when not in dispatched input event or when it's being consumed
	 */
	public void closeReleaseFocus()
	{
		if (this.focused) {
			this.setFocused(false);
			input.removeListener(this);
		}
	}

	@Override
	public void setFocused(boolean focused) {
		super.setFocused(focused);
		expanded = focused;
	}

	@Override
	public boolean isFocusable() {
		return true;
	}

	@Override
	public void mouseReleased(MouseEvent e)
	{
		if (e.button == Input.MMB || e.dragDistance > 5f) {
			return;
		}

		int idx = getIndexAt(mouseY);
		if (idx < 0) {
			// -1: base clicked
			// -2: somewhere else clicked
			if (idx == -1) {
				e.consume();
			}
			this.closeReleaseFocus();
			return;
		}
		e.consume();
		if (!canSelect(selectedItemIndex)) {
			return;
		}
		this.expanded = (idx == -1) && !expanded;
		if (0 <= idx && idx < items.length && selectedItemIndex != idx) {
			this.selectedItemIndex = idx;
			itemSelected(idx, items[selectedItemIndex]);
			if (!this.expanded) {
				e.consume();
			}
		}
		this.closeReleaseFocus();
	}

	@Override
	public void mouseWheelMoved(MouseWheelEvent e)
	{
	}

	@Override
	public void mousePressed(MouseEvent e)
	{
		if (e.button == Input.MMB) {
			return;
		}

		final int idx = this.getIndexAt(mouseY);
		if (idx >= -1) {
			e.consume();
		}
	}

	@Override
	public void mouseDragged(MouseDragEvent e)
	{
	}

	@Override
	public void keyPressed(KeyEvent e)
	{
		if (e.keyCode == Keyboard.KEY_ESCAPE) {
			this.closeReleaseFocus();
			e.consume();
		}
	}

	@Override
	public void keyReleased(KeyEvent e)
	{
	}

	protected boolean canSelect(int index) {
		return true;
	}

	protected void itemSelected(int index, E item) {
	}

	public E getSelectedItem() {
		return items[selectedItemIndex];
	}

	public void setBackgroundColor(Color c) {
		this.backgroundColor = c;
	}

	public void setHighlightColor(Color c) {
		this.highlightColor = c;
	}

	public void setBorderColor(Color c) {
		this.borderColor = c;
	}

	public void setChevronDownColor(Color c) {
		this.chevronDownColor = c;
	}

	public void setChevronRightColor(Color c) {
		this.chevronRightColor = c;
	}

	public void setTextColor(Color c) {
		this.textColor = c;
	}
}