/*
 * 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.ui.animations.AnimationEquation;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.Input;
import org.newdawn.slick.MouseListener;
import org.newdawn.slick.UnicodeFont;

/**
 * Notification manager.
 */
public class NotificationManager {
	/** Duration, in milliseconds, to display bubble notifications. */
	private static final int NOTIFICATION_TIME = 8000;

	/** Duration, in milliseconds, to animate bubble notifications in/out. */
	private static final int NOTIFICATION_ANIMATION_TIME = 450;

	/** Duration, in milliseconds, to display bar notifications. */
	private static final int BAR_NOTIFICATION_TIME = 1500;

	/** Listener for notification clicks. */
	public interface NotificationListener {
		/** Fired when this notification is clicked. */
		public void click();
	}

	/** Notification. */
	private class BubbleNotification {
		/** The listener. */
		private final NotificationListener listener;

		/** The notification string. */
		private final String message;

		/** The lines of text. */
		private final List<String> lines;

		/** The border color. */
		private final Color borderColor, borderFocusColor;

		/** The timer. */
		private int time = 0;

		/** The coordinates. */
		private int x, y;

		/** The dimensions. */
		private final int width, height;

		/** The font to use. */
		private final UnicodeFont font = Fonts.SMALLBOLD;

		/** Whether this notification has been clicked. */
		private boolean clicked = false;

		/**
		 * Creates a new bubble notification.
		 * @param s the notification string
		 * @param c the border color
		 * @param listener the listener
		 * @param width the element width
		 */
		public BubbleNotification(String s, Color c, NotificationListener listener, int width) {
			this.message = s;
			this.lines = Fonts.wrap(font, s, (int) (width * 0.96f), true);
			this.borderColor = new Color(c);
			this.borderFocusColor = (borderColor.equals(Color.white)) ?
				new Color(Colors.GREEN) : new Color(Color.white);
			this.listener = listener;
			this.width = width;
			this.height = (int) (font.getLineHeight() * (lines.size() + 0.5f));
		}

		/** Returns the x position. */
		public int getX() { return x; }

		/** Returns the y position. */
		public int getY() { return y; }

		/** Returns the width of this notification. */
		public int getWidth() { return width; }

		/** Returns the height of this notification. */
		public int getHeight() { return height; }

		/**
		 * Sets the position of this bubble.
		 * @param x the x coordinate
		 * @param y the y coordinate
		 */
		public void setPosition(int x, int y) {
			this.x = x;
			this.y = y;
		}

		/**
		 * Returns true if the coordinates are within the bubble bounds.
		 * @param cx the x coordinate
		 * @param cy the y coordinate
		 */
		public boolean contains(int cx, int cy) {
			return ((cx > x && cx < x + width) && (cy > y && cy < y + height));
		}

		/**
		 * Draws the bubble notification.
		 * @param g the graphics context
		 * @param focus true if focused
		 */
		public void draw(Graphics g, boolean focus) {
			// get animation values
			float alpha = 1f;
			int offset = 0;
			if (time < NOTIFICATION_ANIMATION_TIME) {
				float t = AnimationEquation.OUT_BACK.calc((float) time / NOTIFICATION_ANIMATION_TIME);
				alpha = t;
				offset = (int) ((1f - t) * (width / 2f));
			} else if (NOTIFICATION_TIME - time < NOTIFICATION_ANIMATION_TIME) {
				float t = (float) (NOTIFICATION_TIME - time) / NOTIFICATION_ANIMATION_TIME;
				alpha = t;
			}

			float oldBlackAlpha = Colors.BLACK_ALPHA.a;
			float oldWhiteAlpha = Colors.WHITE_FADE.a;
			Colors.BLACK_ALPHA.a = alpha;
			Colors.WHITE_FADE.a = alpha;
			Color border = focus ? borderFocusColor : borderColor;
			border.a = alpha;

			// draw rectangle
			int lineHeight = font.getLineHeight();
			int cornerRadius = 6;
			g.setColor(Colors.BLACK_ALPHA);
			g.fillRoundRect(x + offset, y, width, height, cornerRadius);
			g.setLineWidth(1f);
			g.setColor(border);
			g.drawRoundRect(x + offset, y, width, height, cornerRadius);

			// draw text
			Fonts.loadGlyphs(font, message);
			int cx = x + (int) (width * 0.02f) + offset, cy = y + lineHeight / 4;
			for (String s : lines) {
				font.drawString(cx, cy, s, Colors.WHITE_FADE);
				cy += lineHeight;
			}

			Colors.BLACK_ALPHA.a = oldBlackAlpha;
			Colors.WHITE_FADE.a = oldWhiteAlpha;
		}

		/**
		 * Updates the bubble notification by a delta interval.
		 * @param delta the delta interval since the last call.
		 * @return true if an update was applied, false if the timer is finished
		 */
		public boolean update(int delta) {
			if (isFinished())
				return false;

			time = Math.min(time + delta, NOTIFICATION_TIME);
			return true;
		}

		/** Returns whether this notification is finished being displayed. */
		public boolean isFinished() { return time >= NOTIFICATION_TIME; }

		/** Returns whether this notification has finished animating in. */
		public boolean isStartAnimationFinished() { return time >= NOTIFICATION_ANIMATION_TIME; }

		/**
		 * Click handler.
		 * @param doAction whether to fire the listener
		 */
		public synchronized void click(boolean doAction) {
			if (isFinished() || clicked)
				return;
			clicked = true;
			time = Math.max(time, NOTIFICATION_TIME - NOTIFICATION_ANIMATION_TIME);
			if (listener != null && doAction)
				listener.click();
		}
	}

	/** All bubble notifications. */
	private List<BubbleNotification> notifications;

	/** The current bar notification string. */
	private String barNotif;

	/** The current bar notification timer. */
	private int barNotifTimer = -1;

	// game-related variables
	private final GameContainer container;
	private final Input input;

	/**
	 * Constructor.
	 * @param container the game container
	 */
	public NotificationManager(GameContainer container) {
		this.container = container;
		this.input = container.getInput();
		this.notifications = Collections.synchronizedList(new ArrayList<BubbleNotification>());
		input.addMouseListener(new MouseListener() {
			@Override
			public void mousePressed(int button, int x, int y) {
				if (button == Input.MOUSE_MIDDLE_BUTTON)
					return;
				synchronized (notifications) {
					for (BubbleNotification n : notifications) {
						if (n.contains(x, y)) {
							n.click(button == Input.MOUSE_LEFT_BUTTON);
							break;
						}
					}
				}
			}
			@Override public void setInput(Input input) {}
			@Override public boolean isAcceptingInput() { return true; }
			@Override public void inputEnded() {}
			@Override public void inputStarted() {}
			@Override public void mouseWheelMoved(int change) {}
			@Override public void mouseClicked(int button, int x, int y, int clickCount) {}
			@Override public void mouseReleased(int button, int x, int y) {}
			@Override public void mouseMoved(int oldx, int oldy, int newx, int newy) {}
			@Override public void mouseDragged(int oldx, int oldy, int newx, int newy) {}
		});
	}

	/**
	 * Draws all notifications.
	 * @param g the graphics context
	 */
	public void draw(Graphics g) {
		drawNotifications(g);
		drawBarNotification(g);
	}

	/**
	 * Draws the notifications sent from {@link #sendNotification(String, Color)}.
	 * @param g the graphics context
	 */
	private void drawNotifications(Graphics g) {
		int mouseX = input.getMouseX(), mouseY = input.getMouseY();
		synchronized (notifications) {
			for (BubbleNotification n : notifications) {
				if (!n.isFinished())
					n.draw(g, n.contains(mouseX, mouseY));
			}
		}
	}

	/**
	 * Draws the notification sent from {@link #sendBarNotification(String)}.
	 * @param g the graphics context
	 */
	private void drawBarNotification(Graphics g) {
		if (barNotifTimer <= 0 || barNotifTimer >= BAR_NOTIFICATION_TIME)
			return;

		float alpha = 1f;
		if (barNotifTimer >= BAR_NOTIFICATION_TIME * 0.9f)
			alpha -= 1 - ((BAR_NOTIFICATION_TIME - barNotifTimer) / (BAR_NOTIFICATION_TIME * 0.1f));
		int midX = container.getWidth() / 2, midY = container.getHeight() / 2;
		float barHeight = Fonts.LARGE.getLineHeight() * (1f + 0.6f * Math.min(barNotifTimer * 15f / BAR_NOTIFICATION_TIME, 1f));
		float oldAlphaB = Colors.BLACK_ALPHA.a, oldAlphaW = Colors.WHITE_ALPHA.a;
		Colors.BLACK_ALPHA.a *= alpha;
		Colors.WHITE_ALPHA.a = alpha;
		g.setColor(Colors.BLACK_ALPHA);
		g.fillRect(0, midY - barHeight / 2f, container.getWidth(), barHeight);
		Fonts.LARGE.drawString(
			midX - Fonts.LARGE.getWidth(barNotif) / 2f,
			midY - Fonts.LARGE.getLineHeight() / 2f,
			barNotif, Colors.WHITE_ALPHA
		);
		Colors.BLACK_ALPHA.a = oldAlphaB;
		Colors.WHITE_ALPHA.a = oldAlphaW;
	}

	/**
	 * Updates all notifications by a delta interval.
	 * @param delta the delta interval since the last call.
	 */
	public void update(int delta) {
		// update notifications
		boolean allFinished = true, startFinished = true;
		synchronized (notifications) {
			for (BubbleNotification n : notifications) {
				if (startFinished) {
					if (n.update(delta))
						allFinished = false;
				} else if (!n.isFinished())
					allFinished = false;
				startFinished = n.isStartAnimationFinished();
			}
			if (allFinished)
				notifications.clear();  // clear when all are finished showing
		}

		// update bar notification
		if (barNotifTimer > -1 && barNotifTimer < BAR_NOTIFICATION_TIME) {
			barNotifTimer += delta;
			if (barNotifTimer > BAR_NOTIFICATION_TIME)
				barNotifTimer = BAR_NOTIFICATION_TIME;
		}
	}

	/**
	 * Submits a bubble notification for drawing.
	 * @param s the notification string
	 */
	public void sendNotification(String s) { sendNotification(s, Color.white); }

	/**
	 * Submits a bubble notification for drawing.
	 * @param s the notification string
	 * @param c the border color
	 */
	public void sendNotification(String s, Color c) { sendNotification(s, c, null); }

	/**
	 * Submits a bubble notification for drawing.
	 * @param s the notification string
	 * @param c the border color
	 * @param listener the listener
	 */
	public synchronized void sendNotification(String s, Color c, NotificationListener listener) {
		BubbleNotification notif = new BubbleNotification(s, c, listener, container.getWidth() / 5);
		int x, y;
		int bottomY = (int) (container.getHeight() * 0.9645f);
		int paddingX = 6;
		int paddingY = (int) (container.getHeight() * 0.0144f);
		if (notifications.isEmpty()) {
			x = container.getWidth() - paddingX - notif.getWidth();
			y = bottomY - notif.getHeight();
		} else {
			BubbleNotification n = notifications.get(notifications.size() - 1);
			x = n.getX();
			y = n.getY() - paddingY - notif.getHeight();
			if (y <= paddingY) {
				x -= paddingX + notif.getWidth();
				y = bottomY - notif.getHeight();
			}
		}
		notif.setPosition(x, y);
		notifications.add(notif);
	}

	/**
	 * Submits a bar notification for drawing.
	 * @param s the notification string
	 */
	public void sendBarNotification(String s) {
		if (s != null) {
			barNotif = s;
			barNotifTimer = 0;
		}
	}

	/**
	 * Resets all notifications.
	 */
	public void reset() {
		// resets the bar notification
		barNotifTimer = -1;
		barNotif = null;
	}
}