/*
 * Created on Mar 11, 2005
 */
package org.flexdock.docking.defaults;

import java.awt.Component;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;

import org.flexdock.docking.Dockable;
import org.flexdock.docking.DockingConstants;
import org.flexdock.docking.DockingManager;
import org.flexdock.docking.DockingPort;
import org.flexdock.docking.RegionChecker;

/**
 * @author Christopher Butler
 */
public class DefaultRegionChecker implements RegionChecker, DockingConstants {

    /**
     * Returns the docking region of the supplied {@code Component} that
     * contains the coordinates of the specified {@code Point}. If either
     * {@code comp} or {@code point} is {@code null}, then
     * {@code UNKNOWN_REGION} is returned. If the specified {@code Component}
     * bounds do not contain the supplied {@code Point}, then
     * {@code UNKNOWN_REGION} is returned.
     * <p>
     * This implementation assumes that {@code comp} is a {@code Component}
     * embedded within a {@code DockingPort}. If {@code comp} is itself a
     * {@code DockingPort}, then {@code CENTER_REGION} is returned. Otherwise,
     * the returned region is based upon a section of the bounds of the
     * specified {@code Component} relative to the containing
     * {@code DockingPort}.
     * <p>
     * This method divides the specified {@code Component's} bounds into four
     * {@code Rectangles} determined by {@code getNorthRegion(Component c)},
     * {@code getSouthRegion(Component c)}, {@code getEastRegion(Component c)},
     * and {@code getWestRegion(Component c)}, respectively. Each
     * {@code Rectangle} is then checked to see if it contains the specified
     * {@code Point}. The order of precedence is NORTH, SOUTH, EAST, and then
     * WEST. If the specified {@code Point} is contained by the
     * {@code Component} bounds but none of the sub-{@code Rectangles}, then
     * {@code CENTER_REGION} is returned.
     * <p>
     * For NORTH and SOUTH {@code Rectangles}, the distance is checked between
     * the top/bottom and left or right edge of the regional bounds. If the
     * horizontal distance to the regional edge is smaller than the vertical
     * distance, then EAST or WEST takes precendence of NORTH or SOUTH. This
     * allows for proper determination between "northeast", "northwest",
     * "southeast", and "southwest" cases.
     *
     * @param comp
     *            the {@code Component} whose region is to be examined.
     * @param point
     *            the coordinates whose region is to be determined.
     * @return the docking region containing the specified {@code Point}.
     * @see RegionChecker#getRegion(Component, Point)
     * @see #getNorthRegion(Component)
     * @see #getSouthRegion(Component)
     * @see #getEastRegion(Component)
     * @see #getWestRegion(Component)
     */
    public String getRegion(Component comp, Point point) {
        if (comp == null || point == null)
            return UNKNOWN_REGION;

        // make sure the point is actually inside of the target dockingport
        Rectangle targetArea = comp.getBounds();
        // if our target component is the dockingport itself, then getBounds()
        // would
        // have returned a target area relative to the dockingport's parent.
        // reset
        // relative to the dockingport.
        if (comp instanceof DockingPort)
            targetArea.setLocation(0, 0);
        if (!targetArea.contains(point))
            return UNKNOWN_REGION;

        // if our target component is the dockingport, then the dockingport is
        // currently empty and all points within it are in the CENTER
        if (comp instanceof DockingPort)
            return CENTER_REGION;

        // start with the north region
        Rectangle north = getNorthRegion(comp);
        int rightX = north.x + north.width;
        if (north.contains(point)) {
            // check NORTH_WEST
            Rectangle west = getWestRegion(comp);
            if (west.contains(point)) {
                Polygon westPoly = new Polygon();
                westPoly.addPoint(0, 0);
                westPoly.addPoint(0, north.height);
                westPoly.addPoint(west.width, north.height);
                return westPoly.contains(point) ? WEST_REGION : NORTH_REGION;
            }
            // check NORTH_EAST
            Rectangle east = getEastRegion(comp);
            if (east.contains(point)) {
                Polygon eastPoly = new Polygon();
                eastPoly.addPoint(rightX, 0);
                eastPoly.addPoint(rightX, north.height);
                eastPoly.addPoint(east.x, north.height);
                return eastPoly.contains(point) ? EAST_REGION : NORTH_REGION;
            }
            return NORTH_REGION;
        }

        // check with the south region
        Rectangle south = getSouthRegion(comp);
        int bottomY = south.y + south.height;
        if (south.contains(point)) {
            // check SOUTH_WEST
            Rectangle west = getWestRegion(comp);
            if (west.contains(point)) {
                Polygon westPoly = new Polygon();
                westPoly.addPoint(0, south.y);
                westPoly.addPoint(west.width, south.y);
                westPoly.addPoint(0, bottomY);
                return westPoly.contains(point) ? WEST_REGION : SOUTH_REGION;
            }
            // check SOUTH_EAST
            Rectangle east = getEastRegion(comp);
            if (east.contains(point)) {
                Polygon eastPoly = new Polygon();
                eastPoly.addPoint(east.y, south.y);
                eastPoly.addPoint(rightX, south.y);
                eastPoly.addPoint(rightX, bottomY);
                return eastPoly.contains(point) ? EAST_REGION : SOUTH_REGION;
            }
            return SOUTH_REGION;
        }

        // Now check EAST and WEST. We've already checked NORTH and SOUTH, so we
        // don't have to
        // check for NE, SE, NW, and SW anymore.
        Rectangle east = getEastRegion(comp);
        if (east.contains(point))
            return EAST_REGION;
        Rectangle west = getWestRegion(comp);
        if (west.contains(point))
            return WEST_REGION;

        // not in any of the outer regions, so return CENTER.
        return CENTER_REGION;
    }

    /**
     * Returns the rectangular bounds within the specified component that
     * represent it's {@code DockingConstants.NORTH_REGION}. This method
     * dispatches to {@code getRegionBounds(Component c, String region)},
     * passing an argument of {@code DockingConstants.NORTH_REGION} for the
     * region parameter. If the specified {@code Component} is {@code null},
     * then a {@code null} reference is returned.
     *
     * @param c
     *            the {@code Component} whose north region is to be returned.
     * @return the bounds containing the north region of the specified
     *         {@code Component}.
     * @see RegionChecker#getNorthRegion(Component)
     * @see #getRegionBounds(Component, String)
     */
    public Rectangle getNorthRegion(Component c) {
        return getRegionBounds(c, NORTH_REGION);
    }

    /**
     * Returns the rectangular bounds within the specified component that
     * represent it's {@code DockingConstants.SOUTH_REGION}. This method
     * dispatches to {@code getRegionBounds(Component c, String region)},
     * passing an argument of {@code DockingConstants.SOUTH_REGION} for the
     * region parameter. If the specified {@code Component} is {@code null},
     * then a {@code null} reference is returned.
     *
     * @param c
     *            the {@code Component} whose south region is to be returned.
     * @return the bounds containing the north region of the specified
     *         {@code Component}.
     * @see RegionChecker#getSouthRegion(Component)
     * @see #getRegionBounds(Component, String)
     */
    public Rectangle getSouthRegion(Component c) {
        return getRegionBounds(c, SOUTH_REGION);
    }

    /**
     * Returns the rectangular bounds within the specified component that
     * represent it's {@code DockingConstants.EAST_REGION}. This method
     * dispatches to {@code getRegionBounds(Component c, String region)},
     * passing an argument of {@code DockingConstants.EAST_REGION} for the
     * region parameter. If the specified {@code Component} is {@code null},
     * then a {@code null} reference is returned.
     *
     * @param c
     *            the {@code Component} whose east region is to be returned.
     * @return the bounds containing the north region of the specified
     *         {@code Component}.
     * @see RegionChecker#getEastRegion(Component)
     * @see #getRegionBounds(Component, String)
     */
    public Rectangle getEastRegion(Component c) {
        return getRegionBounds(c, EAST_REGION);
    }

    /**
     * Returns the rectangular bounds within the specified component that
     * represent it's {@code DockingConstants.WEST_REGION}. This method
     * dispatches to {@code getRegionBounds(Component c, String region)},
     * passing an argument of {@code DockingConstants.WEST_REGION} for the
     * region parameter. If the specified {@code Component} is {@code null},
     * then a {@code null} reference is returned.
     *
     * @param c
     *            the {@code Component} whose west region is to be returned.
     * @return the bounds containing the north region of the specified
     *         {@code Component}.
     * @see RegionChecker#getWestRegion(Component)
     * @see #getRegionBounds(Component, String)
     */
    public Rectangle getWestRegion(Component c) {
        return getRegionBounds(c, WEST_REGION);
    }

    /**
     * Returns the bounding {@code Rectangle} within the specified component
     * that represents the specified region. If {@code c} or {@code region} are
     * null, then this method returns a {@code null} reference.
     * <p>
     * This method dispatches to
     * {@code getRegionSize(Component c, String region)} to determine the
     * proportional size of the specified {@code Component} dedicated to the
     * specified region. It then multiplies this value by the relevant
     * {@code Component} dimension (<i>{@code width} for east/west,
     * {@code height} for north/south</i>) and returns a {@code Rectangle} with
     * the resulting dimension, spanning the {@code Component} edge for the
     * specified region.
     *
     * @param c
     *            the {@code Component} whose region bounds are to be returned.
     * @param region
     *            the specified region that is to be examined.
     * @return the bounds containing the supplied region of the specified
     *         {@code Component}.
     * @see RegionChecker#getRegionBounds(Component, String)
     * @see #getRegionSize(Component, String)
     */
    public Rectangle getRegionBounds(Component c, String region) {
        if (c != null && region != null) {
            float size = getRegionSize(c, region);
            return calculateRegionalBounds(c, region, size);
        }
        return null;
    }

    /**
     * Returns the bounding {@code Rectangle} within the specified component
     * that represents the desired area to be allotted for sibling
     * {@code Components} in the specified region. If {@code c} or
     * {@code region} are null, then this method returns a {@code null}
     * reference.
     * <p>
     * This method dispatches to
     * {@code getSiblingSize(Component c, String region)} to determine the
     * proportional size of the specified {@code Component} dedicated to
     * siblings in the specified region. It then multiplies this value by the
     * relevant {@code Component} dimension (<i>{@code width} for east/west,
     * {@code height} for north/south</i>) and returns a {@code Rectangle} with
     * the resulting dimension, spanning the {@code Component} edge for the
     * specified region.
     *
     * @param c
     *            the {@code Component} whose sibling bounds are to be returned.
     * @param region
     *            the specified region that is to be examined.
     * @return the bounds representing the allotted sibling area for the
     *         supplied region of the specified {@code Component}.
     * @see RegionChecker#getSiblingBounds(Component, String)
     * @see #getSiblingSize(Component, String)
     */
    public Rectangle getSiblingBounds(Component c, String region) {
        if (c != null && region != null) {
            float size = getSiblingSize(c, region);
            return calculateRegionalBounds(c, region, size);
        }
        return null;
    }

    protected Rectangle calculateRegionalBounds(Component c, String region,
            float size) {
        if (c == null || region == null)
            return null;

        Rectangle bounds = c.getBounds();

        if (NORTH_REGION.equals(region) || SOUTH_REGION.equals(region)) {
            int h = (int) ((float) bounds.height * size);
            int y = NORTH_REGION.equals(region) ? 0 : bounds.height - h;
            return new Rectangle(0, y, bounds.width, h);
        }

        if (WEST_REGION.equals(region) || EAST_REGION.equals(region)) {
            int w = (int) ((float) bounds.width * size);
            int x = WEST_REGION.equals(region) ? 0 : bounds.width - w;
            return new Rectangle(x, 0, w, bounds.height);
        }
        return null;
    }

    /**
     * Returns a percentage (0.0F through 1.0F) representing the amount of space
     * allotted for the specified region within the specified {@code Component}.
     * <p>
     * This method resolves the {@code Dockable} associated with the specified
     * {@code Component} and dispatches to
     * {@code getRegionPreference(Dockable d, String region)}.
     * {@code getRegionPreference(Dockable d, String region)} attempts to invoke
     * {@code getDockingProperties()} on the {@code Dockable} to resolve a
     * {@code DockablePropertySet} instance and return from its
     * {@code getRegionInset(String region)} method.
     * <p>
     * If the specified {@code Component} is {@code null}, no {@code Dockable}
     * can be resolved, or no value is specified in the {@code Dockable's}
     * associated {@code DockingProps} instance, then the default value of
     * {@code RegionChecker.DEFAULT_REGION_SIZE} is returned.
     *
     * @param c
     *            the {@code Component} whose region is to be examined.
     * @param region
     *            the specified region that is to be examined.
     * @return the percentage of the specified {@code Component} allotted for
     *         the specified region.
     * @see RegionChecker#getRegionSize(Component, String)
     * @see DockingManager#getDockable(Component)
     * @see #getRegionPreference(Dockable, String)
     * @see Dockable#getDockingProperties()
     */
    public float getRegionSize(Component c, String region) {
        Dockable d = DockingManager.getDockable(c);
        return getRegionPreference(d, region);
    }

    /**
     * Returns a percentage (0.0F through 1.0F) representing the amount of space
     * allotted for sibling {@code Component} docked to the specified region
     * within the specified {@code Component}.
     * <p>
     * This method resolves the {@code Dockable} associated with the specified
     * {@code Component} and dispatches to
     * {@code getSiblingPreference(Dockable d, String region)}.
     * {@code getSiblingPreference(Dockable d, String region)} attempts to
     * invoke {@code getDockingProperties()} on the {@code Dockable} to resolve
     * a {@code DockablePropertySet} instance and return from its
     * {@code getSiblingSize(String region)} method.
     * <p>
     * If the specified {@code Component} is {@code null}, no {@code Dockable}
     * can be resolved, or no value is specified in the {@code Dockable's}
     * associated {@code DockingProps} instance, then the default value of
     * {@code RegionChecker.DEFAULT_SIBLING_SIZE} is returned.
     *
     * @param c
     *            the {@code Component} whose sibling size is to be examined.
     * @param region
     *            the specified region that is to be examined.
     * @return the percentage of the specified {@code Component} allotted for
     *         the siblings within the specified region.
     * @see DockingManager#getDockable(Component)
     * @see #getSiblingPreference(Dockable, String)
     * @see Dockable#getDockingProperties()
     */
    public float getSiblingSize(Component c, String region) {
        Dockable d = DockingManager.getDockable(c);
        return getSiblingPreference(d, region);
    }

    protected static float getDockingInset(Float value, float defaultVal,
                                           float max, float min) {
        float f = value == null ? -1 : value.floatValue();
        if (f == -1)
            f = defaultVal;
        return checkBounds(f, max, min);
    }

    protected static float checkBounds(float val, float max, float min) {
        val = Math.min(val, max);
        return Math.max(val, min);
    }

    /**
     * Returns {@code size} if it is between the values
     * {@code RegionChecker.MIN_REGION_SIZE} and
     * {@code RegionChecker.MAX_REGION_SIZE}. If {@code size} is less than
     * {@code RegionChecker.MIN_REGION_SIZE}, then
     * {@code RegionChecker.MIN_REGION_SIZE} is returned. If {@code size} is
     * greater than {@code RegionChecker.MAX_REGION_SIZE}, then
     * {@code RegionChecker.MAX_REGION_SIZE} is returned.
     *
     * @return a valid {@code size} value between
     *         {@code RegionChecker.MIN_REGION_SIZE} and
     *         {@code RegionChecker.MAX_REGION_SIZE}, inclusive.
     */
    public static float validateRegionSize(float size) {
        return checkBounds(size, MAX_REGION_SIZE, MIN_REGION_SIZE);
    }

    /**
     * Returns {@code size} if it is between the values
     * {@code RegionChecker.MIN_SIBILNG_SIZE} and
     * {@code RegionChecker.MAX_SIBILNG_SIZE}. If {@code size} is less than
     * {@code RegionChecker.MIN_SIBILNG_SIZE}, then
     * {@code RegionChecker.MIN_SIBILNG_SIZE} is returned. If {@code size} is
     * greater than {@code RegionChecker.MAX_SIBILNG_SIZE}, then
     * {@code RegionChecker.MAX_SIBILNG_SIZE} is returned.
     *
     * @return a valid {@code size} value between
     *         {@code RegionChecker.MIN_SIBILNG_SIZE} and
     *         {@code RegionChecker.MAX_SIBILNG_SIZE}, inclusive.
     */
    public static float validateSiblingSize(float size) {
        return checkBounds(size, MAX_SIBILNG_SIZE, MIN_SIBILNG_SIZE);
    }

    /**
     * Returns a percentage (0.0F through 1.0F) representing the amount of space
     * allotted for the specified region within the specified {@code Dockable}.
     * <p>
     * This method calls {@code getDockingProperties()} on the {@code Dockable}
     * to resolve a {@code DockablePropertySet} instance. It then invokes
     * {@code getRegionInset(String region)} on the {@code DockablePropertySet}
     * to retrieve the preferred region size. If the {@code Dockable} is
     * {@code null} or no region preference can be found, then the default value
     * of {@code RegionChecker.DEFAULT_REGION_SIZE} is returned. Otherwise, the
     * retrieved region preference is passed through
     * {@code validateRegionSize(float size)} and returned.
     *
     * @param d
     *            the {@code Dockable} whose region is to be checked
     * @param region
     *            the region of the specified {@code Dockable} to be checked
     * @return a percentage (0.0F through 1.0F) representing the amount of space
     *         allotted for the specified region within the specified
     *         {@code Dockable}.
     * @see Dockable#getDockingProperties()
     * @see RegionChecker#DEFAULT_REGION_SIZE
     * @see #validateRegionSize(float)
     */
    public static float getRegionPreference(Dockable d, String region) {
        Float inset = d == null ? null : d.getDockingProperties()
                      .getRegionInset(region);
        return getDockingInset(inset, DEFAULT_REGION_SIZE, MAX_REGION_SIZE,
                               MIN_REGION_SIZE);
    }

    /**
     * Returns a percentage (0.0F through 1.0F) representing the amount of space
     * allotted for sibling {@code Components} docked to the specified region
     * within the specified {@code Dockable}.
     * <p>
     * This method calls {@code getDockingProperties()} on the {@code Dockable}
     * to resolve a {@code DockablePropertySet} instance. It then invokes
     * {@code getSiblingSize(String region)} on the {@code DockablePropertySet}
     * to retrieve the preferred sibling size. If the {@code Dockable} is
     * {@code null} or no sibling preference can be found, then the default
     * value of {@code RegionChecker.DEFAULT_SIBLING_SIZE} is returned.
     * Otherwise, the retrieved region preference is passed through
     * {@code validateSiblingSize(float size)} and returned.
     *
     * @param d
     *            the {@code Dockable} whose sibling size is to be checked
     * @param region
     *            the region of the specified {@code Dockable} to be checked
     * @return a percentage (0.0F through 1.0F) representing the amount of space
     *         allotted for sibling {@code Components} docked to the specified
     *         region within the specified {@code Dockable}.
     * @see Dockable#getDockingProperties()
     * @see RegionChecker#DEFAULT_SIBLING_SIZE
     * @see #validateSiblingSize(float)
     */
    public static float getSiblingPreference(Dockable d, String region) {
        Float size = d == null ? null : d.getDockingProperties()
                     .getSiblingSize(region);
        return getDockingInset(size, DockingManager.getDefaultSiblingSize(),
                               MAX_SIBILNG_SIZE, MIN_SIBILNG_SIZE);
    }
}
