/*
 * 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.
 */

/* $Id: PDFObject.java 1357883 2012-07-05 20:29:53Z gadams $ */

package org.apache.fop.pdf;

// Java
import java.io.IOException;
import java.io.OutputStream;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * generic PDF object.
 *
 * A PDF Document is essentially a collection of these objects. A PDF
 * Object has a number and a generation (although the generation will always
 * be 0 in new documents).
 */
public abstract class PDFObject implements PDFWritable {

    /** logger for all PDFObjects (and descendants) */
    protected static final Log log = LogFactory.getLog(PDFObject.class.getName());

    /**
     * the object's number
     */
    private int objnum;

    /**
     * the object's generation (0 in new documents)
     */
    private int generation = 0;

    /**
     * the parent PDFDocument
     */
    private PDFDocument document;

    /** the parent PDFObject (may be null and may not always be set, needed for encryption) */
    private PDFObject parent;

    /**
     * Returns the object's number.
     * @return the PDF Object number
     */
    public int getObjectNumber() {
        if (this.objnum == 0) {
            throw new IllegalStateException("Object has no number assigned: " + this.toString());
        }
        return this.objnum;
    }

    /**
     * Default constructor.
     */
    public PDFObject() {
        //nop
    }

    /**
     * Constructor for direct objects.
     * @param parent the containing PDFObject instance
     */
    public PDFObject(PDFObject parent) {
        setParent(parent);
    }

    /**
     * Indicates whether this PDFObject has already been assigned an
     * object number.
     * @return True if it has an object number
     */
    public boolean hasObjectNumber() {
        return this.objnum > 0;
    }

    /**
     * Sets the object number
     * @param objnum the object number
     */
    public void setObjectNumber(int objnum) {
        this.objnum = objnum;
        PDFDocument doc = getDocument();
        setParent(null);
        setDocument(doc); //Restore reference to PDFDocument after setting parent to null
        if (log.isTraceEnabled()) {
            log.trace("Assigning " + this + " object number " + objnum);
        }
    }

    /**
     * Returns this object's generation.
     * @return the PDF Object generation
     */
    public int getGeneration() {
        return this.generation;
    }

    /**
     * Returns the parent PDFDocument if assigned.
     * @return the parent PDFDocument (May be null if the parent PDFDocument
     * has not been assigned)
     */
    public final PDFDocument getDocument() {
        if (this.document != null) {
            return this.document;
        } else if (getParent() != null) {
            return getParent().getDocument();
        } else {
            return null;
        }
    }

    /**
     * Returns the parent PDFDocument, but unlike <code>getDocument()</code>
     * it throws an informative Exception if the parent document is unavailable
     * instead of having a NullPointerException somewhere without a message.
     * @return the parent PDFDocument
     */
    public final PDFDocument getDocumentSafely() {
        final PDFDocument doc = getDocument();
        if (doc == null) {
            throw new IllegalStateException("Parent PDFDocument is unavailable on "
                    + getClass().getName());
        }
        return doc;
    }

    /**
     * Sets the parent PDFDocument.
     * @param doc the PDFDocument.
     */
    public void setDocument(PDFDocument doc) {
        this.document = doc;
    }

    /**
     * Returns this objects's parent. The parent is null if it is a "direct object".
     * @return the parent or null if there's no parent (or it hasn't been set)
     */
    public PDFObject getParent() {
        return this.parent;
    }

    /**
     * Sets the direct parent object.
     * @param parent the direct parent
     */
    public void setParent(PDFObject parent) {
        this.parent = parent;
    }

    /**
     * Returns the PDF representation of the Object ID.
     * @return the Object ID
     */
    public String getObjectID() {
        return getObjectNumber() + " " + getGeneration() + " obj\n";
    }

    /**
     * Returns the PDF representation of a reference to this object.
     * @return the reference string
     */
    public String referencePDF() {
        if (!hasObjectNumber()) {
            throw new IllegalArgumentException(
                    "Cannot reference this object. It doesn't have an object number");
        }
        String ref = getObjectNumber() + " " + getGeneration() + " R";
        return ref;
    }

    /**
     * Creates and returns a reference to this object.
     * @return the object reference
     */
    public PDFReference makeReference() {
        return new PDFReference(this);
    }

    /**
     * Write the PDF represention of this object
     *
     * @param stream the stream to write the PDF to
     * @throws IOException if there is an error writing to the stream
     * @return the number of bytes written
     */
    public int output(OutputStream stream) throws IOException {
        byte[] pdf = this.toPDF();
        stream.write(pdf);
        return pdf.length;
    }

    /** {@inheritDoc} */
    public void outputInline(OutputStream out, StringBuilder textBuffer) throws IOException {
        if (hasObjectNumber()) {
            textBuffer.append(referencePDF());
        } else {
            PDFDocument.flushTextBuffer(textBuffer, out);
            output(out);
        }
    }

    /**
     * Encodes the object as a byte array for output to a PDF file.
     *
     * @return PDF string
     */
    protected byte[] toPDF() {
        return encode(toPDFString());
    }


    /**
     * This method returns a String representation of the PDF object. The result
     * is normally converted/encoded to a byte array by toPDF(). Only use
     * this method to implement the serialization if the object can be fully
     * represented as text. If the PDF representation of the object contains
     * binary content use toPDF() or output(OutputStream) instead. This applies
     * to any object potentially containing a string object because string object
     * are encrypted and therefore need to be binary.
     * @return String the String representation
     */
    protected String toPDFString() {
        throw new UnsupportedOperationException("Not implemented. "
                    + "Use output(OutputStream) instead.");
    }

    /**
     * Converts text to a byte array for writing to a PDF file.
     * @param text text to convert/encode
     * @return byte[] the resulting byte array
     */
    public static final byte[] encode(String text) {
        return PDFDocument.encode(text);
    }

    /**
     * Encodes a Text String (3.8.1 in PDF 1.4 specs)
     * @param text the text to encode
     * @return byte[] the encoded text
     */
    protected byte[] encodeText(String text) {
        if (getDocumentSafely().isEncryptionActive()) {
            final byte[] buf = PDFText.toUTF16(text);
            return PDFText.escapeByteArray(
                getDocument().getEncryption().encrypt(buf, this));
        } else {
            return encode(PDFText.escapeText(text, false));
        }
    }

    /**
     * Encodes a String (3.2.3 in PDF 1.4 specs)
     * @param string the string to encode
     * @return byte[] the encoded string
     */
    protected byte[] encodeString(String string) {
        return encodeText(string);
    }

    /**
     * Encodes binary data as hexadecimal string object.
     * @param data the binary data
     * @param out the OutputStream to write the encoded object to
     * @throws IOException if an I/O error occurs
     */
    protected void encodeBinaryToHexString(byte[] data, OutputStream out) throws IOException {
        out.write('<');
        if (getDocumentSafely().isEncryptionActive()) {
            data = getDocument().getEncryption().encrypt(data, this);
        }
        String hex = PDFText.toHex(data, false);
        byte[] encoded = hex.getBytes("US-ASCII");
        out.write(encoded);
        out.write('>');
    }

    /**
     * Formats an object for serialization to PDF.
     * <p>
     * IMPORTANT: If you need to write out binary output, call
     * {@link PDFDocument#flushTextBuffer(StringBuilder, OutputStream)} before writing any content
     * to the {@link OutputStream}!
     * @param obj the object
     * @param out the OutputStream to write to
     * @param textBuffer a text buffer for text output
     * @throws IOException If an I/O error occurs
     */
    protected void formatObject(Object obj, OutputStream out, StringBuilder textBuffer)
                throws IOException {
        if (obj == null) {
            textBuffer.append("null");
        } else if (obj instanceof PDFWritable) {
            ((PDFWritable)obj).outputInline(out, textBuffer);
        } else if (obj instanceof Number) {
            if (obj instanceof Double || obj instanceof Float) {
                textBuffer.append(PDFNumber.doubleOut(((Number)obj).doubleValue()));
            } else {
                textBuffer.append(obj.toString());
            }
        } else if (obj instanceof Boolean) {
            textBuffer.append(obj.toString());
        } else if (obj instanceof byte[]) {
            PDFDocument.flushTextBuffer(textBuffer, out);
            encodeBinaryToHexString((byte[])obj, out);
        } else {
            PDFDocument.flushTextBuffer(textBuffer, out);
            out.write(encodeText(obj.toString()));
        }
    }

    /**
     * Check if the other PDFObject has the same content as the current object.
     * <p>
     * Note: This function has a contract which is less binding than
     * {@link #equals(Object)}. Whereas equals would require all values to be
     * identical, this method is not required to check everything. In the case
     * of PDFObjects, this means that the overriding function does not have to
     * check for {@link #getObjectID()}.
     *
     * @param o
     *            object to compare to.
     * @return true if the other object has the same content.
     */
    protected boolean contentEquals(PDFObject o) {
        return this.equals(o);
    }
}
