/*
 * opsu! - an open-source osu! client
 * Copyright (C) 2014-2017 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.GameImage;
import itdelatrisu.opsu.Opsu;
import itdelatrisu.opsu.Utils;
import itdelatrisu.opsu.audio.SoundController;
import itdelatrisu.opsu.beatmap.BeatmapParser;
import itdelatrisu.opsu.beatmap.OszUnpacker;
import itdelatrisu.opsu.options.Options;
import itdelatrisu.opsu.replay.ReplayImporter;
import itdelatrisu.opsu.skins.SkinUnpacker;
import itdelatrisu.opsu.ui.animations.AnimatedValue;
import itdelatrisu.opsu.ui.animations.AnimationEquation;

import javax.swing.JOptionPane;
import javax.swing.UIManager;

import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Image;
import org.newdawn.slick.Input;
import org.newdawn.slick.state.StateBasedGame;
import org.newdawn.slick.util.Log;

/**
 * Draws common UI components.
 */
public class UI {
	/** The target frame rate when the window does not have focus. */
	private static final int IDLE_FPS = 30;

	/** Cursor. */
	private static Cursor cursor = new Cursor();

	/** Back button. */
	private static BackButton backButton;

	/** Time to show volume image, in milliseconds. */
	private static final int VOLUME_DISPLAY_TIME = 1500;

	/** Volume display elapsed time. */
	private static int volumeDisplay = -1;

	/** The current tooltip. */
	private static String tooltip;

	/** Whether or not to check the current tooltip for line breaks. */
	private static boolean tooltipNewlines;

	/** The alpha level of the current tooltip (if any). */
	private static AnimatedValue tooltipAlpha = new AnimatedValue(200, 0f, 1f, AnimationEquation.LINEAR);

	/** The displayed FPS. */
	private static float fpsDisplay = 0f;

	/** Notification manager. */
	private static NotificationManager notificationManager;

	// game-related variables
	private static GameContainer container;
	private static StateBasedGame game;
	private static Input input;

	// This class should not be instantiated.
	private UI() {}

	/**
	 * Initializes UI data.
	 * @param container the game container
	 * @param game the game object
	 */
	public static void init(GameContainer container, StateBasedGame game) {
		UI.container = container;
		UI.game = game;
		UI.input = container.getInput();

		// cursor
		Cursor.init(container, game);
		cursor.hide();

		// back button
		backButton = new BackButton(container);

		// notification manager
		notificationManager = new NotificationManager(container);
	}

	/**
	 * Updates all UI components by a delta interval.
	 * @param delta the delta interval since the last call.
	 */
	public static void update(int delta) {
		cursor.update(delta);
		updateVolumeDisplay(delta);
		notificationManager.update(delta);
		tooltipAlpha.update(-delta);
		updateFPS(delta);
	}

	/**
	 * Draws the global UI components: cursor, FPS, volume bar, tooltips, bar notifications.
	 * @param g the graphics context
	 */
	public static void draw(Graphics g) {
		notificationManager.draw(g);
		drawVolume(g);
		drawFPS();
		cursor.draw();
		drawTooltip(g);
	}

	/**
	 * Draws the global UI components: cursor, FPS, volume bar, tooltips, bar notifications.
	 * @param g the graphics context
	 * @param mouseX the mouse x coordinate
	 * @param mouseY the mouse y coordinate
	 * @param mousePressed whether or not the mouse button is pressed
	 */
	public static void draw(Graphics g, int mouseX, int mouseY, boolean mousePressed) {
		notificationManager.draw(g);
		drawVolume(g);
		drawFPS();
		cursor.draw(mouseX, mouseY, mousePressed);
		drawTooltip(g);
	}

	/**
	 * Resets the necessary UI components upon entering a state.
	 */
	public static void enter() {
		backButton.resetHover();
		cursor.resetLocations();
		notificationManager.reset();
		resetTooltip();
	}

	/**
	 * Returns the game cursor.
	 */
	public static Cursor getCursor() { return cursor; }

	/**
	 * Returns the notification manager instance.
	 */
	public static NotificationManager getNotificationManager() { return notificationManager; }

	/**
	 * Returns the back button instance.
	 */
	public static BackButton getBackButton() { return backButton; }

	/**
	 * Draws a tab image and text centered at a location.
	 * @param x the center x coordinate
	 * @param y the center y coordinate
	 * @param text the text to draw inside the tab
	 * @param selected whether the tab is selected (white) or not (red)
	 * @param isHover whether to include a hover effect (unselected only)
	 */
	public static void drawTab(float x, float y, String text, boolean selected, boolean isHover) {
		Image tabImage = GameImage.MENU_TAB.getImage();
		float tabTextX = x - (Fonts.MEDIUM.getWidth(text) / 2);
		float tabTextY = y - (tabImage.getHeight() / 2);
		Color filter, textColor;
		if (selected) {
			filter = Color.white;
			textColor = Color.black;
		} else {
			filter = (isHover) ? Colors.RED_HOVER : Color.red;
			textColor = Color.white;
		}
		tabImage.drawCentered(x, y, filter);
		Fonts.MEDIUM.drawString(tabTextX, tabTextY, text, textColor);
	}

	/**
	 * Draws the FPS at the bottom-right corner of the game container.
	 * If the option is not activated, this will do nothing.
	 */
	public static void drawFPS() {
		if (!Options.isFPSCounterEnabled())
			return;

		int fps = Math.round(fpsDisplay);
		String s = String.format("%dFPS", fps);
		Fonts.BOLD.drawString(
			container.getWidth() * 0.997f - Fonts.BOLD.getWidth(s),
			container.getHeight() * 0.997f - Fonts.BOLD.getHeight(s),
			Integer.toString(fps), Color.white
		);
		Fonts.DEFAULT.drawString(
			container.getWidth() * 0.997f - Fonts.BOLD.getWidth("FPS"),
			container.getHeight() * 0.997f - Fonts.BOLD.getHeight("FPS"),
			"FPS", Color.white
		);
	}

	/**
	 * Draws the volume bar on the middle right-hand side of the game container.
	 * Only draws if the volume has recently been changed using with {@link #changeVolume(int)}.
	 * @param g the graphics context
	 */
	public static void drawVolume(Graphics g) {
		if (volumeDisplay == -1)
			return;

		int width = container.getWidth(), height = container.getHeight();
		Image img = GameImage.VOLUME.getImage();

		// move image in/out
		float xOffset = 0;
		float ratio = (float) volumeDisplay / VOLUME_DISPLAY_TIME;
		if (ratio <= 0.1f)
			xOffset = img.getWidth() * (1 - (ratio * 10f));
		else if (ratio >= 0.9f)
			xOffset = img.getWidth() * (1 - ((1 - ratio) * 10f));

		img.drawCentered(width - img.getWidth() / 2f + xOffset, height / 2f);
		float barHeight = img.getHeight() * 0.9f;
		float volume = Options.getMasterVolume();
		g.setColor(Color.white);
		g.fillRoundRect(
				width - (img.getWidth() * 0.368f) + xOffset,
				(height / 2f) - (img.getHeight() * 0.47f) + (barHeight * (1 - volume)),
				img.getWidth() * 0.15f, barHeight * volume, 3
		);
	}

	/**
	 * Updates volume display by a delta interval.
	 * @param delta the delta interval since the last call
	 */
	private static void updateVolumeDisplay(int delta) {
		if (volumeDisplay == -1)
			return;

		volumeDisplay += delta;
		if (volumeDisplay > VOLUME_DISPLAY_TIME)
			volumeDisplay = -1;
	}

	/**
	 * Changes the master volume by a unit (positive or negative).
	 * @param units the number of units
	 */
	public static void changeVolume(int units) {
		final float UNIT_OFFSET = 0.05f;
		Options.setMasterVolume(container, Utils.clamp(Options.getMasterVolume() + (UNIT_OFFSET * units), 0f, 1f));
		if (volumeDisplay == -1)
			volumeDisplay = 0;
		else if (volumeDisplay >= VOLUME_DISPLAY_TIME / 10)
			volumeDisplay = VOLUME_DISPLAY_TIME / 10;
	}

	/**
	 * Draws loading progress (OSZ unpacking, beatmap parsing, replay importing, sound loading)
	 * at the bottom of the screen.
	 * @param g the graphics context
	 * @param alpha the text alpha level
	 */
	public static void drawLoadingProgress(Graphics g, float alpha) {
		String text, file;
		int progress;

		// determine current action
		if ((file = OszUnpacker.getCurrentFileName()) != null) {
			text = "Unpacking new beatmaps...";
			progress = OszUnpacker.getUnpackerProgress();
		} else if ((file = BeatmapParser.getCurrentFileName()) != null) {
			text = (BeatmapParser.getStatus() == BeatmapParser.Status.INSERTING) ?
					"Updating database..." : "Loading beatmaps...";
			progress = BeatmapParser.getParserProgress();
		} else if ((file = SkinUnpacker.getCurrentFileName()) != null) {
			text = "Unpacking new skins...";
			progress = SkinUnpacker.getUnpackerProgress();
		} else if ((file = ReplayImporter.getCurrentFileName()) != null) {
			text = "Importing replays...";
			progress = ReplayImporter.getLoadingProgress();
		} else if ((file = SoundController.getCurrentFileName()) != null) {
			text = "Loading sounds...";
			progress = SoundController.getLoadingProgress();
		} else
			return;

		// draw loading info
		float marginX = container.getWidth() * 0.02f, marginY = container.getHeight() * 0.02f;
		float lineY = container.getHeight() - marginY;
		int lineOffsetY = Fonts.MEDIUM.getLineHeight();
		float oldWhiteAlpha = Colors.WHITE_FADE.a;
		Colors.WHITE_FADE.a = alpha;
		if (Options.isLoadVerbose()) {
			// verbose: display percentages and file names
			Fonts.MEDIUM.drawString(
				marginX, lineY - (lineOffsetY * 2),
				String.format("%s (%d%%)", text, progress), Colors.WHITE_FADE
			);
			Fonts.MEDIUM.drawString(marginX, lineY - lineOffsetY, file, Colors.WHITE_FADE);
		} else {
			// draw loading bar
			Fonts.MEDIUM.drawString(marginX, lineY - (lineOffsetY * 2), text, Colors.WHITE_FADE);
			g.setColor(Colors.WHITE_FADE);
			g.fillRoundRect(
				marginX, lineY - (lineOffsetY / 2f),
				(container.getWidth() - (marginX * 2f)) * progress / 100f, lineOffsetY / 4f, 4
			);
		}
		Colors.WHITE_FADE.a = oldWhiteAlpha;
	}

	/**
	 * Draws a scroll bar.
	 * @param g the graphics context
	 * @param position the position in the virtual area
	 * @param totalLength the total length of the virtual area
	 * @param lengthShown the length of the virtual area shown
	 * @param unitBaseX the base x coordinate
	 * @param unitBaseY the base y coordinate
	 * @param unitWidth the width of a unit
	 * @param scrollAreaHeight the height of the scroll area
	 * @param bgColor the scroll bar area background color (null if none)
	 * @param scrollbarColor the scroll bar color
	 * @param right whether or not to place the scroll bar on the right side of the unit
	 */
	public static void drawScrollbar(
			Graphics g, float position, float totalLength, float lengthShown,
			float unitBaseX, float unitBaseY, float unitWidth, float scrollAreaHeight,
			Color bgColor, Color scrollbarColor, boolean right
	) {
		float scrollbarWidth = container.getWidth() * 0.00347f;
		float scrollbarHeight = scrollAreaHeight * lengthShown / totalLength;
		float offsetY = (scrollAreaHeight - scrollbarHeight) * (position / (totalLength - lengthShown));
		float scrollbarX = unitBaseX + unitWidth - ((right) ? scrollbarWidth : 0);
		if (bgColor != null) {
			g.setColor(bgColor);
			g.fillRect(scrollbarX, unitBaseY, scrollbarWidth, scrollAreaHeight);
		}
		g.setColor(scrollbarColor);
		g.fillRect(scrollbarX, unitBaseY + offsetY, scrollbarWidth, scrollbarHeight);
	}

	/**
	 * Sets or updates a tooltip for drawing.
	 * Must be called with {@link #drawTooltip(Graphics)}.
	 * @param delta the delta interval since the last call
	 * @param s the tooltip text
	 * @param newlines whether to check for line breaks ('\n')
	 */
	public static void updateTooltip(int delta, String s, boolean newlines) {
		if (s != null) {
			tooltip = s;
			tooltipNewlines = newlines;
			tooltipAlpha.update(delta * 2);
		}
	}

	/**
	 * Draws a tooltip, if any, near the current mouse coordinates,
	 * bounded by the container dimensions.
	 * @param g the graphics context
	 */
	public static void drawTooltip(Graphics g) {
		if (tooltipAlpha.getTime() == 0 || tooltip == null || tooltip.isEmpty())
			return;

		int containerWidth = container.getWidth(), containerHeight = container.getHeight();
		int margin = containerWidth / 100, textMarginX = 2;
		int offset = GameImage.CURSOR_MIDDLE.getImage().getWidth() / 2;
		int lineHeight = Fonts.SMALL.getLineHeight();
		int textWidth = textMarginX * 2, textHeight = lineHeight;
		if (tooltipNewlines) {
			String[] lines = tooltip.split("\\n");
			int maxWidth = Fonts.SMALL.getWidth(lines[0]);
			for (int i = 1; i < lines.length; i++) {
				int w = Fonts.SMALL.getWidth(lines[i]);
				if (w > maxWidth)
					maxWidth = w;
			}
			textWidth += maxWidth;
			textHeight += lineHeight * (lines.length - 1);
		} else
			textWidth += Fonts.SMALL.getWidth(tooltip);

		// get drawing coordinates
		int x = input.getMouseX() + offset, y = input.getMouseY() + offset;
		if (x + textWidth > containerWidth - margin)
			x = containerWidth - margin - textWidth;
		else if (x < margin)
			x = margin;
		if (y + textHeight > containerHeight - margin)
			y = containerHeight - margin - textHeight;
		else if (y < margin)
			y = margin;

		// draw tooltip text inside a filled rectangle
		float alpha = tooltipAlpha.getValue();
		float oldAlpha = Colors.BLACK_ALPHA.a;
		Colors.BLACK_ALPHA.a = alpha;
		g.setColor(Colors.BLACK_ALPHA);
		Colors.BLACK_ALPHA.a = oldAlpha;
		g.fillRect(x, y, textWidth, textHeight);
		oldAlpha = Colors.DARK_GRAY.a;
		Colors.DARK_GRAY.a = alpha;
		g.setColor(Colors.DARK_GRAY);
		g.setLineWidth(1);
		g.drawRect(x, y, textWidth, textHeight);
		Colors.DARK_GRAY.a = oldAlpha;
		oldAlpha = Colors.WHITE_ALPHA.a;
		Colors.WHITE_ALPHA.a = alpha;
		Fonts.SMALL.drawString(x + textMarginX, y, tooltip, Colors.WHITE_ALPHA);
		Colors.WHITE_ALPHA.a = oldAlpha;
	}

	/**
	 * Resets the tooltip.
	 */
	public static void resetTooltip() {
		tooltipAlpha.setTime(0);
		tooltip = null;
	}

	/**
	 * Updates the FPS display by a delta interval.
	 * Also changes the frame rate if the window has lost or restored focus.
	 * @param delta the delta interval since the last call
	 */
	private static void updateFPS(int delta){
		// change frame rate when focus is lost/restored
		boolean focus = (game.getCurrentStateID() == Opsu.STATE_GAME) ? true : container.hasFocus();
		container.setTargetFrameRate(focus ? Options.getTargetFPS() : IDLE_FPS);

		// update displayed FPS
		if (Options.isFPSCounterEnabled()) {
			int fps = container.getFPS();
			float multiplier = delta / 250f;
			if (fpsDisplay < fps)
				fpsDisplay = Math.min(fpsDisplay + (fps - fpsDisplay) * multiplier, fps);
			else if (fpsDisplay > fps)
				fpsDisplay = Math.max(fpsDisplay - (fpsDisplay - fps) * multiplier, fps);
		}
	}

	/**
	 * Resets the displayed FPS.
	 */
	public static void resetFPSDisplay() { fpsDisplay = 0f; }

	/**
	 * Shows a confirmation dialog (used before exiting the game).
	 * @param message the message to display
	 * @return true if user selects "yes", false otherwise
	 */
	public static boolean showExitConfirmation(String message) {
		try {
			UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
		} catch (Exception e) {
			Log.warn("Could not set system look and feel for exit confirmation.", e);
		}
		int n = JOptionPane.showConfirmDialog(null, message, "Warning",
				JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE);
		return (n != JOptionPane.YES_OPTION);
	}

	/**
	 * Processes global hotkey actions.
	 * @param key the key code that was pressed
	 * @return {@code true} if a global hotkey was pressed
	 */
	public static boolean globalKeyPressed(int key) {
		switch (key) {
		case Input.KEY_F7:
			Options.setNextFPS(container);
			break;
		case Input.KEY_F10:
			Options.toggleMouseDisabled();
			break;
		case Input.KEY_F12:
			Utils.takeScreenShot();
			break;
		default:
			return false;
		}
		return true;
	}

	/**
	 * Processes global mouse wheel actions.
	 * @param delta the amount the wheel has moved
	 * @param requiresAlt if the "ALT" key must be pressed
	 * @return {@code true} if a global mouse wheel action was processed
	 */
	public static boolean globalMouseWheelMoved(int delta, boolean requiresAlt) {
		if (!requiresAlt || (input.isKeyDown(Input.KEY_LALT) || input.isKeyDown(Input.KEY_RALT))) {
			UI.changeVolume((delta < 0) ? -1 : 1);
			return true;
		}
		return false;
	}
}