/* ====================================================================
   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.openxml4j.opc;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;

import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.openxml4j.exceptions.InvalidOperationException;
import org.apache.poi.openxml4j.exceptions.OpenXML4JException;
import org.apache.poi.openxml4j.opc.internal.ContentType;

/**
 * Provides a base class for parts stored in a Package.
 */
public abstract class PackagePart implements RelationshipSource, Comparable<PackagePart> {

    /**
     * This part's container.
     */
    protected OPCPackage _container;

    /**
     * The part name. (required by the specification [M1.1])
     */
    protected PackagePartName _partName;

    /**
     * The type of content of this part. (required by the specification [M1.2])
     */
    protected ContentType _contentType;

    /**
     * Flag to know if this part is a relationship.
     */
    private final boolean _isRelationshipPart;

    /**
     * Flag to know if this part has been logically deleted.
     */
    private boolean _isDeleted;

    /**
     * This part's relationships.
     */
    private PackageRelationshipCollection _relationships;


    /**
     * Constructor.
     *
     * @param pack
     *            Parent package.
     * @param partName
     *            The part name, relative to the parent Package root.
     * @param contentType
     *            The content type.
     * @throws InvalidFormatException
     *             If the specified URI is not valid.
     */
    protected PackagePart(OPCPackage pack, PackagePartName partName,
            ContentType contentType) throws InvalidFormatException {
        this(pack, partName, contentType, true);
    }

    /**
     * Constructor.
     *
     * @param pack
     *            Parent package.
     * @param partName
     *            The part name, relative to the parent Package root.
     * @param contentType
     *            The content type.
     * @param loadRelationships
     *            Specify if the relationships will be loaded
     * @throws InvalidFormatException
     *             If the specified URI is not valid.
     */
    protected PackagePart(OPCPackage pack, PackagePartName partName,
            ContentType contentType, boolean loadRelationships)
            throws InvalidFormatException {
        _partName = partName;
        _contentType = contentType;
        _container = pack;

        // Check if this part is a relationship part
        _isRelationshipPart = this._partName.isRelationshipPartURI();

        // Load relationships if any
        if (loadRelationships) {
            loadRelationships();
        }
    }

    /**
     * Constructor.
     *
     * @param pack
     *            Parent package.
     * @param partName
     *            The part name, relative to the parent Package root.
     * @param contentType
     *            The Multipurpose Internet Mail Extensions (MIME) content type
     *            of the part's data stream.
     * @throws InvalidFormatException
     *             If the specified URI is not valid.
     */
    public PackagePart(OPCPackage pack, PackagePartName partName,
            String contentType) throws InvalidFormatException {
        this(pack, partName, new ContentType(contentType));
    }

    /**
     * Check if the new part was already added before via PackagePart.addRelationship()
     *
     * @param packagePart to find the relationship for
     * @return The existing relationship, or null if there isn't yet one
     */
    public PackageRelationship findExistingRelation(PackagePart packagePart) {
        return _relationships.findExistingInternalRelation(packagePart);
    }

    /**
     * Adds an external relationship to a part (except relationships part).
     *
     * The targets of external relationships are not subject to the same
     * validity checks that internal ones are, as the contents is potentially
     * any file, URL or similar.
     *
     * @param target
     *            External target of the relationship
     * @param relationshipType
     *            Type of relationship.
     * @return The newly created and added relationship
     */
    @Override
    public PackageRelationship addExternalRelationship(String target,
            String relationshipType) {
        return addExternalRelationship(target, relationshipType, null);
    }

    /**
     * Adds an external relationship to a part (except relationships part).
     *
     * The targets of external relationships are not subject to the same
     * validity checks that internal ones are, as the contents is potentially
     * any file, URL or similar.
     *
     * @param target
     *            External target of the relationship
     * @param relationshipType
     *            Type of relationship.
     * @param id
     *            Relationship unique id.
     * @return The newly created and added relationship
     * @see org.apache.poi.openxml4j.opc.RelationshipSource#addExternalRelationship(java.lang.String,
     *      java.lang.String)
     */
    @Override
    public PackageRelationship addExternalRelationship(String target,
            String relationshipType, String id) {
        if (target == null) {
            throw new IllegalArgumentException("target is null for type " + relationshipType);
        }
        if (relationshipType == null) {
            throw new IllegalArgumentException("relationshipType");
        }

        if (_relationships == null) {
            _relationships = new PackageRelationshipCollection();
        }

        URI targetURI;
        try {
            targetURI = new URI(target);
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException("Invalid target - " + e);
        }

        return _relationships.addRelationship(targetURI, TargetMode.EXTERNAL,
                relationshipType, id);
    }

    /**
     * Add a relationship to a part (except relationships part).
     *
     * @param targetPartName
     *            Name of the target part. This one must be relative to the
     *            source root directory of the part.
     * @param targetMode
     *            Mode [Internal|External].
     * @param relationshipType
     *            Type of relationship.
     * @return The newly created and added relationship
     */
    @Override
    public PackageRelationship addRelationship(PackagePartName targetPartName,
            TargetMode targetMode, String relationshipType) {
        return addRelationship(targetPartName, targetMode, relationshipType,
                null);
    }

    /**
     * Add a relationship to a part (except relationships part).
     * <p>
     * Check rule M1.25: The Relationships part shall not have relationships to
     * any other part. Package implementers shall enforce this requirement upon
     * the attempt to create such a relationship and shall treat any such
     * relationship as invalid.
     * </p>
     * @param targetPartName
     *            Name of the target part. This one must be relative to the
     *            source root directory of the part.
     * @param targetMode
     *            Mode [Internal|External].
     * @param relationshipType
     *            Type of relationship.
     * @param id
     *            Relationship unique id.
     * @return The newly created and added relationship
     *
     * @throws InvalidOperationException
     *             If a writing operation is done on a read only package or
     *             invalid nested relations are created.
     * @throws IllegalArgumentException if targetPartName, targetMode
     *             or relationshipType are passed as null
     */
    @Override
    public PackageRelationship addRelationship(PackagePartName targetPartName,
            TargetMode targetMode, String relationshipType, String id) {
        _container.throwExceptionIfReadOnly();

        if (targetPartName == null) {
            throw new IllegalArgumentException("targetPartName");
        }
        if (targetMode == null) {
            throw new IllegalArgumentException("targetMode");
        }
        if (relationshipType == null) {
            throw new IllegalArgumentException("relationshipType");
        }

        if (this._isRelationshipPart || targetPartName.isRelationshipPartURI()) {
            throw new InvalidOperationException(
                    "Rule M1.25: The Relationships part shall not have relationships to any other part.");
        }

        if (_relationships == null) {
            _relationships = new PackageRelationshipCollection();
        }

        return _relationships.addRelationship(targetPartName.getURI(),
                targetMode, relationshipType, id);
    }

    /**
     * Add a relationship to a part (except relationships part).
     *
     * @param targetURI
     *            URI the target part. Must be relative to the source root
     *            directory of the part.
     * @param targetMode
     *            Mode [Internal|External].
     * @param relationshipType
     *            Type of relationship.
     * @return The newly created and added relationship
     * @see org.apache.poi.openxml4j.opc.RelationshipSource#addRelationship(org.apache.poi.openxml4j.opc.PackagePartName,
     *      org.apache.poi.openxml4j.opc.TargetMode, java.lang.String)
     */
    public PackageRelationship addRelationship(URI targetURI,
            TargetMode targetMode, String relationshipType) {
        return addRelationship(targetURI, targetMode, relationshipType, null);
    }

    /**
     * Add a relationship to a part (except relationships part).
     * <p>
     * Check rule M1.25: The Relationships part shall not have relationships to
     * any other part. Package implementers shall enforce this requirement upon
     * the attempt to create such a relationship and shall treat any such
     * relationship as invalid.
     * </p>
     * @param targetURI
     *            URI of the target part. Must be relative to the source root
     *            directory of the part.
     * @param targetMode
     *            Mode [Internal|External].
     * @param relationshipType
     *            Type of relationship.
     * @param id
     *            Relationship unique id.
     * @return The newly created and added relationship
     *
     * @throws InvalidOperationException
     *             If the URI point to a relationship part URI.
     * @see org.apache.poi.openxml4j.opc.RelationshipSource#addRelationship(org.apache.poi.openxml4j.opc.PackagePartName,
     *      org.apache.poi.openxml4j.opc.TargetMode, java.lang.String, java.lang.String)
     */
    public PackageRelationship addRelationship(URI targetURI,
            TargetMode targetMode, String relationshipType, String id) {
        _container.throwExceptionIfReadOnly();

        if (targetURI == null) {
            throw new IllegalArgumentException("targetPartName");
        }
        if (targetMode == null) {
            throw new IllegalArgumentException("targetMode");
        }
        if (relationshipType == null) {
            throw new IllegalArgumentException("relationshipType");
        }

        // Try to retrieve the target part

        if (this._isRelationshipPart
                || PackagingURIHelper.isRelationshipPartURI(targetURI)) {
            throw new InvalidOperationException(
                    "Rule M1.25: The Relationships part shall not have relationships to any other part.");
        }

        if (_relationships == null) {
            _relationships = new PackageRelationshipCollection();
        }

        return _relationships.addRelationship(targetURI,
                targetMode, relationshipType, id);
    }

    @Override
    public void clearRelationships() {
        if (_relationships != null) {
            _relationships.clear();
        }
    }

    /**
     * Delete the relationship specified by its id.
     *
     * @param id
     *            The ID identified the part to delete.
     */
    @Override
    public void removeRelationship(String id) {
        this._container.throwExceptionIfReadOnly();
        if (this._relationships != null)
            this._relationships.removeRelationship(id);
    }

    /**
     * Retrieve all the relationships attached to this part.
     *
     * @return This part's relationships.
     * @throws InvalidOperationException
     *             Throws if the package is open en write only mode.
     */
    @Override
    public PackageRelationshipCollection getRelationships()
            throws InvalidFormatException {
        return getRelationshipsCore(null);
    }

    /**
     * Retrieves a package relationship from its id.
     *
     * @param id
     *            ID of the package relationship to retrieve.
     * @return The package relationship
     */
    @Override
    public PackageRelationship getRelationship(String id) {
        return this._relationships.getRelationshipByID(id);
    }

    /**
     * Retrieve all relationships attached to this part which have the specified
     * type.
     *
     * @param relationshipType
     *            Relationship type filter.
     * @return All relationships from this part that have the specified type.
     * @throws InvalidFormatException
     *             If an error occurs while parsing the part.
     * @throws InvalidOperationException
     *             If the package is open in write only mode.
     */
    @Override
    public PackageRelationshipCollection getRelationshipsByType(
            String relationshipType) throws InvalidFormatException {
        _container.throwExceptionIfWriteOnly();

        return getRelationshipsCore(relationshipType);
    }

    /**
     * Implementation of the getRelationships method().
     *
     * @param filter
     *            Relationship type filter. If <i>null</i> then the filter is
     *            disabled and return all the relationships.
     * @return All relationships from this part that have the specified type.
     * @throws InvalidFormatException
     *             Throws if an error occurs during parsing the relationships
     *             part.
     * @throws InvalidOperationException
     *             Throws if the package is open en write only mode.
     * @see #getRelationshipsByType(String)
     */
    private PackageRelationshipCollection getRelationshipsCore(String filter)
            throws InvalidFormatException {
        this._container.throwExceptionIfWriteOnly();
        if (_relationships == null) {
            this.throwExceptionIfRelationship();
            _relationships = new PackageRelationshipCollection(this);
        }
        return new PackageRelationshipCollection(_relationships, filter);
    }

    /**
     * Knows if the part have any relationships.
     *
     * @return <b>true</b> if the part have at least one relationship else
     *         <b>false</b>.
     */
    @Override
    public boolean hasRelationships() {
        return (!this._isRelationshipPart && (_relationships != null && !_relationships.isEmpty()));
    }

    /**
     * Checks if the specified relationship is part of this package part.
     *
     * @param rel
     *            The relationship to check.
     * @return <b>true</b> if the specified relationship exists in this part,
     *         else returns <b>false</b>
     */
    @Override
    public boolean isRelationshipExists(PackageRelationship rel) {
        return rel != null && _relationships.getRelationshipByID(rel.getId()) != null;
    }

   /**
    * Get the PackagePart that is the target of a relationship.
    *
    * @param rel A relationship from this part to another one
    * @return The target part of the relationship
    * @throws InvalidFormatException
    *             If the specified URI is not valid.
    */
    public PackagePart getRelatedPart(PackageRelationship rel) throws InvalidFormatException {
        // Ensure this is one of ours
        if(! isRelationshipExists(rel)) {
            throw new IllegalArgumentException("Relationship " + rel + " doesn't start with this part " + _partName);
        }
        // Get the target URI, excluding any relative fragments
        URI target = rel.getTargetURI();
        if(target.getFragment() != null) {
            String t = target.toString();
            try {
                target = new URI( t.substring(0, t.indexOf('#')) );
            } catch(URISyntaxException e) {
                throw new InvalidFormatException("Invalid target URI: " + target);
            }
        }

        // Turn that into a name, and fetch
        PackagePartName relName = PackagingURIHelper.createPartName(target);
        PackagePart part = _container.getPart(relName);
        if (part == null) {
            throw new IllegalArgumentException("No part found for relationship " + rel);
        }
        return part;
    }

    /**
     * Get the input stream of this part to read its content.
     *
     * @return The input stream of the content of this part, else
     *         {@code null}.
     *
     * @throws IOException If creating the input-stream fails.
     */
    public InputStream getInputStream() throws IOException {
        InputStream inStream = this.getInputStreamImpl();
        if (inStream == null) {
            throw new IOException("Can't obtain the input stream from "
                    + _partName.getName());
        }
        return inStream;
    }

    /**
     * Get the output stream of this part. If the part is originally embedded in
     * Zip package, it'll be transform into a <i>MemoryPackagePart</i> in
     * order to write inside (the standard Java API doesn't allow to write in
     * the file)
     *
     * @return output stream for this part
     * @see org.apache.poi.openxml4j.opc.internal.MemoryPackagePart
     */
    public OutputStream getOutputStream() throws IOException {
        OutputStream outStream;
        // If this part is a zip package part (read only by design) we convert
        // this part into a MemoryPackagePart instance for write purpose.
        if (this instanceof ZipPackagePart) {
            // Delete logically this part
            _container.removePart(this._partName);

            // Create a memory part
            PackagePart part = _container.createPart(this._partName,
                    this._contentType.toString(), false);
            if (part == null) {
                throw new InvalidOperationException(
                        "Can't create a temporary part !");
            }
            part._relationships = this._relationships;
            outStream = part.getOutputStreamImpl();
        } else {
            outStream = this.getOutputStreamImpl();
        }
        return outStream;
    }

    /**
     * Throws an exception if this package part is a relationship part.
     *
     * @throws InvalidOperationException
     *             If this part is a relationship part.
     */
    private void throwExceptionIfRelationship()
            throws InvalidOperationException {
        if (this._isRelationshipPart)
            throw new InvalidOperationException(
                    "Can do this operation on a relationship part !");
    }

    /**
     * Ensure the package relationships collection instance is built.
     *
     * @throws InvalidFormatException
     *             Throws if
     */
    /* package */ void loadRelationships() throws InvalidFormatException {
        if (this._relationships == null && !this._isRelationshipPart) {
            this.throwExceptionIfRelationship();
            _relationships = new PackageRelationshipCollection(this);
        }
    }

    /*
     * Accessors
     */

    /**
     * @return the uri
     */
    public PackagePartName getPartName() {
        return _partName;
    }

    /**
     * @return The Content Type of the part
     */
    public String getContentType() {
        return _contentType.toString();
    }

    /**
     * @return The Content Type, including parameters, of the part
     */
    public ContentType getContentTypeDetails() {
        return _contentType;
    }

    /**
     * Set the content type.
     *
     * @param contentType
     *            the contentType to set
     *
     * @throws InvalidFormatException
     *             Throws if the content type is not valid.
     * @throws InvalidOperationException
     *             Throws if you try to change the content type whereas this
     *             part is already attached to a package.
     */
    public void setContentType(String contentType)
            throws InvalidFormatException {
        if (_container == null) {
            _contentType = new ContentType(contentType);
        }
        else {
            _container.unregisterPartAndContentType(_partName);
            _contentType = new ContentType(contentType);
            _container.registerPartAndContentType(this);
        }
    }

    public OPCPackage getPackage() {
        return _container;
    }

    /**
     * @return true if this part is a relationship
     */
    public boolean isRelationshipPart() {
        return this._isRelationshipPart;
    }

    /**
     * @return true if this part has been logically deleted
     */
    public boolean isDeleted() {
        return _isDeleted;
    }

    /**
     * @param isDeleted
     *            the isDeleted to set
     */
    public void setDeleted(boolean isDeleted) {
        this._isDeleted = isDeleted;
    }

    /**
     * @return The length of the part in bytes, or -1 if not known
     */
    public long getSize() {
       return -1;
    }

    @Override
    public String toString() {
        return "Name: " + this._partName + " - Content Type: "
                + this._contentType;
    }

    /**
     * Compare based on the package part name, using a natural sort order
     */
    @Override
    public int compareTo(PackagePart other)
    {
        // NOTE could also throw a NullPointerException() if desired
        if (other == null)
            return -1;

        return PackagePartName.compare(this._partName, other._partName);
    }

    /*-------------- Abstract methods ------------- */

    /**
     * Method that gets the input stream for this part.
     *
     * @return input stream for this part
     * @throws IOException
     *                Throws if an IO Exception occur in the implementation method.
     */
    protected abstract InputStream getInputStreamImpl() throws IOException;

    /**
     * Method that gets the output stream for this part.
     *
     * @return output stream for this part
     * @throws IOException
     *                Throws if an IO Exception occur in the implementation method.
     */
    protected abstract OutputStream getOutputStreamImpl() throws IOException;

    /**
     * Save the content of this part and the associated relationships part (if
     * this part own at least one relationship) into the specified output
     * stream.
     *
     * @param zos
     *            Output stream to save this part.
     * @return true if the content has been successfully stored, false otherwise.
     *         More information about errors may be logged via Log4j 2.
     * @throws OpenXML4JException
     *             If any exception occur.
     */
    public abstract boolean save(OutputStream zos) throws OpenXML4JException;

    /**
     * Load the content of this part.
     *
     * @param ios
     *            The input stream of the content to load.
     * @return true if the content has been successfully loaded, false otherwise.
     *         More information about errors may be logged via Log4j 2.
     * @throws InvalidFormatException
     *             Throws if the content format is invalid.
     */
    public abstract boolean load(InputStream ios) throws InvalidFormatException;

    /**
     * Close this part : flush this part, close the input stream and output
     * stream. After this method call, the part must be available for packaging.
     */
    public abstract void close();

    /**
     * Flush the content of this part. If the input stream and/or output stream
     * as in a waiting state to read or write, the must to empty their
     * respective buffer.
     */
    public abstract void flush();

    /**
     * Allows sub-classes to clean up before new data is added.
     */
    public void clear() {
    }
}
