package mage.client.components.layout;

import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Insets;
import java.awt.LayoutManager2;
import java.util.HashMap;
import java.util.Map;

/**
 *  The <code>RelativeLayout</code> class is a layout manager that
 *  lays out a container's components on the specified X or Y axis.
 *
 *  Components can be layed out at their preferred size or at a
 *  relative size. When relative sizing is used the component must be added
 *  to the container using a relative size constraint, which is simply a
 *  Float value.
 *
 *  The space available for relative sized components is determined by
 *  subtracting the preferred size of the other components from the space
 *  available in the container. Each component is then assigned a size
 *  based on its relative size value. For example:
 *
 *  container.add(component1, new Float(1));
 *  container.add(component2, new Float(2));
 *
 *  There is a total of 3 relative units. If the container has 300 pixels
 *  of space available then component1 will get 100 and component2, 200.
 *
 *  It is possible that rounding errors will occur in which case you can
 *  specify a rounding policy to use to allocate the extra pixels.
 *
 *  By defaults components are center aligned on the secondary axis
 *  however this can be changed at the container or component level.
 */
public class RelativeLayout implements LayoutManager2, java.io.Serializable {

    private static final long serialVersionUID = 1L;
    //  Used in the constructor
    public static final int X_AXIS = 0;
    public static final int Y_AXIS = 1;

    //  See setAlignment() method
    public static final float LEADING = 0.0f;
    public static final float CENTER = 0.5f;
    public static final float TRAILING = 1.0f;
    public static final float COMPONENT = -1.0f;

    //  See setRoundingPolicy() method
    public static final int DO_NOTHING = 0;
    public static final int FIRST = 1;
    public static final int LAST = 2;
    public static final int LARGEST = 3;
    public static final int EQUAL = 4;

    private static final int MINIMUM = 0;
    private static final int PREFERRED = 1;

    private final Map<Component, Float> constraints = new HashMap<>();

    /**
     * The axis of the Components within the Container.
     */
    private int axis;

    /**
     * The alignment of the Components on the other axis of the Container.
     * For X-AXIS this would refer to the Y alignment.
     * For Y-AXIS this would refer to the X alignment.
     */
    private float alignment = CENTER;

    /**
     * This is the gap (in pixels) which specifies the space between components
     * It can be changed at any time and should be a non-negative integer.
     */
    private int gap;

    /**
     * The gap (in pixels) used before the leading component and after the trailing component.
     * It can be changed at any time and should be a non-negative integer.
     */
    private int borderGap;

    //  Fill space available for relative components
    private boolean fill = false;

    //  Gap to prevent the component from completely filling the space available
    private int fillGap;

    //  Specify the rounding policy when rounding problems happen.
    private int roundingPolicy = LARGEST;

    /**
     * Creates a relative layout with the components layed out on the X-Axis using the default gap
     */
    public RelativeLayout() {
        this(X_AXIS, 0);
    }

    /**
     * Creates a relative layout with the components layed out on the specified axis
     * using the default gap
     *
     * @param axis X-AXIS or Y_AXIS
     */
    public RelativeLayout(int axis) {
        this(axis, 0);
    }

    /**
     * Creates a relative layout with the components layed out on the specified axis
     * using the specfied gap
     * <p/>
     * All <code>RelativeLayout</code> constructors defer to this one.
     *
     * @param axis X-AXIS or Y_AXIS
     * @param gap the gap
     */
    public RelativeLayout(int axis, int gap) {
        setAxis(axis);
        setGap(gap);
        setBorderGap(gap);
    }

    /**
     * Gets the layout axis.
     *
     * @return the layout axis
     */
    public int getAxis() {
        return axis;
    }

    /**
     * Sets the layout axis
     *
     * @param axis the layout axis
     */
    public final void setAxis(int axis) {
        if (axis != X_AXIS && axis != Y_AXIS) {
            throw new IllegalArgumentException("invalid axis specified");
        }

        this.axis = axis;
    }

    /**
     * Gets the gap between components.
     *
     * @return the gap between components
     */
    public int getGap() {
        return gap;
    }

    /**
     * Sets the gap between components to the specified value.
     *
     * @param gap the gap between components
     */
    public final void setGap(int gap) {
        this.gap = gap < 0 ? 0 : gap;
    }

    /**
     * Gets the initial gap. This gap is used before the leading component
     * and after the trailing component.
     *
     * @return the leading/trailing gap
     */
    public int getBorderGap() {
        return borderGap;
    }

    /**
     * Sets the initial gap. This gap is used before the leading component
     * and after the trailing component. The default is set to the gap.
     *
     * @param borderGap the leading/trailing gap
     */
    public final void setBorderGap(int borderGap) {
        this.borderGap = borderGap < 0 ? 0 : borderGap;
    }

    /**
     * Gets the alignment of the components on the opposite axis.
     *
     * @return the alignment
     */
    public float getAlignment() {
        return alignment;
    }

    /*
     *  Set the alignment of the component on the opposite axis.
     *
     *  For X-AXIS this would refer to the Y alignemt.
     *  For Y-AXIS this would refer to the X alignment.
     *
     *  Must be between 0.0 and 1.0, or -1. Values can be specified using:
     *
     *  RelativeLayout.LEADING
     *  RelativeLayout.CENTER
     *  RelativeLayout.TRAILING
     *  RelativeLayout.COMPONENT - the getAlignemntX/Y method for the opposite axis will be used
     */
    public void setAlignment(float alignment) {
        this.alignment = alignment > 1.0f ? 1.0f : alignment < 0.0f ? -1.0f : alignment;
    }

    /**
     * Gets the fill property for the component size on the opposite edge.
     *
     * @return the fill property
     */
    public boolean isFill() {
        return fill;
    }

    /**
     * Change size of relative components to fill the space available For X-AXIS aligned components the height will be
     * filled. For Y-AXIS aligned components the width will be filled.
     * @param fill
     */
    public void setFill(boolean fill) {
        this.fill = fill;
    }

    /**
     * Gets the fill gap amount.
     *
     * @return the fill gap value
     */
    public int getFillGap() {
        return fillGap;
    }

    /**
     * Specify the number of pixels by which the fill size is decreased when setFill(true) has been specified.
     * @param fillGap
     */
    public void setFillGap(int fillGap) {
        this.fillGap = fillGap;
    }

    /**
     * Gets the rounding policy.
     *
     * @return the rounding policy
     */
    public int getRoundingPolicy() {
        return roundingPolicy;
    }

    /**
     * Specify the rounding policy to be used when all the avialable pixels have not been allocated to a component.
     *
     * DO_NOTHING FIRST - extra pixels added to the first relative component LAST - extra pixels added to the last
     * relative component LARGEST (default) - extra pixels added to the larger relative component EQUAL - a single pixel
     * is added to each relative component (until pixels are used up)
     * @param roundingPolicy
     */
    public void setRoundingPolicy(int roundingPolicy) {
        this.roundingPolicy = roundingPolicy;
    }

    /**
     * Gets the constraints for the specified component.
     *
     * @param component the component to be queried
     * @return the constraint for the specified component, or null if component is null or is not present in this layout
     */
    public Float getConstraints(Component component) {
        return constraints.get(component);
    }

    /**
     * Not supported
     * @param name
     * @param component
     */
    @Override
    public void addLayoutComponent(String name, Component component) {
    }

    /*
     * Keep track of any specified constraint for the component.
     */
    @Override
    public void addLayoutComponent(Component component, Object constraint) {
        if (constraint != null) {
            if (constraint instanceof Float) {
                constraints.put(component, (Float) constraint);
            } else {
                throw new IllegalArgumentException("Constraint parameter must be of type Float");
            }
        }
    }

    /**
     * Removes the specified component from the layout.
     *
     * @param comp the component to be removed
     */
    @Override
    public void removeLayoutComponent(Component comp) {
    }

    /**
     * Determines the preferred size of the container argument using this column layout. <p> The preferred width of a
     * column layout is the largest preferred width of each column in the container, plus the horizontal padding times
     * the number of columns minus one, plus the left and right insets of the target container. <p> The preferred height
     * of a column layout is the largest preferred height of each row in the container, plus the vertical padding times
     * the number of rows minus one, plus the top and bottom insets of the target container.
     *
     * @param parent the container in which to do the layout
     * @return the preferred dimensions to lay out the subcomponents of the specified container
     * @see java.awt.RelativeLayout#minimumLayoutSize
     * @see java.awt.Container#getPreferredSize()
     */
    @Override
    public Dimension preferredLayoutSize(Container parent) {
        synchronized (parent.getTreeLock()) {
            return getLayoutSize(parent, PREFERRED);
        }
    }

    /**
     * Determines the minimum size of the container argument using this column layout. <p> The minimum width of a grid
     * layout is the largest minimum width of each column in the container, plus the horizontal padding times the number
     * of columns minus one, plus the left and right insets of the target container. <p> The minimum height of a column
     * layout is the largest minimum height of each row in the container, plus the vertical padding times the number of
     * rows minus one, plus the top and bottom insets of the target container.
     *
     * @param parent the container in which to do the layout
     * @return the minimum dimensions needed to lay out the subcomponents of the specified container
     * @see java.awt.RelativeLayout#preferredLayoutSize
     * @see java.awt.Container#doLayout
     */
    @Override
    public Dimension minimumLayoutSize(Container parent) {
        synchronized (parent.getTreeLock()) {
            return getLayoutSize(parent, MINIMUM);
        }
    }

    /**
     * Lays out the specified container using this layout. <p> This method reshapes the components in the specified
     * target container in order to satisfy the constraints of the
     * <code>RelativeLayout</code> object. <p> The grid layout manager determines the size of individual components by
     * dividing the free space in the container into equal-sized portions according to the number of rows and columns in
     * the layout. The container's free space equals the container's size minus any insets and any specified horizontal
     * or vertical gap. All components in a grid layout are given the same size.
     *
     * @param parent the container in which to do the layout
     * @see java.awt.Container
     * @see java.awt.Container#doLayout
     */
    @Override
    public void layoutContainer(Container parent) {
        synchronized (parent.getTreeLock()) {
            if (axis == X_AXIS) {
                layoutContainerHorizontally(parent);
            } else {
                layoutContainerVertically(parent);
            }
        }
    }

    /*
     *  Lay out all the components in the Container along the X-Axis
     */
    private void layoutContainerHorizontally(Container parent) {
        int components = parent.getComponentCount();
        int visibleComponents = getVisibleComponents(parent);

        if (components == 0) {
            return;
        }

        //  Determine space available for components using relative sizing

        float relativeTotal = 0.0f;
        Insets insets = parent.getInsets();
        int spaceAvailable = parent.getSize().width
                - insets.left
                - insets.right
                - ((visibleComponents - 1) * gap)
                - (2 * borderGap);

        for (int i = 0; i < components; i++) {
            Component component = parent.getComponent(i);

            if (!component.isVisible()) {
                continue;
            }

            Float constraint = constraints.get(component);

            if (constraint == null) {
                Dimension d = component.getPreferredSize();
                spaceAvailable -= d.width;
            } else {
                relativeTotal += constraint.doubleValue();
            }
        }

        //  Allocate space to each component using relative sizing

        int[] relativeSpace = allocateRelativeSpace(parent, spaceAvailable, relativeTotal);

        //  Position each component in the container

        int x = insets.left + borderGap;
        int y = insets.top;
        int insetGap = insets.top + insets.bottom;
        int parentHeight = parent.getSize().height - insetGap;

        for (int i = 0; i < components; i++) {
            Component component = parent.getComponent(i);

            if (!component.isVisible()) {
                continue;
            }

            if (i > 0) {
                x += gap;
            }

            Dimension d = component.getPreferredSize();

            if (fill) {
                d.height = parentHeight - fillGap;
            }

            Float constraint = constraints.get(component);

            if (constraint == null) {
                component.setSize(d);
                int locationY = getLocationY(component, parentHeight) + y;
                component.setLocation(x, locationY);
                x += d.width;
            } else {
                int width = relativeSpace[i];
                component.setSize(width, d.height);
                int locationY = getLocationY(component, parentHeight) + y;
                component.setLocation(x, locationY);
                x += width;
            }
        }
    }

    /*
     *  Align the component on the Y-Axis
     */
    private int getLocationY(Component component, int height) {
        //  Use the Container alignment policy

        float alignmentY = alignment;

        //  Override with the Component alignment

        if (alignmentY == COMPONENT) {
            alignmentY = component.getAlignmentY();
        }

        return (int) ((height - component.getSize().height) * alignmentY);
    }

    /*
     *  Lay out all the components in the Container along the Y-Axis
     */
    private void layoutContainerVertically(Container parent) {
        int components = parent.getComponentCount();
        int visibleComponents = getVisibleComponents(parent);

        if (components == 0) {
            return;
        }

        //  Determine space available for components using relative sizing

        float relativeTotal = 0.0f;
        Insets insets = parent.getInsets();
        int spaceAvailable = parent.getSize().height
                - insets.top
                - insets.bottom
                - ((visibleComponents - 1) * gap)
                - (2 * borderGap);

        for (int i = 0; i < components; i++) {
            Component component = parent.getComponent(i);

            if (!component.isVisible()) {
                continue;
            }

            Float constraint = constraints.get(component);

            if (constraint == null) {
                Dimension d = component.getPreferredSize();
                spaceAvailable -= d.height;
            } else {
                relativeTotal += constraint.doubleValue();
            }
        }

        //  Allocate space to each component using relative sizing

        int[] relativeSpace = allocateRelativeSpace(parent, spaceAvailable, relativeTotal);

        //  Position each component in the container

        int x = insets.left;
        int y = insets.top + borderGap;
        int insetGap = insets.left + insets.right;
        int parentWidth = parent.getSize().width - insetGap;

        for (int i = 0; i < components; i++) {
            Component component = parent.getComponent(i);

            if (!component.isVisible()) {
                continue;
            }

            if (i > 0) {
                y += gap;
            }

            Dimension d = component.getPreferredSize();

            if (fill) {
                d.width = parentWidth - fillGap;
            }

            Float constraint = constraints.get(component);

            if (constraint == null) {
                component.setSize(d);
                int locationX = getLocationX(component, parentWidth) + x;
                component.setLocation(locationX, y);
                y += d.height;
            } else {
                int height = relativeSpace[i];
                component.setSize(d.width, height);
                int locationX = getLocationX(component, parentWidth) + x;
                component.setLocation(locationX, y);
                y += height;
            }

        }
    }

    /*
     *  Align the component on the X-Axis
     */
    private int getLocationX(Component component, int width) {
        //  Use the Container alignment policy

        float alignmentX = alignment;

        //  Override with the Component alignment

        if (alignmentX == COMPONENT) {
            alignmentX = component.getAlignmentX();
        }

        return (int) ((width - component.getSize().width) * alignmentX);
    }

    /*
     *  Allocate the space available to each component using relative sizing
     */
    private int[] allocateRelativeSpace(Container parent, int spaceAvailable, float relativeTotal) {
        int spaceUsed = 0;
        int components = parent.getComponentCount();
        int[] relativeSpace = new int[components];

        for (int i = 0; i < components; i++) {
            relativeSpace[i] = 0;

            if (relativeTotal > 0 && spaceAvailable > 0) {
                Component component = parent.getComponent(i);
                Float constraint = constraints.get(component);

                if (constraint != null) {
                    int space = (int) (spaceAvailable * constraint / relativeTotal);
                    relativeSpace[i] = space;
                    spaceUsed += space;
                }
            }
        }

        int spaceRemaining = spaceAvailable - spaceUsed;

        if (relativeTotal > 0 && spaceRemaining > 0) {
            adjustForRounding(relativeSpace, spaceRemaining);
        }

        return relativeSpace;
    }

    /*
     *  Because of rounding, all the space has not been allocated
     *  Override this method to create a custom rounding policy
     */
    protected void adjustForRounding(int[] relativeSpace, int spaceRemaining) {
        switch (roundingPolicy) {
            case DO_NOTHING:
                break;
            case FIRST:
                adjustFirst(relativeSpace, spaceRemaining);
                break;
            case LAST:
                adjustLast(relativeSpace, spaceRemaining);
                break;
            case LARGEST:
                adjustLargest(relativeSpace, spaceRemaining);
                break;
            case EQUAL:
                adjustEqual(relativeSpace, spaceRemaining);
                break;
            default:
                adjustLargest(relativeSpace, spaceRemaining);
        }
    }

    /*
     * First component using relative sizing gets all the space
     */
    private void adjustFirst(int[] relativeSpace, int spaceRemaining) {
        for (int i = 0; i < relativeSpace.length; i++) {
            if (relativeSpace[i] > 0) {
                relativeSpace[i] += spaceRemaining;
                break;
            }
        }
    }

    /*
     *  Last component using relative sizing gets all the space
     */
    private void adjustLast(int[] relativeSpace, int spaceRemaining) {
        for (int i = relativeSpace.length - 1; i > 0; i--) {
            if (relativeSpace[i] > 0) {
                relativeSpace[i] += spaceRemaining;
                break;
            }
        }
    }

    /*
     *  Largest component using relative sizing gets all the space.
     *  When multiple components are the same size, the last one found is used.
     */
    private void adjustLargest(int[] relativeSpace, int spaceRemaining) {
        int largest = 0;
        int largestSpace = 0;

        for (int i = 0; i < relativeSpace.length; i++) {
            int space = relativeSpace[i];

            if (space > 0 && largestSpace < space) {
                largestSpace = space;
                largest = i;
            }
        }

        relativeSpace[largest] += spaceRemaining;
    }

    /*
     *  Each component using relative sizing gets 1 more pixel
     *  until all the space is used, starting with the first.
     */
    private void adjustEqual(int[] relativeSpace, int spaceRemaining) {
        int pixelsLeft = spaceRemaining;
        for (int i = 0; i < relativeSpace.length; i++) {
            if (relativeSpace[i] > 0) {
                relativeSpace[i]++;
                pixelsLeft--;

                if (pixelsLeft == 0) {
                    break;
                }
            }
        }
    }

    /*
     *  Determine the Preferred or Minimum layout size
     */
    private Dimension getLayoutSize(Container parent, int type) {
        int width = 0;
        int height = 0;
        int components = parent.getComponentCount();
        int visibleComponents = getVisibleComponents(parent);

        for (int i = 0; i < components; i++) {
            Component component = parent.getComponent(i);

            if (!component.isVisible()) {
                continue;
            }

            Dimension d = getDimension(component, type);

            if (axis == X_AXIS) {
                width += d.width;
                height = Math.max(height, d.height);
            } else {
                width = Math.max(width, d.width);
                height += d.height;
            }
        }

        Insets insets = parent.getInsets();
        int totalGap = ((visibleComponents - 1) * gap) + (2 * borderGap);

        if (axis == X_AXIS) {
            width += insets.left + insets.right + totalGap;
            height += insets.top + insets.bottom;
        } else {
            width += insets.left + insets.right;
            height += insets.top + insets.bottom + totalGap;
        }

        return new Dimension(width, height);
    }

    private int getVisibleComponents(Container container) {
        int visibleComponents = 0;

        for (Component component : container.getComponents()) {
            if (component.isVisible()) {
                visibleComponents++;
            }
        }

        return visibleComponents;
    }

    private Dimension getDimension(Component component, int type) {
        switch (type) {
            case PREFERRED:
                return component.getPreferredSize();
            case MINIMUM:
                return component.getMinimumSize();
            default:
                return new Dimension(0, 0);
        }
    }

    /**
     * There is no maximum.
     * @param target
     * @return 
     */
    @Override
    public Dimension maximumLayoutSize(Container target) {
        return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
    }

    /**
     * Returns the alignment along the x axis. Use center alignment.
     * @param parent
     * @return 
     */
    @Override
    public float getLayoutAlignmentX(Container parent) {
        return 0.5f;
    }

    /**
     * Returns the alignment along the y axis. Use center alignment.
     * @param parent
     * @return 
     */
    @Override
    public float getLayoutAlignmentY(Container parent) {
        return 0.5f;
    }

    /**
     * Invalidates the layout, indicating that if the layout manager has cached information it should be discarded.
     * @param target
     */
    @Override
    public void invalidateLayout(Container target) {
        // remove constraints here?
    }

    /**
     * Returns the string representation of this column layout's values.
     *
     * @return a string representation of this grid layout
     */
    @Override
    public String toString() {
        return getClass().getName()
                + "[axis=" + axis
                + ",gap=" + gap
                + ']';
    }
}
