package api.mig.layout;

/*
 * License (BSD):
 * ==============
 *
 * Copyright (c) 2004, Mikael Grev, MiG InfoCom AB. (miglayout (at) miginfocom (dot) com)
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 * Redistributions of source code must retain the above copyright notice, this list
 * of conditions and the following disclaimer.
 * 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.
 * Neither the name of the MiG InfoCom AB 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.
 *
 * @version 1.0
 * @author Mikael Grev, MiG InfoCom AB
 *         Date: 2006-sep-08
 */

import java.beans.Encoder;
import java.beans.Expression;
import java.beans.PersistenceDelegate;
import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;

public final class UnitValue implements Serializable {

	private static final HashMap<String, Integer> UNIT_MAP = new HashMap<String, Integer>(32);

	private static final ArrayList<UnitConverter> CONVERTERS = new ArrayList<UnitConverter>();

	/**
	 * An operation indicating a static value.
	 */
	public static final int STATIC = 100;

	/**
	 * An operation indicating a addition of two sub units.
	 */
	public static final int ADD = 101; // Must have "sub-unit values"

	/**
	 * An operation indicating a subtraction of two sub units
	 */
	public static final int SUB = 102; // Must have "sub-unit values"

	/**
	 * An operation indicating a multiplication of two sub units.
	 */
	public static final int MUL = 103; // Must have "sub-unit values"

	/**
	 * An operation indicating a division of two sub units.
	 */
	public static final int DIV = 104; // Must have "sub-unit values"

	/**
	 * An operation indicating the minimum of two sub units
	 */
	public static final int MIN = 105; // Must have "sub-unit values"

	/**
	 * An operation indicating the maximum of two sub units
	 */
	public static final int MAX = 106; // Must have "sub-unit values"

	/**
	 * An operation indicating the middle value of two sub units
	 */
	public static final int MID = 107; // Must have "sub-unit values"

	/**
	 * A unit indicating pixels.
	 */
	public static final int PIXEL = 0;

	/**
	 * A unit indicating logical horizontal pixels.
	 */
	public static final int LPX = 1;

	/**
	 * A unit indicating logical vertical pixels.
	 */
	public static final int LPY = 2;

	/**
	 * A unit indicating millimeters.
	 */
	public static final int MM = 3;

	/**
	 * A unit indicating centimeters.
	 */
	public static final int CM = 4;

	/**
	 * A unit indicating inches.
	 */
	public static final int INCH = 5;

	/**
	 * A unit indicating percent.
	 */
	public static final int PERCENT = 6;

	/**
	 * A unit indicating points.
	 */
	public static final int PT = 7;

	/**
	 * A unit indicating screen percentage width.
	 */
	public static final int SPX = 8;

	/**
	 * A unit indicating screen percentage height.
	 */
	public static final int SPY = 9;

	/**
	 * A unit indicating alignment.
	 */
	public static final int ALIGN = 12;

	/**
	 * A unit indicating minimum size.
	 */
	public static final int MIN_SIZE = 13;

	/**
	 * A unit indicating preferred size.
	 */
	public static final int PREF_SIZE = 14;

	/**
	 * A unit indicating maximum size.
	 */
	public static final int MAX_SIZE = 15;

	/**
	 * A unit indicating botton size.
	 */
	public static final int BUTTON = 16;

	/**
	 * A unit indicating linking to x.
	 */
	public static final int LINK_X = 18;   // First link

	/**
	 * A unit indicating linking to y.
	 */
	public static final int LINK_Y = 19;

	/**
	 * A unit indicating linking to width.
	 */
	public static final int LINK_W = 20;

	/**
	 * A unit indicating linking to height.
	 */
	public static final int LINK_H = 21;

	/**
	 * A unit indicating linking to x2.
	 */
	public static final int LINK_X2 = 22;

	/**
	 * A unit indicating linking to y2.
	 */
	public static final int LINK_Y2 = 23;

	/**
	 * A unit indicating linking to x position on screen.
	 */
	public static final int LINK_XPOS = 24;

	/**
	 * A unit indicating linking to y position on screen.
	 */
	public static final int LINK_YPOS = 25;    // Last link

	/**
	 * A unit indicating a lookup.
	 */
	public static final int LOOKUP = 26;

	/**
	 * A unit indicating label alignment.
	 */
	public static final int LABEL_ALIGN = 27;

	private static final int IDENTITY = -1;

	static {
		UNIT_MAP.put("px", PIXEL);
		UNIT_MAP.put("lpx", LPX);
		UNIT_MAP.put("lpy", LPY);
		UNIT_MAP.put("%", PERCENT);
		UNIT_MAP.put("cm", CM);
		UNIT_MAP.put("in", INCH);
		UNIT_MAP.put("spx", SPX);
		UNIT_MAP.put("spy", SPY);
		UNIT_MAP.put("al", ALIGN);
		UNIT_MAP.put("mm", MM);
		UNIT_MAP.put("pt", PT);
		UNIT_MAP.put("min", MIN_SIZE);
		UNIT_MAP.put("minimum", MIN_SIZE);
		UNIT_MAP.put("p", PREF_SIZE);
		UNIT_MAP.put("pref", PREF_SIZE);
		UNIT_MAP.put("max", MAX_SIZE);
		UNIT_MAP.put("maximum", MAX_SIZE);
		UNIT_MAP.put("button", BUTTON);
		UNIT_MAP.put("label", LABEL_ALIGN);
	}

	static final UnitValue ZERO = new UnitValue(0, null, PIXEL, true, STATIC, null, null, "0px");
	static final UnitValue TOP = new UnitValue(0, null, PERCENT, false, STATIC, null, null, "top");
	static final UnitValue LEADING = new UnitValue(0, null, PERCENT, true, STATIC, null, null, "leading");
	static final UnitValue LEFT = new UnitValue(0, null, PERCENT, true, STATIC, null, null, "left");
	static final UnitValue CENTER = new UnitValue(50, null, PERCENT, true, STATIC, null, null, "center");
	static final UnitValue TRAILING = new UnitValue(100, null, PERCENT, true, STATIC, null, null, "trailing");
	static final UnitValue RIGHT = new UnitValue(100, null, PERCENT, true, STATIC, null, null, "right");
	static final UnitValue BOTTOM = new UnitValue(100, null, PERCENT, false, STATIC, null, null, "bottom");
	static final UnitValue LABEL = new UnitValue(0, null, LABEL_ALIGN, false, STATIC, null, null, "label");

	static final UnitValue INF = new UnitValue(LayoutUtil.INF, null, PIXEL, true, STATIC, null, null, "inf");

	static final UnitValue BASELINE_IDENTITY = new UnitValue(0, null, IDENTITY, false, STATIC, null, null, "baseline");

	private final transient float value;
	private final transient int unit;
	private final transient int oper;
	private final transient String unitStr;
	private transient String linkId = null; // Should be final, but initializes in a sub method.
	private final transient boolean isHor;
	private final transient UnitValue[] subUnits;

	// Pixel
	public UnitValue(float value) // If hor/ver does not matter.
	{
		this(value, null, PIXEL, true, STATIC, null, null, value + "px");
	}

	public UnitValue(float value, int unit, String createString) // If hor/ver does not matter.
	{
		this(value, null, unit, true, STATIC, null, null, createString);
	}

	UnitValue(float value, String unitStr, boolean isHor, int oper, String createString) {
		this(value, unitStr, -1, isHor, oper, null, null, createString);
	}

	UnitValue(boolean isHor, int oper, UnitValue sub1, UnitValue sub2, String createString) {
		this(0, "", -1, isHor, oper, sub1, sub2, createString);
		if (sub1 == null || sub2 == null) {
			throw new IllegalArgumentException("Sub units is null!");
		}
	}

	private UnitValue(float value, String unitStr, int unit, boolean isHor, int oper, UnitValue sub1, UnitValue sub2, String createString) {
		if (oper < STATIC || oper > MID) {
			throw new IllegalArgumentException("Unknown Operation: " + oper);
		}

		if (oper >= ADD && oper <= MID && (sub1 == null || sub2 == null)) {
			throw new IllegalArgumentException(oper + " Operation may not have null sub-UnitValues.");
		}

		this.value = value;
		this.oper = oper;
		this.isHor = isHor;
		this.unitStr = unitStr;
		this.unit = unitStr != null ? parseUnitString() : unit;
		this.subUnits = sub1 != null && sub2 != null ? new UnitValue[]{sub1, sub2} : null;

		LayoutUtil.putCCString(this, createString);    // "this" escapes!! Safe though.
	}

	/**
	 * Returns the size in pixels rounded.
	 *
	 * @param refValue The reference value. Normally the size of the parent. For unit {@link #ALIGN}
	 * the current size of the component should be sent in.
	 * @param parent The parent. May be <code>null</code> for testing the validity of the value, but
	 * should normally not and are not required to return any usable value if <code>null</code>.
	 * @param comp The component, if any, that the value is for. Might be <code>null</code> if the
	 * value is not connected to any component.
	 * @return The size in pixels.
	 */
	public final int getPixels(float refValue, ContainerWrapper parent, ComponentWrapper comp) {
		return Math.round(getPixelsExact(refValue, parent, comp));
	}

	private static final float[] SCALE = new float[]{25.4f, 2.54f, 1f, 0f, 72f};

	/**
	 * Returns the size in pixels.
	 *
	 * @param refValue The reference value. Normally the size of the parent. For unit {@link #ALIGN}
	 * the current size of the component should be sent in.
	 * @param parent The parent. May be <code>null</code> for testing the validity of the value, but
	 * should normally not and are not required to return any usable value if <code>null</code>.
	 * @param comp The component, if any, that the value is for. Might be <code>null</code> if the
	 * value is not connected to any component.
	 * @return The size in pixels.
	 */
	public final float getPixelsExact(float refValue, ContainerWrapper parent, ComponentWrapper comp) {
		if (parent == null) {
			return 1;
		}

		if (oper == STATIC) {
			switch (unit) {
				case PIXEL:
					return value;

				case LPX:
				case LPY:
					return parent.getPixelUnitFactor(unit == LPX) * value;

				case MM:
				case CM:
				case INCH:
				case PT:
					float f = SCALE[unit - MM];
					Float s = isHor ? PlatformDefaults.getHorizontalScaleFactor() : PlatformDefaults.getVerticalScaleFactor();
					if (s != null) {
						f *= s;
					}
					return (isHor ? parent.getHorizontalScreenDPI() : parent.getVerticalScreenDPI()) * value / f;

				case PERCENT:
					return value * refValue * 0.01f;

				case SPX:
				case SPY:
					return (unit == SPX ? parent.getScreenWidth() : parent.getScreenHeight()) * value * 0.01f;

				case ALIGN:
					Integer st = LinkHandler.getValue(parent.getLayout(), "visual", isHor ? LinkHandler.X : LinkHandler.Y);
					Integer sz = LinkHandler.getValue(parent.getLayout(), "visual", isHor ? LinkHandler.WIDTH : LinkHandler.HEIGHT);
					if (st == null || sz == null) {
						return 0;
					}
					return value * (Math.max(0, sz.intValue()) - refValue) + st;

				case MIN_SIZE:
					if (comp == null) {
						return 0;
					}
					return isHor ? comp.getMinimumWidth(comp.getHeight()) : comp.getMinimumHeight(comp.getWidth());

				case PREF_SIZE:
					if (comp == null) {
						return 0;
					}
					return isHor ? comp.getPreferredWidth(comp.getHeight()) : comp.getPreferredHeight(comp.getWidth());

				case MAX_SIZE:
					if (comp == null) {
						return 0;
					}
					return isHor ? comp.getMaximumWidth(comp.getHeight()) : comp.getMaximumHeight(comp.getWidth());

				case BUTTON:
					return PlatformDefaults.getMinimumButtonWidth().getPixels(refValue, parent, comp);

				case LINK_X:
				case LINK_Y:
				case LINK_W:
				case LINK_H:
				case LINK_X2:
				case LINK_Y2:
				case LINK_XPOS:
				case LINK_YPOS:
					Integer v = LinkHandler.getValue(parent.getLayout(), getLinkTargetId(), unit - (unit >= LINK_XPOS ? LINK_XPOS : LINK_X));
					if (v == null) {
						return 0;
					}

					if (unit == LINK_XPOS) {
						return parent.getScreenLocationX() + v;
					}
					if (unit == LINK_YPOS) {
						return parent.getScreenLocationY() + v;
					}

					return v;

				case LOOKUP:
					float res = lookup(refValue, parent, comp);
					if (res != UnitConverter.UNABLE) {
						return res;
					}

				case LABEL_ALIGN:
					return PlatformDefaults.getLabelAlignPercentage() * refValue;

				case IDENTITY:
			}
			throw new IllegalArgumentException("Unknown/illegal unit: " + unit + ", unitStr: " + unitStr);
		}

		if (subUnits != null && subUnits.length == 2) {
			float r1 = subUnits[0].getPixelsExact(refValue, parent, comp);
			float r2 = subUnits[1].getPixelsExact(refValue, parent, comp);
			switch (oper) {
				case ADD:
					return r1 + r2;
				case SUB:
					return r1 - r2;
				case MUL:
					return r1 * r2;
				case DIV:
					return r1 / r2;
				case MIN:
					return r1 < r2 ? r1 : r2;
				case MAX:
					return r1 > r2 ? r1 : r2;
				case MID:
					return (r1 + r2) * 0.5f;
			}
		}

		throw new IllegalArgumentException("Internal: Unknown Oper: " + oper);
	}

	private float lookup(float refValue, ContainerWrapper parent, ComponentWrapper comp) {
		float res = UnitConverter.UNABLE;
		for (int i = CONVERTERS.size() - 1; i >= 0; i--) {
			res = CONVERTERS.get(i).convertToPixels(value, unitStr, isHor, refValue, parent, comp);
			if (res != UnitConverter.UNABLE) {
				return res;
			}
		}
		return PlatformDefaults.convertToPixels(value, unitStr, isHor, refValue, parent, comp);
	}

	private int parseUnitString() {
		int len = unitStr.length();
		if (len == 0) {
			return isHor ? PlatformDefaults.getDefaultHorizontalUnit() : PlatformDefaults.getDefaultVerticalUnit();
		}

		Integer u = UNIT_MAP.get(unitStr);
		if (u != null) {
			return u;
		}

		if (unitStr.equals("lp")) {
			return isHor ? LPX : LPY;
		}

		if (unitStr.equals("sp")) {
			return isHor ? SPX : SPY;
		}

		if (lookup(0, null, null) != UnitConverter.UNABLE) // To test so we can fail fast
		{
			return LOOKUP;
		}

		// Only link left. E.g. "otherID.width"
		int pIx = unitStr.indexOf('.');
		if (pIx != -1) {
			linkId = unitStr.substring(0, pIx);
			String e = unitStr.substring(pIx + 1);

			if (e.equals("x")) {
				return LINK_X;
			}
			if (e.equals("y")) {
				return LINK_Y;
			}
			if (e.equals("w") || e.equals("width")) {
				return LINK_W;
			}
			if (e.equals("h") || e.equals("height")) {
				return LINK_H;
			}
			if (e.equals("x2")) {
				return LINK_X2;
			}
			if (e.equals("y2")) {
				return LINK_Y2;
			}
			if (e.equals("xpos")) {
				return LINK_XPOS;
			}
			if (e.equals("ypos")) {
				return LINK_YPOS;
			}
		}

		throw new IllegalArgumentException("Unknown keyword: " + unitStr);
	}

	final boolean isLinked() {
		return linkId != null;
	}

	final boolean isLinkedDeep() {
		if (subUnits == null) {
			return linkId != null;
		}

		for (UnitValue subUnit : subUnits) {
			if (subUnit.isLinkedDeep()) {
				return true;
			}
		}

		return false;
	}

	final String getLinkTargetId() {
		return linkId;
	}

	final UnitValue getSubUnitValue(int i) {
		return subUnits[i];
	}

	final int getSubUnitCount() {
		return subUnits != null ? subUnits.length : 0;
	}

	public final UnitValue[] getSubUnits() {
		return subUnits != null ? subUnits.clone() : null;
	}

	public final int getUnit() {
		return unit;
	}

	public final String getUnitString() {
		return unitStr;
	}

	public final int getOperation() {
		return oper;
	}

	public final float getValue() {
		return value;
	}

	public final boolean isHorizontal() {
		return isHor;
	}

	final public String toString() {
		return getClass().getName() + ". Value=" + value + ", unit=" + unit + ", unitString: " + unitStr + ", oper=" + oper + ", isHor: " + isHor;
	}

	/**
	 * Returns the creation string for this object. Note that
	 * {@link LayoutUtil#setDesignTime(ContainerWrapper, boolean)} must be set to <code>true</code>
	 * for the creation strings to be stored.
	 *
	 * @return The constraint string or <code>null</code> if none is registered.
	 */
	public final String getConstraintString() {
		return LayoutUtil.getCCString(this);
	}

	public final int hashCode() {
		return (int) (value * 12345) + (oper >>> 5) + unit >>> 17;
	}

	/**
	 * Adds a global unit converter that can convert from some <code>unit</code> to pixels.
	 * <p>
	 * This converter will be asked before the platform converter so the values for it (e.g.
	 * "related" and "unrelated") can be overridden. It is however not possible to override the
	 * built in ones (e.g. "mm", "pixel" or "lp").
	 *
	 * @param conv The converter. Not <code>null</code>.
	 */
	public synchronized static void addGlobalUnitConverter(UnitConverter conv) {
		if (conv == null) {
			throw new NullPointerException();
		}
		CONVERTERS.add(conv);
	}

	/**
	 * Removed the converter.
	 *
	 * @param unit The converter.
	 * @return If there was a converter found and thus removed.
	 */
	public synchronized static boolean removeGlobalUnitConverter(UnitConverter unit) {
		return CONVERTERS.remove(unit);
	}

	/**
	 * Returns the global converters currently registered. The platform converter will not be in
	 * this list.
	 *
	 * @return The converters. Never <code>null</code>.
	 */
	public synchronized static UnitConverter[] getGlobalUnitConverters() {
		return CONVERTERS.toArray(new UnitConverter[CONVERTERS.size()]);
	}

	/**
	 * Returns the current default unit. The default unit is the unit used if no unit is set. E.g.
	 * "width 10".
	 *
	 * @return The current default unit.
	 * @see #PIXEL
	 * @see #LPX
	 */
	public static int getDefaultUnit() {
		return PlatformDefaults.getDefaultHorizontalUnit();
	}

	/**
	 * Sets the default unit. The default unit is the unit used if no unit is set. E.g. "width 10".
	 *
	 * @param unit The new default unit.
	 * @see #PIXEL
	 * @see #LPX
	 */
	public static void setDefaultUnit(int unit) {
		PlatformDefaults.setDefaultHorizontalUnit(unit);
		PlatformDefaults.setDefaultVerticalUnit(unit);
	}

	static {
		if (LayoutUtil.HAS_BEANS) {
			LayoutUtil.setDelegate(UnitValue.class, new PersistenceDelegate() {
				protected Expression instantiate(Object oldInstance, Encoder out) {
					UnitValue uv = (UnitValue) oldInstance;
					String cs = uv.getConstraintString();
					if (cs == null) {
						throw new IllegalStateException("Design time must be on to use XML persistence. See LayoutUtil.");
					}

					return new Expression(oldInstance, ConstraintParser.class, "parseUnitValueOrAlign", new Object[]{
						uv.getConstraintString(), (uv.isHorizontal() ? Boolean.TRUE : Boolean.FALSE), null
					});
				}
			});
		}
	}

	// ************************************************
	// Persistence Delegate and Serializable combined.
	// ************************************************
	private static final long serialVersionUID = 1L;

	private Object readResolve() throws ObjectStreamException {
		return LayoutUtil.getSerializedObject(this);
	}

	private void writeObject(ObjectOutputStream out) throws IOException {
		if (getClass() == UnitValue.class) {
			LayoutUtil.writeAsXML(out, this);
		}
	}

	private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
		LayoutUtil.setSerializedObject(this, LayoutUtil.readAsXML(in));
	}
}
