

package org.apache.poi.hpsf;

import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;

import org.apache.poi.util.HexDump;
import org.apache.poi.util.LittleEndian;
import org.apache.poi.util.POILogFactory;
import org.apache.poi.util.POILogger;

/**
 * <p>A property in a {@link Section} of a {@link PropertySet}.</p>
 *
 * <p>The property's <strong>ID</strong> gives the property a meaning
 * in the context of its {@link Section}. Each {@link Section} spans
 * its own name space of property IDs.</p>
 *
 * <p>The property's <strong>type</strong> determines how its
 * <strong>value </strong> is interpreted. For example, if the type is
 * {@link Variant#VT_LPSTR} (byte string), the value consists of a
 * DWord telling how many bytes the string contains. The bytes follow
 * immediately, including any null bytes that terminate the
 * string. The type {@link Variant#VT_I4} denotes a four-byte integer
 * value, {@link Variant#VT_FILETIME} some date and time (of a
 * file).</p>
 *
 * <p>Please note that not all {@link Variant} types yet. This might change
 * over time but largely depends on your feedback so that the POI team knows
 * which variant types are really needed. So please feel free to submit error
 * reports or patches for the types you need.</p>
 *
 * <p>Microsoft documentation: <a
 * href="http://msdn.microsoft.com/library/en-us/stg/stg/property_set_display_name_dictionary.asp?frame=true">
 * Property Set Display Name Dictionary</a>.
 *
 * @author Rainer Klute <a
 * href="mailto:klute@rainer-klute.de">&lt;klute@rainer-klute.de&gt;</a>
 * @author Drew Varner (Drew.Varner InAndAround sc.edu)
 * @see Section
 * @see Variant
 */
public class Property
{

    /** <p>The property's ID.</p> */
    protected long id;


    /**
     * <p>Returns the property's ID.</p>
     *
     * @return The ID value
     */
    public long getID()
    {
        return id;
    }



    /** <p>The property's type.</p> */
    protected long type;


    /**
     * <p>Returns the property's type.</p>
     *
     * @return The type value
     */
    public long getType()
    {
        return type;
    }



    /** <p>The property's value.</p> */
    protected Object value;


    /**
     * <p>Returns the property's value.</p>
     *
     * @return The property's value
     */
    public Object getValue()
    {
        return value;
    }



    /**
     * <p>Creates a property.</p>
     *
     * @param id the property's ID.
     * @param type the property's type, see {@link Variant}.
     * @param value the property's value. Only certain types are allowed, see
     *        {@link Variant}.
     */
    public Property(final long id, final long type, final Object value)
    {
        this.id = id;
        this.type = type;
        this.value = value;
    }



    /**
     * <p>Creates a {@link Property} instance by reading its bytes
     * from the property set stream.</p>
     *
     * @param id The property's ID.
     * @param src The bytes the property set stream consists of.
     * @param offset The property's type/value pair's offset in the
     * section.
     * @param length The property's type/value pair's length in bytes.
     * @param codepage The section's and thus the property's
     * codepage. It is needed only when reading string values.
     * @exception UnsupportedEncodingException if the specified codepage is not
     * supported.
     */
    public Property(final long id, final byte[] src, final long offset,
                    final int length, final int codepage)
    throws UnsupportedEncodingException
    {
        this.id = id;

        /*
         * ID 0 is a special case since it specifies a dictionary of
         * property IDs and property names.
         */
        if (id == 0)
        {
            value = readDictionary(src, offset, length, codepage);
            return;
        }

        int o = (int) offset;
        type = LittleEndian.getUInt(src, o);
        o += LittleEndian.INT_SIZE;

        try
        {
            value = VariantSupport.read(src, o, length, (int) type, codepage);
        }
        catch (UnsupportedVariantTypeException ex)
        {
            VariantSupport.writeUnsupportedTypeMessage(ex);
            value = ex.getValue();
        }
    }



    /**
     * <p>Creates an empty property. It must be filled using the set method to
     * be usable.</p>
     */
    protected Property()
    { }



    /**
     * <p>Reads a dictionary.</p>
     *
     * @param src The byte array containing the bytes making out the dictionary.
     * @param offset At this offset within <var>src </var> the dictionary
     *        starts.
     * @param length The dictionary contains at most this many bytes.
     * @param codepage The codepage of the string values.
     * @return The dictonary
     * @throws UnsupportedEncodingException if the dictionary's codepage is not
     *         (yet) supported.
     */
    protected Map readDictionary(final byte[] src, final long offset,
                                 final int length, final int codepage)
    throws UnsupportedEncodingException
    {
        /* Check whether "offset" points into the "src" array". */
        if (offset < 0 || offset > src.length)
            throw new HPSFRuntimeException
                ("Illegal offset " + offset + " while HPSF stream contains " +
                 length + " bytes.");
        int o = (int) offset;

        /*
         * Read the number of dictionary entries.
         */
        final long nrEntries = LittleEndian.getUInt(src, o);
        o += LittleEndian.INT_SIZE;

        final Map m = new HashMap((int) nrEntries, (float) 1.0);

        try
        {
            for (int i = 0; i < nrEntries; i++)
            {
                /* The key. */
                final Long id = Long.valueOf(LittleEndian.getUInt(src, o));
                o += LittleEndian.INT_SIZE;

                /* The value (a string). The length is the either the
                 * number of (two-byte) characters if the character set is Unicode
                 * or the number of bytes if the character set is not Unicode.
                 * The length includes terminating 0x00 bytes which we have to strip
                 * off to create a Java string. */
                long sLength = LittleEndian.getUInt(src, o);
                o += LittleEndian.INT_SIZE;

                /* Read the string. */
                final StringBuffer b = new StringBuffer();
                switch (codepage)
                {
                    case -1:
                    {
                        /* Without a codepage the length is equal to the number of
                         * bytes. */
                        b.append(new String(src, o, (int) sLength));
                        break;
                    }
                    case Constants.CP_UNICODE:
                    {
                        /* The length is the number of characters, i.e. the number
                         * of bytes is twice the number of the characters. */
                        final int nrBytes = (int) (sLength * 2);
                        final byte[] h = new byte[nrBytes];
                        for (int i2 = 0; i2 < nrBytes; i2 += 2)
                        {
                            h[i2] = src[o + i2 + 1];
                            h[i2 + 1] = src[o + i2];
                        }
                        b.append(new String(h, 0, nrBytes,
                                VariantSupport.codepageToEncoding(codepage)));
                        break;
                    }
                    default:
                    {
                        /* For encodings other than Unicode the length is the number
                         * of bytes. */
                        b.append(new String(src, o, (int) sLength,
                                 VariantSupport.codepageToEncoding(codepage)));
                        break;
                    }
                }

                /* Strip 0x00 characters from the end of the string: */
                while (b.length() > 0 && b.charAt(b.length() - 1) == 0x00)
                    b.setLength(b.length() - 1);
                if (codepage == Constants.CP_UNICODE)
                {
                    if (sLength % 2 == 1)
                        sLength++;
                    o += (sLength + sLength);
                }
                else
                    o += sLength;
                m.put(id, b.toString());
            }
        }
        catch (RuntimeException ex)
        {
            final POILogger l = POILogFactory.getLogger(getClass());
            l.log(POILogger.WARN,
                    "The property set's dictionary contains bogus data. "
                    + "All dictionary entries starting with the one with ID "
                    + id + " will be ignored.", ex);
        }
        return m;
    }



    /**
     * <p>Returns the property's size in bytes. This is always a multiple of
     * 4.</p>
     *
     * @return the property's size in bytes
     *
     * @exception WritingNotSupportedException if HPSF does not yet support the
     * property's variant type.
     */
    protected int getSize() throws WritingNotSupportedException
    {
        int length = VariantSupport.getVariantLength(type);
        if (length >= 0)
            return length; /* Fixed length */
        if (length == -2)
            /* Unknown length */
            throw new WritingNotSupportedException(type, null);

        /* Variable length: */
        final int PADDING = 4; /* Pad to multiples of 4. */
        switch ((int) type)
        {
            case Variant.VT_LPSTR:
            {
                int l = ((String) value).length() + 1;
                int r = l % PADDING;
                if (r > 0)
                    l += PADDING - r;
                length += l;
                break;
            }
            case Variant.VT_EMPTY:
                break;
            default:
                throw new WritingNotSupportedException(type, value);
        }
        return length;
    }



    /**
     * <p>Compares two properties.</p> <p>Please beware that a property with
     * ID == 0 is a special case: It does not have a type, and its value is the
     * section's dictionary. Another special case are strings: Two properties
     * may have the different types Variant.VT_LPSTR and Variant.VT_LPWSTR;</p>
     *
     * @see Object#equals(java.lang.Object)
     */
    public boolean equals(final Object o)
    {
        if (!(o instanceof Property)) {
            return false;
        }
        final Property p = (Property) o;
        final Object pValue = p.getValue();
        final long pId = p.getID();
        if (id != pId || (id != 0 && !typesAreEqual(type, p.getType())))
            return false;
        if (value == null && pValue == null)
            return true;
        if (value == null || pValue == null)
            return false;

        /* It's clear now that both values are non-null. */
        final Class<?> valueClass = value.getClass();
        final Class<?> pValueClass = pValue.getClass();
        if (!(valueClass.isAssignableFrom(pValueClass)) &&
            !(pValueClass.isAssignableFrom(valueClass)))
            return false;

        if (value instanceof byte[])
            return Util.equal((byte[]) value, (byte[]) pValue);

        return value.equals(pValue);
    }



    private boolean typesAreEqual(final long t1, final long t2)
    {
        if (t1 == t2 ||
            (t1 == Variant.VT_LPSTR && t2 == Variant.VT_LPWSTR) ||
            (t2 == Variant.VT_LPSTR && t1 == Variant.VT_LPWSTR)) {
            return true;
        }
        return false;
    }



    /**
     * @see Object#hashCode()
     */
    public int hashCode()
    {
        long hashCode = 0;
        hashCode += id;
        hashCode += type;
        if (value != null)
            hashCode += value.hashCode();
        final int returnHashCode = (int) (hashCode & 0x0ffffffffL );
        return returnHashCode;

    }



    /**
     * @see Object#toString()
     */
    public String toString()
    {
        final StringBuffer b = new StringBuffer();
        b.append(getClass().getName());
        b.append('[');
        b.append("id: ");
        b.append(getID());
        b.append(", type: ");
        b.append(getType());
        final Object value = getValue();
        b.append(", value: ");
        b.append(value.toString());
        if (value instanceof String)
        {
            final String s = (String) value;
            final int l = s.length();
            final byte[] bytes = new byte[l * 2];
            for (int i = 0; i < l; i++)
            {
                final char c = s.charAt(i);
                final byte high = (byte) ((c & 0x00ff00) >> 8);
                final byte low  = (byte) ((c & 0x0000ff) >> 0);
                bytes[i * 2]     = high;
                bytes[i * 2 + 1] = low;
            }
            final String hex = HexDump.dump(bytes, 0L, 0);
            b.append(" [");
            b.append(hex);
            b.append("]");
        }
        b.append(']');
        return b.toString();
    }

}
