/* ====================================================================
   Licensed to the Apache Software Foundation (ASF) under one or more
   contributor license agreements.  See the NOTICE file distributed with
   this work for additional information regarding copyright ownership.
   The ASF licenses this file to You under the Apache License, Version 2.0
   (the "License"); you may not use this file except in compliance with
   the License.  You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
==================================================================== */

package org.apache.poi.hpsf;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.ListIterator;

import org.apache.poi.poifs.filesystem.DirectoryEntry;
import org.apache.poi.poifs.filesystem.Entry;
import org.apache.poi.util.LittleEndian;
import org.apache.poi.util.LittleEndianConsts;

/**
 * <p>Adds writing support to the {@link PropertySet} class.</p>
 *
 * <p>Please be aware that this class' functionality will be merged into the
 * {@link PropertySet} class at a later time, so the API will change.</p>
 *
 * @author Rainer Klute <a
 * href="mailto:klute@rainer-klute.de">&lt;klute@rainer-klute.de&gt;</a>
 */
public class MutablePropertySet extends PropertySet
{

    /**
     * <p>Constructs a <code>MutablePropertySet</code> instance. Its
     * primary task is to initialize the immutable field with their proper
     * values. It also sets fields that might change to reasonable defaults.</p>
     */
    public MutablePropertySet()
    {
        /* Initialize the "byteOrder" field. */
        byteOrder = LittleEndian.getUShort(BYTE_ORDER_ASSERTION);

        /* Initialize the "format" field. */
        format = LittleEndian.getUShort(FORMAT_ASSERTION);

        /* Initialize "osVersion" field as if the property has been created on
         * a Win32 platform, whether this is the case or not. */
        osVersion = (OS_WIN32 << 16) | 0x0A04;

        /* Initailize the "classID" field. */
        classID = new ClassID();

        /* Initialize the sections. Since property set must have at least
         * one section it is added right here. */
        sections = new LinkedList<Section>();
        sections.add(new MutableSection());
    }



    /**
     * <p>Constructs a <code>MutablePropertySet</code> by doing a deep copy of
     * an existing <code>PropertySet</code>. All nested elements, i.e.
     * <code>Section</code>s and <code>Property</code> instances, will be their
     * mutable counterparts in the new <code>MutablePropertySet</code>.</p>
     *
     * @param ps The property set to copy
     */
    public MutablePropertySet(final PropertySet ps)
    {
        byteOrder = ps.getByteOrder();
        format = ps.getFormat();
        osVersion = ps.getOSVersion();
        setClassID(ps.getClassID());
        clearSections();
        if (sections == null)
            sections = new LinkedList();
        for (final Iterator i = ps.getSections().iterator(); i.hasNext();)
        {
            final MutableSection s = new MutableSection((Section) (i.next()));
            addSection(s);
        }
    }



    /**
     * <p>The length of the property set stream header.</p>
     */
    private final int OFFSET_HEADER =
        BYTE_ORDER_ASSERTION.length + /* Byte order    */
        FORMAT_ASSERTION.length +     /* Format        */
        LittleEndianConsts.INT_SIZE + /* OS version    */
        ClassID.LENGTH +              /* Class ID      */
        LittleEndianConsts.INT_SIZE;  /* Section count */



    /**
     * <p>Sets the "byteOrder" property.</p>
     *
     * @param byteOrder the byteOrder value to set
     */
    public void setByteOrder(final int byteOrder)
    {
        this.byteOrder = byteOrder;
    }



    /**
     * <p>Sets the "format" property.</p>
     *
     * @param format the format value to set
     */
    public void setFormat(final int format)
    {
        this.format = format;
    }



    /**
     * <p>Sets the "osVersion" property.</p>
     *
     * @param osVersion the osVersion value to set
     */
    public void setOSVersion(final int osVersion)
    {
        this.osVersion = osVersion;
    }



    /**
     * <p>Sets the property set stream's low-level "class ID"
     * field.</p>
     *
     * @param classID The property set stream's low-level "class ID" field.
     *
     * @see PropertySet#getClassID()
     */
    public void setClassID(final ClassID classID)
    {
        this.classID = classID;
    }



    /**
     * <p>Removes all sections from this property set.</p>
     */
    public void clearSections()
    {
        sections = null;
    }



    /**
     * <p>Adds a section to this property set.</p>
     *
     * @param section The {@link Section} to add. It will be appended
     * after any sections that are already present in the property set
     * and thus become the last section.
     */
    public void addSection(final Section section)
    {
        if (sections == null)
            sections = new LinkedList();
        sections.add(section);
    }



    /**
     * <p>Writes the property set to an output stream.</p>
     *
     * @param out the output stream to write the section to
     * @exception IOException if an error when writing to the output stream
     * occurs
     * @exception WritingNotSupportedException if HPSF does not yet support
     * writing a property's variant type.
     */
    public void write(final OutputStream out)
        throws WritingNotSupportedException, IOException
    {
        /* Write the number of sections in this property set stream. */
        final int nrSections = sections.size();
        int length = 0;

        /* Write the property set's header. */
        length += TypeWriter.writeToStream(out, (short) getByteOrder());
        length += TypeWriter.writeToStream(out, (short) getFormat());
        length += TypeWriter.writeToStream(out, getOSVersion());
        length += TypeWriter.writeToStream(out, getClassID());
        length += TypeWriter.writeToStream(out, nrSections);
        int offset = OFFSET_HEADER;

        /* Write the section list, i.e. the references to the sections. Each
         * entry in the section list consist of the section's class ID and the
         * section's offset relative to the beginning of the stream. */
        offset += nrSections * (ClassID.LENGTH + LittleEndian.INT_SIZE);
        final int sectionsBegin = offset;
        for (final ListIterator i = sections.listIterator(); i.hasNext();)
        {
            final MutableSection s = (MutableSection) i.next();
            final ClassID formatID = s.getFormatID();
            if (formatID == null)
                throw new NoFormatIDException();
            length += TypeWriter.writeToStream(out, s.getFormatID());
            length += TypeWriter.writeUIntToStream(out, offset);
            try
            {
                offset += s.getSize();
            }
            catch (HPSFRuntimeException ex)
            {
                final Throwable cause = ex.getReason();
                if (cause instanceof UnsupportedEncodingException) {
                    throw new IllegalPropertySetDataException(cause);
                }
                throw ex;
            }
        }

        /* Write the sections themselves. */
        offset = sectionsBegin;
        for (final ListIterator i = sections.listIterator(); i.hasNext();)
        {
            final MutableSection s = (MutableSection) i.next();
            offset += s.write(out);
        }
    }



    /**
     * <p>Returns the contents of this property set stream as an input stream.
     * The latter can be used for example to write the property set into a POIFS
     * document. The input stream represents a snapshot of the property set.
     * If the latter is modified while the input stream is still being
     * read, the modifications will not be reflected in the input stream but in
     * the {@link MutablePropertySet} only.</p>
     *
     * @return the contents of this property set stream
     *
     * @throws WritingNotSupportedException if HPSF does not yet support writing
     * of a property's variant type.
     * @throws IOException if an I/O exception occurs.
     */
    public InputStream toInputStream()
        throws IOException, WritingNotSupportedException
    {
        final ByteArrayOutputStream psStream = new ByteArrayOutputStream();
        write(psStream);
        psStream.close();
        final byte[] streamData = psStream.toByteArray();
        return new ByteArrayInputStream(streamData);
    }

    /**
     * <p>Writes a property set to a document in a POI filesystem directory.</p>
     *
     * @param dir The directory in the POI filesystem to write the document to.
     * @param name The document's name. If there is already a document with the
     * same name in the directory the latter will be overwritten.
     *
     * @throws WritingNotSupportedException
     * @throws IOException
     */
    public void write(final DirectoryEntry dir, final String name)
    throws WritingNotSupportedException, IOException
    {
        /* If there is already an entry with the same name, remove it. */
        try
        {
            final Entry e = dir.getEntry(name);
            e.delete();
        }
        catch (FileNotFoundException ex)
        {
            /* Entry not found, no need to remove it. */
        }
        /* Create the new entry. */
        dir.createDocument(name, toInputStream());
    }

}
