package games.strategy.ui;

import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.geom.GeneralPath;
import java.awt.image.BufferedImage;
import java.awt.image.ImageObserver;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;

import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;

import games.strategy.util.EventThreadJOptionPane;

public final class Util {
  public static final String TERRITORY_SEA_ZONE_INFIX = "Sea Zone";

  // all we have is static methods
  private Util() {}

  public interface Task<T> {
    T run();
  }

  public static <T> T runInSwingEventThread(final Task<T> task) {
    if (SwingUtilities.isEventDispatchThread()) {
      return task.run();
    }
    final AtomicReference<T> results = new AtomicReference<>();
    SwingAction.invokeAndWait(() -> results.set(task.run()));
    return results.get();
  }

  private static final Component c = new Component() {
    private static final long serialVersionUID = 1800075529163275600L;
  };

  public static void ensureImageLoaded(final Image anImage) {
    final MediaTracker tracker = new MediaTracker(c);
    tracker.addImage(anImage, 1);
    try {
      tracker.waitForAll();
      tracker.removeImage(anImage);
    } catch (final InterruptedException ignored) {
      // ignore interrupted
    }
  }

  public static Image copyImage(final BufferedImage img) {
    final BufferedImage copy = createImage(img.getWidth(), img.getHeight(), false);
    final Graphics2D g = (Graphics2D) copy.getGraphics();
    g.drawImage(img, 0, 0, null);
    g.dispose();
    return copy;
  }

  public static void notifyError(final Component parent, final String message) {
    EventThreadJOptionPane.showMessageDialog(JOptionPane.getFrameForComponent(parent), message, "Error",
        JOptionPane.ERROR_MESSAGE);
  }

  /**
   * Previously used to use TYPE_INT_BGR and TYPE_INT_ABGR but caused memory
   * problems. Fix is to use 3Byte rather than INT.
   */
  public static BufferedImage createImage(final int width, final int height, final boolean needAlpha) {
    if (needAlpha) {
      return new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR);
    } else {
      return new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
    }
  }

  public static Dimension getDimension(final Image anImage, final ImageObserver obs) {
    return new Dimension(anImage.getWidth(obs), anImage.getHeight(obs));
  }

  public static void center(final Window w) {
    final int screenWidth = Toolkit.getDefaultToolkit().getScreenSize().width;
    final int screenHeight = Toolkit.getDefaultToolkit().getScreenSize().height;
    final int windowWidth = w.getWidth();
    final int windowHeight = w.getHeight();
    if (windowHeight > screenHeight) {
      return;
    }
    if (windowWidth > screenWidth) {
      return;
    }
    final int x = (screenWidth - windowWidth) / 2;
    final int y = (screenHeight - windowHeight) / 2;
    w.setLocation(x, y);
  }

  // code stolen from swingx
  // swingx is lgpl, so no problems with copyright
  public static Image getBanner(final String text) {
    final int w = 400;
    final int h = 60;
    final float loginStringX = w * .05f;
    final float loginStringY = h * .75f;
    final BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
    final Graphics2D g2 = img.createGraphics();
    final Font font = new Font("Arial Bold", Font.PLAIN, 36);
    g2.setFont(font);
    final Graphics2D originalGraphics = g2;
    g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
    g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
    g2.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
    // draw a big square
    g2.setColor(Color.GRAY);
    g2.fillRect(0, 0, w, h);
    // create the curve shape
    final GeneralPath curveShape = new GeneralPath(GeneralPath.WIND_NON_ZERO);
    curveShape.moveTo(0, h * .6f);
    curveShape.curveTo(w * .167f, h * 1.2f, w * .667f, h * -.5f, w, h * .75f);
    curveShape.lineTo(w, h);
    curveShape.lineTo(0, h);
    curveShape.lineTo(0, h * .8f);
    curveShape.closePath();
    // draw into the buffer a gradient (bottom to top), and the text "Login"
    final GradientPaint gp = new GradientPaint(0, h, Color.GRAY, 0, 0, Color.LIGHT_GRAY);
    g2.setPaint(gp);
    g2.fill(curveShape);
    // g2.setPaint(Color.white);
    originalGraphics.setColor(Color.WHITE);
    originalGraphics.drawString(text, loginStringX, loginStringY);
    return img;
  }

  /**
   * java.lang.String findTerritoryName(java.awt.Point)
   * Finds a land territory name or some sea zone name where the point is contained in according to the territory name
   * -> polygons map.
   *
   * @param p A point on the map.
   * @param terrPolygons a map territory name -> polygons
   * @return Optional&lt;String>
   */
  public static Optional<String> findTerritoryName(final Point p, final Map<String, List<Polygon>> terrPolygons) {
    return Optional.ofNullable(findTerritoryName(p, terrPolygons, null));
  }

  /**
   * java.lang.String findTerritoryName(java.awt.Point)
   * Finds a land territory name or some sea zone name where the point is contained in according to the territory name
   * -> polygons map. If no land or sea territory has been found a default name is returned.
   *
   * @param p A point on the map.
   * @param terrPolygons a map territory name -> polygons
   * @param defaultTerrName Default territory name that gets returns if nothing was found.
   * @return found territory name of defaultTerrName
   */
  public static String findTerritoryName(final Point p, final Map<String, List<Polygon>> terrPolygons,
      final String defaultTerrName) {
    String lastWaterTerrName = defaultTerrName;
    // try to find a land territory.
    // sea zones often surround a land territory
    for (final String terrName : terrPolygons.keySet()) {
      final Collection<Polygon> polygons = terrPolygons.get(terrName);
      for (final Polygon poly : polygons) {
        if (poly.contains(p)) {
          if (Util.isTerritoryNameIndicatingWater(terrName)) {
            lastWaterTerrName = terrName;
          } else {
            return terrName;
          }
        } // if p is contained
      } // polygons collection loop
    } // terrPolygons map loop
    return lastWaterTerrName;
  }

  /**
   * Checks whether name indicates water or not (meaning name starts or ends with default text).
   *
   * @param territoryName - territory name
   * @return true if yes, false otherwise
   */
  public static boolean isTerritoryNameIndicatingWater(final String territoryName) {
    return territoryName.endsWith(TERRITORY_SEA_ZONE_INFIX) || territoryName.startsWith(TERRITORY_SEA_ZONE_INFIX);
  }
}
