/*
 * SimplyHTML, a word processor based on Java, HTML and CSS
 * Copyright (C) 2002 Ulrich Hilger
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 */
package com.lightdev.app.shtm;

import java.util.Enumeration;
import java.util.Vector;

import javax.swing.text.AttributeSet;
import javax.swing.text.html.CSS;

/**
 * A class to represent an attribute combining several other attributes.
 *
 * <p>The <a href="http://www.w3.org/TR/REC-CSS1">CSS 1.0 specification</a>
 * defines 'shorthand properties' which can hold more than
 * one value separated by blanks. Depending on the number of values inside
 * the property the values are applied following a fixed ratio.</p>
 *
 * <p>Following is an excerpt of the spec for CSS property
 * <code>border-width</code></p>
 * <pre>
 * There can be from one to four values, with the following interpretation:
 *     one value: all four border widths are set to that value
 *     two values: top and bottom border widths are set to the
 *                    first value, right and left are set to the second
 *     three values: top is set to the first, right and left are set to
 *                    the second, bottom is set to the third
 *     four values: top, right, bottom and left, respectively
 * </pre>
 *
 * <p>In SimplyHTML this spec is used on properties margin,
 * padding, border-width and border-color</p>
 *
 * @author Ulrich Hilger
 * @author Light Development
 * @author <a href="http://www.lightdev.com">http://www.lightdev.com</a>
 * @author <a href="mailto:info@lightdev.com">info@lightdev.com</a>
 * @author published under the terms and conditions of the
 *      GNU General Public License,
 *      for details see file gpl.txt in the distribution
 *      package of this software
 *
 * 
 */
class CombinedAttribute {
    /** index of top value */
    public static final int ATTR_TOP = 0;
    /** index of right value */
    public static final int ATTR_RIGHT = 1;
    /** index of bottom value */
    public static final int ATTR_BOTTOM = 2;
    /** index of left value */
    public static final int ATTR_LEFT = 3;
    /** the values of this <code>CombinedAttribute</code> */
    private final String[] values = new String[4];
    /**
     * the attribute key the values of this
     * <code>CombinedAttribute</code> belong to
     */
    private final Object attributeKey;
    /** indicates which sides were present in the attribute set */
    private final boolean[] present = new boolean[4];
    /** table with attribute names from the source attribute set */
    private final Vector aNames = new Vector();
    /** indicates if attributes of parent elements shall be used */
    private final boolean includeParents;

    /**
     * construct a <code>CombinedAttribute</code> for a certain
     * attribute out of a given set of attributes
     *
     * @param key  the attribute key to get single attribute values from
     * @param a  the set of attributes to get the attribute of type 'key'
     */
    public CombinedAttribute(final Object key, final AttributeSet a, final boolean includeParents) {
        attributeKey = key;
        this.includeParents = includeParents;
        // get names in this attribute set to filter out parent attributes later
        final Enumeration names = a.getAttributeNames();
        while (names.hasMoreElements()) {
            aNames.addElement(names.nextElement());
        }
        // now load attributes into this object
        final Object attr = a.getAttribute(key);
        if (attr != null) {
            copyValues(Util.tokenize(attr.toString(), " "));
        }
        else {
            copyValues(key, a);
        }
    }

    /**
     * copy the values for individual border settings from a given
     * set of attributes into the structure top, right, bottom, left of
     * this <code>CombinedAttribute</code>
     *
     * <p>Used in cases where attributes are not found for a 'shorthand
     * property' such as PADDING or MARGIN.</p>
     *
     * @param key  the 'shorthand property' to copy individual attributes for
     * @param a  the set of attributes to copy from
     */
    private void copyValues(final Object key, final AttributeSet a) {
        if (key.equals(CSS.Attribute.BORDER_WIDTH)) {
            setValue(ATTR_TOP, CSS.Attribute.BORDER_TOP_WIDTH, a);
            setValue(ATTR_RIGHT, CSS.Attribute.BORDER_RIGHT_WIDTH, a);
            setValue(ATTR_BOTTOM, CSS.Attribute.BORDER_BOTTOM_WIDTH, a);
            setValue(ATTR_LEFT, CSS.Attribute.BORDER_LEFT_WIDTH, a);
        }
        else if (key.equals(CSS.Attribute.PADDING)) {
            setValue(ATTR_TOP, CSS.Attribute.PADDING_TOP, a);
            setValue(ATTR_RIGHT, CSS.Attribute.PADDING_RIGHT, a);
            setValue(ATTR_BOTTOM, CSS.Attribute.PADDING_BOTTOM, a);
            setValue(ATTR_LEFT, CSS.Attribute.PADDING_LEFT, a);
        }
        else if (key.equals(CSS.Attribute.MARGIN)) {
            setValue(ATTR_TOP, CSS.Attribute.MARGIN_TOP, a);
            setValue(ATTR_RIGHT, CSS.Attribute.MARGIN_RIGHT, a);
            setValue(ATTR_BOTTOM, CSS.Attribute.MARGIN_BOTTOM, a);
            setValue(ATTR_LEFT, CSS.Attribute.MARGIN_LEFT, a);
        }
    }

    /**
     * set the value of a certain side from a given attribute key and
     * set of attributes.
     *
     * @param side  the side to set the value for, one of ATTR_TOP,
     *    ATTR_RIGHT, ATTR_BOTTOM and ATTR_LEFT
     * @param key  the attribute key to get the value from
     * @param a  the set of attributes to get the value from
     */
    private void setValue(final int side, final Object key, final AttributeSet a) {
        if (includeParents || aNames.contains(key)) { // filter out parent attributes
            final Object attr = a.getAttribute(key);
            values[side] = attr != null ? attr.toString() : defaultValue(attributeKey);
            present[side] = true;
        } else {
            values[side] = defaultValue(attributeKey);
            present[side] = false;
        }
    }

    /**
     * determine whether or not the set of attributes this
     * <code>CombinedAttribute</code> was created from contained any
     * of the attributes in this <code>CombinedAttribute</code>
     *
     * <p>Can be used for instance to determine whether or not this
     * <code>CombinedAttribute</code> should be written</p>
     *
     * @return true, if this <code>CombinedAttribute</code> contains
     * default values only, false if not
     */
    public boolean isEmpty() {
        boolean notEmpty = false;
        int i = 0;
        while (!notEmpty && i < present.length) {
            notEmpty = present[i];
            i++;
        }
        return !notEmpty;
    }

    /**
     * get the default value for a given key
     *
     * @param key  the attribute key to get the default value for
     *
     * @return the default value for the given key
     */
    private String defaultValue(final Object key) {
        String value = "0";
        if (key.equals(CSS.Attribute.BORDER_COLOR)) {
            value = "#000000";
        }
        return value;
    }

    /**
     * get the side opposite of a given side
     *
     * @param side  the side to get the opposite of
     *
     * @return the opposite side of the given side
     */
    public int getOppositeSide(final int side) {
        int oppositeSide = -1;
        switch (side) {
            case ATTR_TOP:
                oppositeSide = ATTR_BOTTOM;
                break;
            case ATTR_RIGHT:
                oppositeSide = ATTR_LEFT;
                break;
            case ATTR_BOTTOM:
                oppositeSide = ATTR_TOP;
                break;
            case ATTR_LEFT:
                oppositeSide = ATTR_RIGHT;
                break;
        }
        return oppositeSide;
    }

    /**
     * copy the attribute value(s) found in a 'shorthand property' such
     * as PADDING or MARGIN into the structure top, right, bottom, left of
     * this <code>CombinedAttribute</code>
     *
     * @param v  the array of Strings holding the found values
     */
    private void copyValues(final String[] v) {
        switch (v.length) {
            case 1:
                values[0] = v[0];
                break;
            case 2:
                values[ATTR_TOP] = v[ATTR_TOP];
                values[ATTR_RIGHT] = v[ATTR_RIGHT];
                values[ATTR_BOTTOM] = v[ATTR_TOP];
                values[ATTR_LEFT] = v[ATTR_RIGHT];
                break;
            case 3:
                values[ATTR_TOP] = v[ATTR_TOP];
                values[ATTR_RIGHT] = v[ATTR_RIGHT];
                values[ATTR_BOTTOM] = v[ATTR_BOTTOM];
                values[ATTR_LEFT] = v[ATTR_RIGHT];
                break;
            case 4:
                System.arraycopy(v, 0, values, 0, values.length);
                break;
        }
    }

    /**
     * set one attribute of this <code>CombinedAttribute</code>
     *
     * @param side  the side to set the attribute for, one of ATTR_TOP,
     *   ATTR_RIGHT, ATTR_BOTTOM, ATTR_LEFT
     * @param value  the attribute value to set
     */
    public void setAttribute(final int side, final String value) {
        values[side] = value;
    }

    /**
     * get one attribute of this <code>CombinedAttribute</code>
     *
     * @param side  the side to get the attribute for, one of ATTR_TOP,
     *   ATTR_RIGHT, ATTR_BOTTOM, ATTR_LEFT
     *
     * @return  the attribute value for the specified side or null, if the
     *    attribute key provided in the constructor was not found
     */
    public String getAttribute(final int side) {
        return values[side];
    }

    /**
     * get the attribute key this <code>CombinedAttribute</code> represents
     *
     * @return the attribute key
     */
    public Object getAttributeKey() {
        return attributeKey;
    }

    /**
     * get all values of this <code>CombinedAttribute</code>
     * as one attribute.
     *
     * @return a String having all values delimited by blanks
     *     in the order top right, bottom, left or null if no
     *     attributes were found
     */
    public String getAttribute() {
        String result = null;
        final StringBuffer buf = new StringBuffer();
        if (values[0] != null) {
            buf.append(values[0]);
            int additionalValueCount = 3;
            if (values[ATTR_RIGHT].equalsIgnoreCase(values[ATTR_LEFT])) {
                --additionalValueCount; // total 3
                if (values[ATTR_TOP].equalsIgnoreCase(values[ATTR_BOTTOM])) {
                    --additionalValueCount; // total 2
                    if (values[ATTR_TOP].equalsIgnoreCase(values[ATTR_RIGHT])) {
                        --additionalValueCount; // total 1
                    }
                }
            }
            appendValues(buf, additionalValueCount);
            result = buf.toString();
        }
        return result;
    }

    /**
     * append a given number of values to a given output buffer
     * starting with ATTR_RIGHT and necessarily continuing
     * with ATTR_BOTTOM and ATTR_LEFT ( helper method to getAttribute() )
     *
     * @param buf  the output buffer to append to
     * @param count  the number of values to append
     */
    private void appendValues(final StringBuffer buf, final int count) {
        for (int i = 1; i < count + 1; i++) {
            buf.append(' ');
            buf.append(values[i]);
        }
    }
}
