/*
 * Copyright (c) 2002-2015 JGoodies Software GmbH. All Rights Reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  o Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 *  o Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 *  o Neither the name of JGoodies Software GmbH nor the names of
 *    its contributors may be used to endorse or promote products derived
 *    from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.jgoodies.forms.layout;

import static com.jgoodies.common.base.Preconditions.checkArgument;

import java.awt.Component;
import java.awt.Container;
import java.io.Serializable;
import java.util.List;

/**
 * An implementation of the {@link Size} interface that represents constant
 * sizes described by a value and unit, for example:
 * 10&nbsp;pixel, 15&nbsp;point or 4&nbsp;dialog units.
 * You can get instances of {@code ConstantSize} using
 * the factory methods and constants in the {@link Sizes} class.
 * Logical constant sizes that vary with the current layout style
 * are delivered by the {@link com.jgoodies.forms.util.LayoutStyle} class.<p>
 *
 * This class supports different size units:
 * <table>
 * <tr><td><b>Unit</b>&nbsp;
 * </td><td>&nbsp;<b>Abbreviation</b>&nbsp;</td><td>&nbsp;
 * <b>Size</b></td></tr>
 * <tr><td>Millimeter</td><td>mm</td><td>0.1 cm</td></tr>
 * <tr><td>Centimeter</td><td>cm</td><td>10.0 mm</td></tr>
 * <tr><td>Inch</td><td>in</td><td>25.4 mm</td></tr>
 * <tr><td>DTP Point</td><td>pt</td><td>1/72 in</td></tr>
 * <tr><td>Pixel</td><td>px</td><td>1/(resolution in dpi) in</td></tr>
 * <tr><td>Dialog Unit</td><td>dlu</td><td>honors l&amp;f, resolution, and
 * dialog font size</td></tr>
 * </table><p>
 *
 * <strong>Examples:</strong><pre>
 * Sizes.ZERO;
 * Sizes.DLUX9;
 * Sizes.dluX(42);
 * Sizes.pixel(99);
 * </pre>
 *
 * @author Karsten Lentzsch
 * @version $Revision: 1.21 $
 *
 * @see	Size
 * @see	Sizes
 */
public final class ConstantSize implements Size, Serializable {

    // Public Units *********************************************************

    public static final Unit PIXEL          = new Unit("Pixel",          "px",   null,  true);
    public static final Unit POINT          = new Unit("Point",          "pt",   null,  true);
    public static final Unit DIALOG_UNITS_X = new Unit("Dialog units X", "dluX", "dlu", true);
    public static final Unit DIALOG_UNITS_Y = new Unit("Dialog units Y", "dluY", "dlu", true);
    public static final Unit MILLIMETER     = new Unit("Millimeter",     "mm",   null,  false);
    public static final Unit CENTIMETER     = new Unit("Centimeter",     "cm",   null,  false);
    public static final Unit INCH           = new Unit("Inch",           "in",   null,  false);

    public static final Unit PX             = PIXEL;
    public static final Unit PT             = POINT;
    public static final Unit DLUX           = DIALOG_UNITS_X;
    public static final Unit DLUY           = DIALOG_UNITS_Y;
    public static final Unit MM             = MILLIMETER;
    public static final Unit CM             = CENTIMETER;
    public static final Unit IN             = INCH;

    /**
     * An array of all enumeration values used to canonicalize
     * deserialized units.
     */
    private static final Unit[] VALUES =
        { PIXEL, POINT, DIALOG_UNITS_X, DIALOG_UNITS_Y, MILLIMETER, CENTIMETER, INCH};


    // Fields ***************************************************************

    private final double value;
    private final Unit    unit;


    // Instance Creation ****************************************************

    /**
     * Constructs a ConstantSize for the given size and unit.
     *
     * @param value     the size value interpreted in the given units
     * @param unit		the size's unit
     *
     * @since 1.1
     */
    public ConstantSize(int value, Unit unit) {
        this.value = value;
        this.unit  = unit;
    }


    /**
     * Constructs a ConstantSize for the given size and unit.
     *
     * @param value     the size value interpreted in the given units
     * @param unit      the size's unit
     *
     * @since 1.1
     */
    public ConstantSize(double value, Unit unit) {
        this.value = value;
        this.unit  = unit;
    }


    /**
     * Creates and returns a ConstantSize from the given encoded size
     * and unit description.
     *
     * @param encodedValueAndUnit  the size's value and unit as string,
     *                             trimmed and in lower case
     * @param horizontal		   true for horizontal, false for vertical
     * @return a constant size for the given encoding and unit description
     *
     * @throws IllegalArgumentException   if the unit requires integer
     *    but the value is not an integer
     */
    static ConstantSize valueOf(String encodedValueAndUnit, boolean horizontal) {
        String[] split = ConstantSize.splitValueAndUnit(encodedValueAndUnit);
        String encodedValue = split[0];
        String encodedUnit  = split[1];
        Unit unit = Unit.valueOf(encodedUnit, horizontal);
        double value = Double.parseDouble(encodedValue);
        if (unit.requiresIntegers) {
            checkArgument(value == (int) value,
                    "%s value %s must be an integer.", unit, encodedValue);
        }
        return new ConstantSize(value, unit);
    }


    /**
     * Creates and returns a ConstantSize for the specified size value
     * in horizontal dialog units.
     *
     * @param value	size value in horizontal dialog units
     * @return the associated Size instance
     */
    static ConstantSize dluX(int value) {
        return new ConstantSize(value, DLUX);
    }


    /**
     * Creates and returns a ConstantSize for the specified size value
     * in vertical dialog units.
     *
     * @param value    size value in vertical dialog units
     * @return the associated Size instance
     */
    static ConstantSize dluY(int value) {
        return new ConstantSize(value, DLUY);
    }


    // Accessors ************************************************************

    /**
     * Returns this size's value.
     *
     * @return the size value
     *
     * @since 1.1
     */
    public double getValue() {
        return value;
    }


    /**
     * Returns this size's unit.
     *
     * @return the size unit
     *
     * @since 1.1
     */
    public Unit getUnit() {
        return unit;
    }


    // Accessing the Value **************************************************

    /**
     * Converts the size if necessary and returns the value in pixels.
     *
     * @param component  the associated component
     * @return the size in pixels
     */
    public int getPixelSize(Component component) {
        if (unit == PIXEL) {
            return intValue();
        } else if (unit == DIALOG_UNITS_X) {
            return Sizes.dialogUnitXAsPixel(intValue(), component);
        } else if (unit == DIALOG_UNITS_Y) {
            return Sizes.dialogUnitYAsPixel(intValue(), component);
        } else if (unit == POINT) {
            return Sizes.pointAsPixel(intValue(), component);
        } else if (unit == INCH) {
            return Sizes.inchAsPixel(value, component);
        } else if (unit == MILLIMETER) {
            return Sizes.millimeterAsPixel(value, component);
        } else if (unit == CENTIMETER) {
            return Sizes.centimeterAsPixel(value, component);
        } else {
            throw new IllegalStateException("Invalid unit " + unit);
        }
    }


    // Implementing the Size Interface **************************************

    /**
     * Returns this size as pixel size. Neither requires the component
     * list nor the specified measures.<p>
     *
     * Invoked by {@link com.jgoodies.forms.layout.FormSpec} to determine
     * the size of a column or row.
     *
     * @param container       the layout container
     * @param components      the list of components used to compute the size
     * @param minMeasure      the measure that determines the minimum sizes
     * @param prefMeasure     the measure that determines the preferred sizes
     * @param defaultMeasure  the measure that determines the default sizes
     * @return the computed maximum size in pixel
     */
    @Override
	public int maximumSize(Container container,
                    List components,
                    FormLayout.Measure minMeasure,
                    FormLayout.Measure prefMeasure,
                    FormLayout.Measure defaultMeasure) {
        return getPixelSize(container);
    }


    /**
     * Describes if this Size can be compressed, if container space gets scarce.
     * Used by the FormLayout size computations in {@code #compressedSizes}
     * to check whether a column or row can be compressed or not.<p>
     *
     * ConstantSizes are incompressible.
     *
     * @return {@code false}
     *
     * @since 1.1
     */
    @Override
	public boolean compressible() {
        return false;
    }


    // Overriding Object Behavior *******************************************

    /**
     * Indicates whether some other ConstantSize is "equal to" this one.
     *
     * @param o   the Object with which to compare
     * @return {@code true} if this object is the same as the obj
     *     argument; {@code false} otherwise.
     *
     * @see     java.lang.Object#hashCode()
     * @see     java.util.Hashtable
     */
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof ConstantSize)) {
            return false;
        }
        ConstantSize size = (ConstantSize) o;
        return this.value == size.value
             && this.unit  == size.unit;
    }


    /**
     * Returns a hash code value for the object. This method is
     * supported for the benefit of hashtables such as those provided by
     * {@code java.util.Hashtable}.
     *
     * @return  a hash code value for this object.
     *
     * @see     java.lang.Object#equals(java.lang.Object)
     * @see     java.util.Hashtable
     */
    @Override
    public int hashCode() {
        return new Double(value).hashCode() + 37 * unit.hashCode();
    }


    /**
     * Returns a string representation of this size object.
     *
     * <strong>Note:</strong> This string representation may change
     * at any time. It is intended for debugging purposes. For parsing,
     * use {@link #encode()} instead.
     *
     * @return  a string representation of the constant size
     */
    @Override
    public String toString() {
        return value == intValue()
            ? Integer.toString(intValue()) + unit.abbreviation()
            : Double.toString(value) + unit.abbreviation();
    }


    /**
     * Returns a parseable string representation of this constant size.
     *
     * @return a String that can be parsed by the Forms parser
     *
     * @since 1.2
     */
    @Override
	public String encode() {
        return value == intValue()
            ? Integer.toString(intValue()) + unit.encode()
            : Double.toString(value) + unit.encode();
    }


    // Helper Code **********************************************************

    private int intValue() {
        return (int) Math.round(value);
    }


    /**
     * Splits a string that encodes size with unit into the size and unit
     * substrings. Returns an array of two strings.
     *
     * @param encodedValueAndUnit  a strings that represents a size with unit,
     *                             trimmed and in lower case
     * @return the first element is size, the second is unit
     */
    private static String[] splitValueAndUnit(String encodedValueAndUnit) {
        String[] result = new String[2];
        int len = encodedValueAndUnit.length();
        int firstLetterIndex = len;
        while (firstLetterIndex > 0
                && Character.isLetter(encodedValueAndUnit.charAt(firstLetterIndex - 1))) {
                firstLetterIndex--;
        }
        result[0] = encodedValueAndUnit.substring(0, firstLetterIndex);
        result[1] = encodedValueAndUnit.substring(firstLetterIndex);
        return result;
    }


    // Helper Class *********************************************************

    /**
     * An ordinal-based serializable typesafe enumeration for units
     * as used in instances of {@link ConstantSize}.
     */
    public static final class Unit implements Serializable {

        private final transient String name;
        private final transient String abbreviation;
        private final transient String parseAbbreviation;
                final transient boolean requiresIntegers;

        private Unit(String name, String abbreviation, String parseAbbreviation, boolean requiresIntegers) {
            this.name = name;
            this.abbreviation = abbreviation;
            this.parseAbbreviation = parseAbbreviation;
            this.requiresIntegers = requiresIntegers;
        }


        /**
         * Returns a Unit that corresponds to the specified string.
         *
         * @param name   the encoded unit, trimmed and in lower case
         * @param horizontal  true for a horizontal unit, false for vertical
         * @return the corresponding Unit
         * @throws IllegalArgumentException if no Unit exists for the string
         */
        static Unit valueOf(String name, boolean horizontal) {
            if (name.length() == 0) {
                Unit defaultUnit = Sizes.getDefaultUnit();
                if (defaultUnit != null) {
                    return defaultUnit;
                }
                return horizontal ? DIALOG_UNITS_X : DIALOG_UNITS_Y;
            } else if (name.equals("px")) {
                return PIXEL;
            } else if (name.equals("dlu")) {
                return horizontal ? DIALOG_UNITS_X : DIALOG_UNITS_Y;
            } else if (name.equals("pt")) {
                return POINT;
            } else if (name.equals("in")) {
                return INCH;
            } else if (name.equals("mm")) {
                return MILLIMETER;
            } else if (name.equals("cm")) {
                return CENTIMETER;
            } else {
                throw new IllegalArgumentException(
                    "Invalid unit name '" + name + "'. Must be one of: " +
                    "px, dlu, pt, mm, cm, in");
            }
        }


        /**
         * Returns a string representation of this unit object.
         *
         * <strong>Note:</strong> This implementation may change at any time.
         * It is intended for debugging purposes. For parsing, use
         * {@link #encode()} instead.
         *
         * @return  a string representation of the constant size
         */
        @Override
        public String toString() {
            return name;
        }


        /**
         * Returns a parseable string representation of this unit.
         *
         * @return a String that can be parsed by the Forms parser
         *
         * @since 1.2
         */
        public String encode() {
            return parseAbbreviation != null
                ? parseAbbreviation
                : abbreviation;
        }


        /**
         * Returns the first character of this Unit's name.
         * Used to identify it in short format strings.
         *
         * @return the first character of this Unit's name.
         */
        public String abbreviation() {
            return abbreviation;
        }


        // Serialization *****************************************************

        private static int nextOrdinal = 0;

        private final int ordinal = nextOrdinal++;

        private Object readResolve() {
            return VALUES[ordinal];  // Canonicalize
        }

    }


}
