/*******************************************************************************
 * PGPTool is a desktop application for pgp encryption/decryption
 * Copyright (C) 2019 Sergey Karpushin
 * 
 * This program 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.
 * 
 * This program 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 this program.  If not, see <http://www.gnu.org/licenses/>
 ******************************************************************************/
package org.pgptool.gui.ui.tools;

import static org.pgptool.gui.app.Messages.text;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeEvent;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.swing.Action;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.UIDefaults;
import javax.swing.UIManager;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringEscapeUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.log4j.Logger;
import org.jdesktop.swingx.imported.JXLabel;
import org.pgptool.gui.app.EntryPoint;
import org.pgptool.gui.app.MessageSeverity;
import org.pgptool.gui.app.Messages;

import ru.skarpushin.swingpm.EXPORT.base.PresentationModelBase;
import ru.skarpushin.swingpm.tools.edt.Edt;

public class UiUtils {
	public static Logger log = Logger.getLogger(UiUtils.class);

	/**
	 * By default window will be placed at 0x0 coordinates, which is not pretty. We
	 * have to position it to screen center
	 * 
	 * @param subject
	 *            the window to position
	 * @param optionalOrigin
	 *            The window where the action to open subject originated from
	 */
	public static void centerWindow(Window subject, Window optionalOrigin) {
		GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
		List<GraphicsDevice> graphicsDevices = Arrays.asList(ge.getScreenDevices());

		if (graphicsDevices.size() == 1) {
			// Single monitor configuration -- easy-peasy
			Dimension scrDim = Toolkit.getDefaultToolkit().getScreenSize();
			Point location = offsetWindowLocation(subject, scrDim.width / 2, scrDim.height / 2);
			subject.setLocation(location);
			return;
		}

		// Multi-monitor configuration -- not a rocket science too
		log.debug(
				"Positioning window according to multi-monitor configuration: " + formatMonitorSizes(graphicsDevices));
		Pair<GraphicsDevice, GraphicsConfiguration> device = null;
		if (optionalOrigin != null) {
			device = determineGraphicsDeviceByWindow(ge, graphicsDevices, optionalOrigin);
			if (device != null && log.isDebugEnabled()) {
				int index = graphicsDevices.indexOf(device.getLeft());
				log.debug("Parent window " + format(optionalOrigin.getBounds()) + " belongs to screen "
						+ format(index, device.getLeft(), device.getRight()));
			}
		}

		if (device == null) {
			device = Pair.of(ge.getDefaultScreenDevice(), ge.getDefaultScreenDevice().getDefaultConfiguration());
			int index = graphicsDevices.indexOf(device.getLeft());
			log.debug("Selecting default screen: " + format(index, device.getLeft(), device.getRight()));
		}

		Rectangle bounds = device.getRight().getBounds();
		Point location = offsetWindowLocation(subject, bounds.x + bounds.width / 2, bounds.y + bounds.height / 2);
		subject.setLocation(location);
	}

	private static Point offsetWindowLocation(Window frm, int centerPointX, int centerPointY) {
		return new Point(centerPointX - frm.getSize().width / 2, centerPointY - frm.getSize().height / 2);
	}

	private static Pair<GraphicsDevice, GraphicsConfiguration> determineGraphicsDeviceByWindow(
			GraphicsEnvironment graphicsEnvironment, List<GraphicsDevice> graphicsDevices, Window window) {
		log.debug("determineGraphicsDeviceByWindow " + format(window.getBounds()));

		Rectangle windowBounds = window.getBounds();
		int lastArea = 0;
		Pair<GraphicsDevice, GraphicsConfiguration> ret = null;
		for (int i = 0; i < graphicsDevices.size(); ++i) {
			GraphicsDevice graphicsDevice = graphicsDevices.get(i);
			log.debug("- Checking GraphicsDevice: "
					+ format(i, graphicsDevice, graphicsDevice.getDefaultConfiguration()));
			GraphicsConfiguration[] graphicsConfigurations = graphicsDevice.getConfigurations();
			Set<Rectangle> seen = new HashSet<>();
			for (int j = 0; j < graphicsConfigurations.length; ++j) {
				GraphicsConfiguration graphicsConfiguration = graphicsConfigurations[j];
				Rectangle graphicsBounds = graphicsConfiguration.getBounds();
				if (!seen.add(graphicsBounds)) {
					continue;
				}

				log.debug("  - Checking GraphicsConfiguration: " + format(graphicsBounds));
				Rectangle intersection = windowBounds.intersection(graphicsBounds);
				int area = intersection.width * intersection.height;
				if (area != 0 && area > lastArea) {
					lastArea = area;
					ret = Pair.of(graphicsDevice, graphicsConfiguration);
				}
			}
		}

		return ret;
	}

	private static String formatMonitorSizes(List<GraphicsDevice> graphicsDevices) {
		String ret = "";
		for (int i = 0; i < graphicsDevices.size(); i++) {
			if (i > 0) {
				ret += ", ";
			}

			GraphicsDevice graphicsDevice = graphicsDevices.get(i);
			ret += format(i, graphicsDevice, graphicsDevice.getDefaultConfiguration());
		}
		return ret;
	}

	private static String format(int index, GraphicsDevice graphicsDevice,
			GraphicsConfiguration graphicsConfiguration) {
		Rectangle bounds = graphicsConfiguration.getBounds();
		return "" + index + ": " + format(bounds);
	}

	private static String format(Rectangle bounds) {
		return "x=" + (int) bounds.getX() + ",y=" + (int) bounds.getY() + ",w=" + (int) bounds.getWidth() + ",h="
				+ (int) bounds.getHeight();
	}

	@SuppressWarnings("deprecation")
	public static String plainToBoldHtmlString(String text) {
		return "<html><body><b>" + StringEscapeUtils.escapeXml(text) + "</b></body></html>";
	}

	@SuppressWarnings("deprecation")
	public static String envelopeStringIntoHtml(String text) {
		return "<html><body>" + StringEscapeUtils.escapeXml(text) + "</body></html>";
	}

	public static boolean confirmRegular(ActionEvent originEvent, String userPromptMessageCode, Object[] messageArgs) {
		return confirm(originEvent, userPromptMessageCode, messageArgs, JOptionPane.QUESTION_MESSAGE);
	}

	public static boolean confirmWarning(ActionEvent originEvent, String userPromptMessageCode, Object[] messageArgs) {
		return confirm(originEvent, userPromptMessageCode, messageArgs, JOptionPane.WARNING_MESSAGE);
	}

	private static boolean confirm(ActionEvent originEvent, String userPromptMessageCode, Object[] messageArgs,
			int severity) {
		int response = JOptionPane.OK_OPTION;

		String msg = Messages.get(userPromptMessageCode, messageArgs);
		if (msg.length() > 70) {
			response = JOptionPane.showConfirmDialog(findWindow(originEvent), getMultilineMessage(msg),
					Messages.get("term.confirmation"), JOptionPane.OK_CANCEL_OPTION, severity);
		} else {
			response = JOptionPane.showConfirmDialog(findWindow(originEvent), msg, Messages.get("term.confirmation"),
					JOptionPane.OK_CANCEL_OPTION, severity);
		}
		return response == JOptionPane.OK_OPTION;
	}

	public static String promptUserForTextString(ActionEvent originEvent, String windowTitle, String fieldLabel,
			Object[] fieldLabelMsgArgs) {
		String ret = JOptionPane.showInputDialog(findWindow(originEvent), Messages.get(fieldLabel, fieldLabelMsgArgs),
				Messages.get(windowTitle), JOptionPane.QUESTION_MESSAGE);

		return ret == null ? null : ret.trim();
	}

	public static int getFontRelativeSize(int size) {
		StringBuilder sb = new StringBuilder(size);
		for (int i = 0; i < size; i++) {
			sb.append("W");
		}
		JLabel lbl = new JLabel(sb.toString());
		return (int) lbl.getPreferredSize().getWidth();
	}

	public static void setLookAndFeel() {
		// NOTE: We doing it this way to prevent dead=locks that is sometimes
		// happens if do it in main thread
		Edt.invokeOnEdtAndWait(new Runnable() {
			@Override
			public void run() {
				try {
					UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
					fixCheckBoxMenuItemForeground();
					fixFontSize();
				} catch (Throwable t) {
					log.error("Failed to set L&F", t);
				}
			}

			/**
			 * In some cases (depends on OS theme) check menu item foreground is same as
			 * background - thus it's invisible when checked
			 */
			private void fixCheckBoxMenuItemForeground() {
				UIDefaults defaults = UIManager.getDefaults();
				Color selectionForeground = defaults.getColor("CheckBoxMenuItem.selectionForeground");
				Color foreground = defaults.getColor("CheckBoxMenuItem.foreground");
				Color background = defaults.getColor("CheckBoxMenuItem.background");
				if (colorsDiffPercentage(selectionForeground, background) < 10) {
					// TBD: That doesn't actually affect defaults. Need to find out how to fix it
					defaults.put("CheckBoxMenuItem.selectionForeground", foreground);
				}
			}

			private int colorsDiffPercentage(Color c1, Color c2) {
				int diffRed = Math.abs(c1.getRed() - c2.getRed());
				int diffGreen = Math.abs(c1.getGreen() - c2.getGreen());
				int diffBlue = Math.abs(c1.getBlue() - c2.getBlue());

				float pctDiffRed = (float) diffRed / 255;
				float pctDiffGreen = (float) diffGreen / 255;
				float pctDiffBlue = (float) diffBlue / 255;

				return (int) ((pctDiffRed + pctDiffGreen + pctDiffBlue) / 3 * 100);
			}

			private void fixFontSize() {
				if (isJreHandlesScaling()) {
					log.info("JRE handles font scaling, won't change it");
					return;
				}
				log.info("JRE doesnt't seem to support font scaling");

				Toolkit toolkit = Toolkit.getDefaultToolkit();
				int dpi = toolkit.getScreenResolution();
				if (dpi == 96) {
					if (log.isDebugEnabled()) {
						Font font = UIManager.getDefaults().getFont("TextField.font");
						String current = font != null ? Integer.toString(font.getSize()) : "unknown";
						log.debug(
								"Screen dpi seem to be 96. Not going to change font size. Btw current size seem to be "
										+ current);
					}
					return;
				}
				int targetFontSize = 12 * dpi / 96;
				log.debug("Screen dpi = " + dpi + ", decided to change font size to " + targetFontSize);
				setDefaultSize(targetFontSize);
			}

			private boolean isJreHandlesScaling() {
				try {
					JreVersion noNeedToScaleForVer = JreVersion.parseString("9");
					String jreVersionStr = System.getProperty("java.version");
					if (jreVersionStr != null) {
						JreVersion curVersion = JreVersion.parseString(jreVersionStr);
						if (noNeedToScaleForVer.compareTo(curVersion) <= 0) {
							return true;
						}
					}

					return false;
				} catch (Throwable t) {
					log.warn("Failed to see oif JRE can handle font scaling. Will assume it does. JRE version: "
							+ System.getProperty("java.version"), t);
					return true;
				}
			}

			public void setDefaultSize(int size) {
				Set<Object> keySet = UIManager.getLookAndFeelDefaults().keySet();
				Object[] keys = keySet.toArray(new Object[keySet.size()]);
				for (Object key : keys) {
					if (key != null && key.toString().toLowerCase().contains("font")) {
						Font font = UIManager.getDefaults().getFont(key);
						if (font != null) {
							Font changedFont = font.deriveFont((float) size);
							UIManager.put(key, changedFont);
							Font doubleCheck = UIManager.getDefaults().getFont(key);
							log.debug("Font size changed for " + key + ". From " + font.getSize() + " to "
									+ doubleCheck.getSize());
						}
					}
				}
			}
		});
		log.info("L&F set");
	}

	/**
	 * Hack to make sure window is visible. On windows it's sometimes created but on
	 * a background. User can see "flashing" icon in a task bar but window stays on
	 * a background.
	 * 
	 * PRESUMING: setVisible(true) was already called
	 * 
	 * More ion tihs jere:
	 * http://stackoverflow.com/questions/309023/how-to-bring-a-window-to-the-front
	 */
	public static void makeSureWindowBroughtToFront(Window window) {
		// int state = dialog.getExtendedState();
		// state &= ~JFrame.ICONIFIED;
		// dialog.setExtendedState(state);
		window.setAlwaysOnTop(true);
		window.toFront();
		window.requestFocus();
		window.setAlwaysOnTop(false);
		window.repaint();
	}

	public static void messageBox(ActionEvent originEvent, String messageText, String messageTitle,
			MessageSeverity messageSeverity) {
		int messageType = JOptionPane.INFORMATION_MESSAGE;
		if (messageSeverity == MessageSeverity.ERROR) {
			messageType = JOptionPane.ERROR_MESSAGE;
		} else if (messageSeverity == MessageSeverity.WARNING) {
			messageType = JOptionPane.WARNING_MESSAGE;
		} else if (messageSeverity == MessageSeverity.INFO) {
			messageType = JOptionPane.INFORMATION_MESSAGE;
		}
		UiUtils.messageBox(originEvent, messageText, messageTitle, messageType);
	}

	/**
	 * @param messageType
	 *            one of the JOptionPane ERROR_MESSAGE, INFORMATION_MESSAGE,
	 *            WARNING_MESSAGE, QUESTION_MESSAGE, or PLAIN_MESSAGE
	 */
	public static void messageBox(ActionEvent originEvent, String msg, String title, int messageType) {
		Object content = buildMessageContentDependingOnLength(msg);

		Component parent = findWindow(originEvent);
		if (messageType != JOptionPane.ERROR_MESSAGE) {
			JOptionPane.showMessageDialog(parent, content, title, messageType);
			return;
		}

		Object[] options = { text("action.ok"), text("phrase.saveMsgToFile") };
		if ("action.ok".equals(options[0])) {
			// if app context wasn't started MessageSource wont be available
			options = new String[] { "OK", "Save message to file" };
		}

		int result = JOptionPane.showOptionDialog(parent, content, title, JOptionPane.YES_NO_OPTION, messageType, null,
				options, JOptionPane.YES_OPTION);
		if (result == JOptionPane.YES_OPTION || result == JOptionPane.CLOSED_OPTION) {
			return;
		}

		// Save to file
		saveMessageToFile(parent, msg);
	}

	private static void saveMessageToFile(Component parent, String msg) {
		JFileChooser fileChooser = new JFileChooser();
		if (fileChooser.showSaveDialog(parent) != JFileChooser.APPROVE_OPTION) {
			return;
		}

		File file = fileChooser.getSelectedFile();
		try {
			FileUtils.write(file, msg, Charset.forName("UTF-8"), false);
		} catch (IOException e) {
			log.error("Failed to save error message to file: " + file, e);
			JOptionPane.showMessageDialog(parent, "Failed to save message to file", "Error", JOptionPane.ERROR_MESSAGE);
			// come on !!!
		}
	}

	private static Object buildMessageContentDependingOnLength(String msg) {
		Object content = "";
		if (msg.length() > 300 || msg.split("\n").length > 2) {
			content = getScrollableMessage(msg);
		} else if (msg.length() > 100) {
			content = getMultilineMessage(msg);
		} else {
			content = msg;
		}
		return content;
	}

	private static JScrollPane getScrollableMessage(String msg) {
		JTextArea textArea = new JTextArea(msg);
		textArea.setLineWrap(true);
		textArea.setWrapStyleWord(true);
		textArea.setEditable(false);
		textArea.setMargin(new Insets(5, 5, 5, 5));
		textArea.setFont(new JTextField().getFont()); // dirty fix to use better font
		JScrollPane scrollPane = new JScrollPane();
		scrollPane.setPreferredSize(new Dimension(700, 150));
		scrollPane.getViewport().setView(textArea);
		return scrollPane;
	}

	public static JComponent getMultilineMessage(String msg) {
		JXLabel lbl = new JXLabel(msg);
		lbl.setLineWrap(true);
		lbl.setMaxLineSpan(getFontRelativeSize(50));
		return lbl;
	}

	public static Window findWindow(ActionEvent event) {
		Window defaultValue = null;
		if (EntryPoint.rootPmStatic != null) {
			defaultValue = EntryPoint.rootPmStatic.findMainFrameWindow();
		}

		if (event == null) {
			return defaultValue;
		}
		Object source = event.getSource();
		if (source == null) {
			return defaultValue;
		}

		if (source instanceof Component) {
			Window containingWindow = findWindow((Component) source);
			if (containingWindow != null) {
				return containingWindow;
			}
		}

		if (source instanceof PresentationModelBase) {
			Window view = ((PresentationModelBase<?, ?>) source).findRegisteredWindowIfAny();
			if (view != null) {
				return view;
			}
		}

		return defaultValue;
	}

	public static Window findWindow(Component c) {
		Window defaultValue = null;
		if (EntryPoint.rootPmStatic != null) {
			defaultValue = EntryPoint.rootPmStatic.findMainFrameWindow();
		}
		if (c == null) {
			return defaultValue;
		} else if (c instanceof JPopupMenu) {
			Component invoker = ((JPopupMenu) c).getInvoker();
			if (invoker == null) {
				return defaultValue;
			}
			return SwingUtilities.getWindowAncestor(invoker);
		} else if (c instanceof Window) {
			return (Window) c;
		} else {
			return findWindow(c.getParent());
		}
	}

	public static ActionEvent actionEvent(Object source, Action action) {
		if (source == null) {
			return null;
		}
		return actionEvent(source, String.valueOf(action.getValue(Action.NAME)));
	}

	public static ActionEvent actionEvent(Object source, String actionName) {
		if (source == null) {
			return null;
		}
		return new ActionEvent(source, ActionEvent.ACTION_PERFORMED, actionName);
	}

	public static ActionEvent actionEvent(PropertyChangeEvent evt) {
		if (evt.getSource() == null) {
			return null;
		}
		// NOTE: This is flawed. Currently, event source is what was hard-coded in
		// PresentationModel upon propertyModel init.
		// We should be propagating source from Binding and
		// ModelProperty.setValueByConsumer
		return actionEvent(evt.getSource(), evt.getPropertyName() + " property changed");
	}

}