/*
 * The MIT License
 * Copyright © 2023 Luis Andrés Lange <https://javacomm.net>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.nexuswob.gui;
/*
 * @(#)SwingUtils.java	1.02 11/15/08
 *
 */

import java.awt.AWTException;

//package darrylbu.util;

import java.awt.Component;
import java.awt.Container;
import java.awt.Graphics2D;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.JComponent;
import javax.swing.UIDefaults;
import javax.swing.UIManager;



/**
 * A collection of utility methods for Swing.
 *
 * @author Darryl Burke
 */
public final class SwingUtils {

  private final static Rectangle screenSize = new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
  private static Robot robot;

  static {
    try {
      robot = new Robot();
    }
    catch (AWTException e) {}
  }

  /**
   * Exclude methods that return values that are meaningless to the user
   */
  static Set<String> setExclude = new HashSet<>();

  static {
    setExclude.add("getFocusCycleRootAncestor");
    setExclude.add("getAccessibleContext");
    setExclude.add("getColorModel");
    setExclude.add("getGraphics");
    setExclude.add("getGraphicsConfiguration");
  }

  private SwingUtils() {
    throw new Error("SwingUtils is just a container for static methods");
  }



  /**
   * Convenience method for determining whether two objects are either equal or
   * both null.
   * 
   * @param obj1
   *             the first reference object to compare.
   * @param obj2
   *             the second reference object to compare.
   * @return true if obj1 and obj2 are equal or if both are null, false otherwise
   */
  public static boolean equals(Object obj1, Object obj2) {
    return obj1 == null ? obj2 == null : obj1.equals(obj2);
  }



  private static <T extends JComponent> T getComponentFromList(Class<T> clazz, List<T> list, String property,
      Object value) throws IllegalArgumentException {
    T retVal = null;
    Method method = null;
    try {
      method = clazz.getMethod("get" + property);
    }
    catch (NoSuchMethodException ex) {
      try {
        method = clazz.getMethod("is" + property);
      }
      catch (NoSuchMethodException ex1) {
        throw new IllegalArgumentException("Property " + property + " not found in class " + clazz.getName());
      }
    }
    try {
      for (T t : list) {
        Object testVal = method.invoke(t);
        if (equals(value, testVal)) {
          return t;
        }
      }
    }
    catch (InvocationTargetException ex) {
      throw new IllegalArgumentException(
          "Error accessing property " + property + " in class " + clazz.getName()
      );
    }
    catch (IllegalAccessException | SecurityException ex) {
      throw new IllegalArgumentException(
          "Property " + property + " cannot be accessed in class " + clazz.getName()
      );
    }
    return retVal;
  }



  /**
   * Convenience method for mapping a container in the hierarchy to its contained
   * components. The keys are the containers, and the values are lists of
   * contained components.
   * <P>
   * Implementation note: The returned value is a HashMap and the values are of
   * type ArrayList. This is subject to change, so callers should code against the
   * interfaces Map and List.
   * 
   * @param container
   *                  The JComponent to be mapped
   * @param nested
   *                  true to drill down to nested containers, false otherwise
   * @return the Map of the UI
   */
  public static Map<JComponent, List<JComponent>> getComponentMap(JComponent container, boolean nested) {
    HashMap<JComponent, List<JComponent>> retVal = new HashMap<>();
    for (JComponent component : getDescendantsOfType(JComponent.class, container, false)) {
      if (!retVal.containsKey(container)) {
        retVal.put(container, new ArrayList<>());
      }
      retVal.get(container).add(component);
      if (nested) {
        retVal.putAll(getComponentMap(component, nested));
      }
    }
    return retVal;
  }



  /**
   * Convenience method that searches below <code>container</code> in the
   * component hierarchy in a depth first manner and returns the first found
   * component of class <code>clazz</code> having the bound property value.
   * <P>
   * Returns {@code null} if such component cannot be found.
   * <P>
   * This method invokes getDescendantOfClass(clazz, container, property, value,
   * true)
   * 
   * @param clazz
   *                  the class of component to be found.
   * @param container
   *                  the container at which to begin the search
   * @param property
   *                  the className of the bound property, exactly as expressed in
   *                  the accessor e.g. "Text" for getText(), "Value" for
   *                  getValue(). This parameter is case sensitive.
   * @param value
   *                  the value of the bound property
   * @return the component, or null if no such component exists in the container's
   *         hierarchy.
   * @throws java.lang.IllegalArgumentException
   *                                            if the bound property does not
   *                                            exist for the class or cannot be
   *                                            accessed
   */
  public static <T extends JComponent> T getDescendantOfClass(Class<T> clazz, Container container,
      String property, Object value) throws IllegalArgumentException {
    return getDescendantOfClass(clazz, container, property, value, true);
  }



  /**
   * Convenience method that searches below <code>container</code> in the
   * component hierarchy in a depth first manner and returns the first found
   * component of class <code>clazz</code> having the bound property value.
   * <P>
   * Returns {@code null} if such component cannot be found.
   * 
   * @param clazz
   *                  the class of component to be found.
   * @param container
   *                  the container at which to begin the search
   * @param property
   *                  the className of the bound property, exactly as expressed in
   *                  the accessor e.g. "Text" for getText(), "Value" for
   *                  getValue(). This parameter is case sensitive.
   * @param value
   *                  the value of the bound property
   * @param nested
   *                  true to include components nested within another listed
   *                  component, false otherwise
   * @return the component, or null if no such component exists in the container's
   *         hierarchy
   * @throws java.lang.IllegalArgumentException
   *                                            if the bound property does not
   *                                            exist for the class or cannot be
   *                                            accessed
   */
  public static <T extends JComponent> T getDescendantOfClass(Class<T> clazz, Container container,
      String property, Object value, boolean nested) throws IllegalArgumentException {
    List<T> list = getDescendantsOfClass(clazz, container, nested);
    return getComponentFromList(clazz, list, property, value);
  }



  /**
   * Convenience method that searches below <code>container</code> in the
   * component hierarchy and returns the first found component that is an instance
   * of class <code>clazz</code> having the bound property value. Returns
   * {@code null} if such component cannot be found.
   * <P>
   * This method invokes getDescendantOfType(clazz, container, property, value,
   * true)
   * 
   * @param clazz
   *                  the class of component whose instance is to be found.
   * @param container
   *                  the container at which to begin the search
   * @param property
   *                  the className of the bound property, exactly as expressed in
   *                  the accessor e.g. "Text" for getText(), "Value" for
   *                  getValue().
   * @param value
   *                  the value of the bound property
   * @return the component, or null if no such component exists in the container
   * @throws java.lang.IllegalArgumentException
   *                                            if the bound property does not
   *                                            exist for the class or cannot be
   *                                            accessed
   */
  public static <T extends JComponent> T getDescendantOfType(Class<T> clazz, Container container,
      String property, Object value) throws IllegalArgumentException {
    return getDescendantOfType(clazz, container, property, value, true);
  }



  /**
   * Convenience method that searches below <code>container</code> in the
   * component hierarchy and returns the first found component that is an instance
   * of class <code>clazz</code> and has the bound property value. Returns
   * {@code null} if such component cannot be found.
   * 
   * @param clazz
   *                  the class of component whose instance to be found.
   * @param container
   *                  the container at which to begin the search
   * @param property
   *                  the className of the bound property, exactly as expressed in
   *                  the accessor e.g. "Text" for getText(), "Value" for
   *                  getValue().
   * @param value
   *                  the value of the bound property
   * @param nested
   *                  true to list components nested within another component
   *                  which is also an instance of <code>clazz</code>, false
   *                  otherwise
   * @return the component, or null if no such component exists in the container
   * @throws java.lang.IllegalArgumentException
   *                                            if the bound property does not
   *                                            exist for the class or cannot be
   *                                            accessed
   */
  public static <T extends JComponent> T getDescendantOfType(Class<T> clazz, Container container,
      String property, Object value, boolean nested) throws IllegalArgumentException {
    List<T> list = getDescendantsOfType(clazz, container, nested);
    return getComponentFromList(clazz, list, property, value);
  }



  /**
   * Convenience method for searching below <code>container</code> in the
   * component hierarchy and return nested components of class <code>clazz</code>
   * it finds. Returns an empty list if no such components exist in the container.
   * <P>
   * This method invokes getDescendantsOfClass(clazz, container, true)
   * 
   * @param clazz
   *                  the class of components to be found.
   * @param container
   *                  the container at which to begin the search
   * @return the List of components
   */
  public static <T extends JComponent> List<T> getDescendantsOfClass(Class<T> clazz, Container container) {
    return getDescendantsOfClass(clazz, container, true);
  }



  /**
   * Convenience method for searching below <code>container</code> in the
   * component hierarchy and return nested components of class <code>clazz</code>
   * it finds. Returns an empty list if no such components exist in the container.
   * 
   * @param clazz
   *                  the class of components to be found.
   * @param container
   *                  the container at which to begin the search
   * @param nested
   *                  true to list components nested within another listed
   *                  component, false otherwise
   * @return the List of components
   */
  public static <T extends JComponent> List<T> getDescendantsOfClass(Class<T> clazz, Container container,
      boolean nested) {
    List<T> tList = new ArrayList<>();
    for (Component component : container.getComponents()) {
      if (clazz.equals(component.getClass())) {
        tList.add(clazz.cast(component));
      }
      if (nested || !clazz.equals(component.getClass())) {
        tList.addAll(SwingUtils.<T>getDescendantsOfClass(clazz, (Container) component, nested));
      }
    }
    return tList;
  }



  /**
   * Convenience method for searching below <code>container</code> in the
   * component hierarchy and return nested components that are instances of class
   * <code>clazz</code> it finds. Returns an empty list if no such components
   * exist in the container.
   * <P>
   * Invoking this method with a class parameter of JComponent.class will return
   * all nested components.
   * <P>
   * This method invokes getDescendantsOfType(clazz, container, true)
   * 
   * @param clazz
   *                  the class of components whose instances are to be found.
   * @param container
   *                  the container at which to begin the search
   * @return the List of components
   */
  public static <T extends JComponent> List<T> getDescendantsOfType(Class<T> clazz, Container container) {
    return getDescendantsOfType(clazz, container, true);
  }



  /**
   * Convenience method for searching below <code>container</code> in the
   * component hierarchy and return nested components that are instances of class
   * <code>clazz</code> it finds. Returns an empty list if no such components
   * exist in the container.
   * <P>
   * Invoking this method with a class parameter of JComponent.class will return
   * all nested components.
   * 
   * @param clazz
   *                  the class of components whose instances are to be found.
   * @param container
   *                  the container at which to begin the search
   * @param nested
   *                  true to list components nested within another listed
   *                  component, false otherwise
   * @return the List of components
   */
  public static <T extends JComponent> List<T> getDescendantsOfType(Class<T> clazz, Container container,
      boolean nested) {
    List<T> tList = new ArrayList<>();
    for (Component component : container.getComponents()) {
      if (clazz.isAssignableFrom(component.getClass())) {
        tList.add(clazz.cast(component));
      }
      if (nested || !clazz.isAssignableFrom(component.getClass())) {
        tList.addAll(SwingUtils.<T>getDescendantsOfType(clazz, (Container) component, nested));
      }
    }
    return tList;
  }



  /**
   * Convenience method to obtain the Swing class from which this component was
   * directly or indirectly derived.
   * 
   * @param component
   *                  The component whose Swing superclass is to be determined
   * @return The nearest Swing class in the inheritance tree
   */
  public static <T extends JComponent> Class<?> getJClass(T component) {
    Class<?> clazz = component.getClass();
    while (!clazz.getName().matches("javax.swing.J[^.]*$")) {
      clazz = clazz.getSuperclass();
    }
    return clazz;
  }



  /**
   * Convenience method for obtaining most non-null human readable properties of a
   * JComponent. Array properties are not included.
   * <P>
   * Implementation note: The returned value is a HashMap. This is subject to
   * change, so callers should code against the interface Map.
   * 
   * @param component
   *                  the component whose proerties are to be determined
   * @return the class and value of the properties
   */
  public static Map<Object, Object> getProperties(JComponent component) {
    Map<Object, Object> retVal = new HashMap<>();
    Class<?> clazz = component.getClass();
    Method[] methods = clazz.getMethods();
    Object value = null;
    for (Method method : methods) {
      if (method.getName().matches("^(is|get).*") && method.getParameterTypes().length == 0) {
        try {
          Class<?> returnType = method.getReturnType();
          if (
            returnType != void.class
                && !returnType.getName().startsWith("[")
                && !setExclude.contains(method.getName())
          ) {
            String key = method.getName();
            value = method.invoke(component);
            if (value != null && !(value instanceof Component)) {
              retVal.put(key, value);
            }
          }
          // ignore exceptions that arise if the property could not be accessed
        }
        catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {}
      }
    }
    return retVal;
  }



  /**
   * Convenience method for retrieving the UIDefault for a single property of a
   * particular class.
   * 
   * @param clazz
   *                 the class of interest
   * @param property
   *                 the property to query
   * @return the UIDefault property, or null if not found
   */
  public static Object getUIDefaultOfClass(Class<?> clazz, String property) {
    Object retVal = null;
    UIDefaults defaults = getUIDefaultsOfClass(clazz);
    List<Object> listKeys = Collections.list(defaults.keys());
    for (Object key : listKeys) {
      if (key.equals(property)) {
        return defaults.get(key);
      }
      if (key.toString().equalsIgnoreCase(property)) {
        retVal = defaults.get(key);
      }
    }
    return retVal;
  }



  /**
   * Convenience method for retrieving a subset of the UIDefaults pertaining to a
   * particular class.
   * 
   * @param clazz
   *              the class of interest
   * @return the UIDefaults of the class
   */
  public static UIDefaults getUIDefaultsOfClass(Class<?> clazz) {
    String name = clazz.getName();
    name = name.substring(name.lastIndexOf(".") + 2);
    return getUIDefaultsOfClass(name);
  }



  /**
   * Convenience method for retrieving a subset of the UIDefaults pertaining to a
   * particular class.
   * 
   * @param className
   *                  fully qualified name of the class of interest
   * @return the UIDefaults of the class named
   */
  public static UIDefaults getUIDefaultsOfClass(String className) {
    UIDefaults retVal = new UIDefaults();
    UIDefaults defaults = UIManager.getLookAndFeelDefaults();
    List<?> listKeys = Collections.list(defaults.keys());
    for (Object key : listKeys) {
      if (key instanceof String && ((String) key).startsWith(className)) {
        String stringKey = (String) key;
        String property = stringKey;
        if (stringKey.contains(".")) {
          property = stringKey.substring(stringKey.indexOf(".") + 1);
        }
        retVal.put(property, defaults.get(key));
      }
    }
    return retVal;
  }



  /**
   * Mache einen Screenshot.
   * 
   * @return dieser Screenshot
   * 
   */
  public static BufferedImage screenshot() {
    if (
      robot == null
    ) throw new UnsupportedOperationException(
        "Diese Methode kann nur in einer Swing-Umgebung ausgeführt werden."
    );
    BufferedImage image = robot.createScreenCapture(screenSize);
    return image;
  }



  /**
   * Ein Screenshot wird erstellt und zusätzlich eine Grafik auf den Screenshot
   * gezeichnet. In der Regel handelt es ich bei der Grafik um einen Mousecursor.
   * 
   * @param mouseCursor
   *                    dieser Mousecursor
   * 
   * @return dieser Screenshot mit Mousecursor
   */
  public static BufferedImage screenshotWithMousecursor(BufferedImage mouseCursor) {
    BufferedImage image = screenshot();
    if (mouseCursor == null) return image;
    Graphics2D g2 = image.createGraphics();
    Point mousepointer = MouseInfo.getPointerInfo().getLocation();
    g2.drawImage(mouseCursor, mousepointer.x, mousepointer.y, null);
    g2.dispose();
    return image;
  }

}
