#region PDFsharp - A .NET library for processing PDF
//
// Authors:
//   Stefan Lange
//
// Copyright (c) 2005-2017 empira Software GmbH, Cologne Area (Germany)
//
// http://www.pdfsharp.com
// http://sourceforge.net/projects/pdfsharp
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
// DEALINGS IN THE SOFTWARE.
#endregion

using System;
using System.Diagnostics;
using PdfSharp.Pdf.Advanced;
using PdfSharp.Pdf.IO;

namespace PdfSharp.Pdf
{
    /// <summary>
    /// Base class of all composite PDF objects.
    /// </summary>
    public abstract class PdfObject : PdfItem
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="PdfObject"/> class.
        /// </summary>
        protected PdfObject()
        { }

        /// <summary>
        /// Initializes a new instance of the <see cref="PdfObject"/> class.
        /// </summary>
        protected PdfObject(PdfDocument document)
        {
            // Calling a virtual member in a constructor is dangerous.
            // In PDFsharp Document is overridden in PdfPage and the code is checked to be save
            // when called for a not completely initialized object.
            Document = document;
        }

        /// <summary>
        /// Initializes a new instance from an existing object. Used for object type transformation.
        /// </summary>
        protected PdfObject(PdfObject obj)
            : this(obj.Owner)
        {
            // If the object that was transformed to an instance of a derived class was an indirect object
            // set the value of the reference to this.
            if (obj._iref != null)
                obj._iref.Value = this;
#if DEBUG_  // BUG
            else
            {
                // If this occurs it is an internal error
                Debug.Assert(false, "Object type transformation must not be done with direct objects");
            }
#endif
        }

        /// <summary>
        /// Creates a copy of this object. The clone does not belong to a document, i.e. its owner and its iref are null.
        /// </summary>
        public new PdfObject Clone()
        {
            return (PdfObject)Copy();
        }

        /// <summary>
        /// Implements the copy mechanism. Must be overridden in derived classes.
        /// </summary>
        protected override object Copy()
        {
            PdfObject obj = (PdfObject)base.Copy();
            obj._document = null;
            obj._iref = null;
            return obj;
        }

#if true_  // works, but may lead to other problems that I cannot assess
        /// <summary>
        /// Determines whether the specified object is equal to the current PdfObject.
        /// </summary>
        public override bool Equals(object obj)
        {
            if (obj is PdfObject)
            {
                PdfObject other = (PdfObject)obj;
                // Take object type transformation into account
                if (_iref != null && other._iref != null)
                {
                    Debug.Assert(_iref.Value != null, "iref without value.");
                    Debug.Assert(other.iref.Value != null, "iref without value.");
                    return Object.ReferenceEquals(_iref.Value, other.iref.Value);
                }
            }
            return base.Equals(obj);
        }

        public override int GetHashCode()
        {
            if (_iref != null)
            {
                Debug.Assert(_iref.Value != null, "iref without value.");
                return _iref.GetHashCode();
            }
            return base.GetHashCode();
        }
#endif

        /// <summary>
        /// Sets the object and generation number.
        /// Setting the object identifier makes this object an indirect object, i.e. the object gets
        /// a PdfReference entry in the PdfReferenceTable.
        /// </summary>
        internal void SetObjectID(int objectNumber, int generationNumber)
        {
            PdfObjectID objectID = new PdfObjectID(objectNumber, generationNumber);

            // TODO: check imported
            if (_iref == null)
                _iref = _document._irefTable[objectID];
            if (_iref == null)
            {
                // ReSharper disable once ObjectCreationAsStatement because the new object is set to this object
                // in the constructor of PdfReference.
                new PdfReference(this);
                Debug.Assert(_iref != null);
                _iref.ObjectID = objectID;
            }
            _iref.Value = this;
            _iref.Document = _document;
        }

        /// <summary>
        /// Gets the PdfDocument this object belongs to.
        /// </summary>
        public virtual PdfDocument Owner
        {
            get { return _document; }
        }

        /// <summary>
        /// Sets the PdfDocument this object belongs to.
        /// </summary>
        internal virtual PdfDocument Document
        {
            set
            {
                if (!ReferenceEquals(_document, value))
                {
                    if (_document != null)
                        throw new InvalidOperationException("Cannot change document.");
                    _document = value;
                    if (_iref != null)
                        _iref.Document = value;
                }
            }
        }
        internal PdfDocument _document;

        /// <summary>
        /// Indicates whether the object is an indirect object.
        /// </summary>
        public bool IsIndirect
        {
            // An object is an indirect object if and only if is has an indirect reference value.
            get { return _iref != null; }
        }

        /// <summary>
        /// Gets the PdfInternals object of this document, that grants access to some internal structures
        /// which are not part of the public interface of PdfDocument.
        /// </summary>
        public PdfObjectInternals Internals
        {
            get { return _internals ?? (_internals = new PdfObjectInternals(this)); }
        }
        PdfObjectInternals _internals;

        /// <summary>
        /// When overridden in a derived class, prepares the object to get saved.
        /// </summary>
        internal virtual void PrepareForSave()
        { }

        /// <summary>
        /// Saves the stream position. 2nd Edition.
        /// </summary>
        internal override void WriteObject(PdfWriter writer)
        {
            Debug.Assert(false, "Must not come here!");
            //Debug.Assert(_inStreamOffset <= 0);
            //if (_inStreamOffset == 0)
            //{
            //    //_InStreamOffset = stream.Position;
            //    _document.xrefTable.AddObject(this);
            //    return Format("{0} {1} obj\n", _objectID, _generation);
            //}
            //else if (_inStreamOffset == -1)
            //{
            //}
            //return null;
        }

        /// <summary>
        /// Gets the object identifier. Returns PdfObjectID.Empty for direct objects,
        /// i.e. never returns null.
        /// </summary>
        internal PdfObjectID ObjectID
        {
            get { return _iref != null ? _iref.ObjectID : PdfObjectID.Empty; }
        }

        /// <summary>
        /// Gets the object number.
        /// </summary>
        internal int ObjectNumber
        {
            get { return ObjectID.ObjectNumber; }
        }

        /// <summary>
        /// Gets the generation number.
        /// </summary>
        internal int GenerationNumber
        {
            get { return ObjectID.GenerationNumber; }
        }

        ///// <summary>
        ///// Creates a deep copy of the specified value and its transitive closure and adds the
        ///// new objects to the specified owner document.
        ///// </summary>
        /// <param name="owner">The document that owns the cloned objects.</param>
        /// <param name="externalObject">The root object to be cloned.</param>
        /// <returns>The clone of the root object</returns>
        internal static PdfObject DeepCopyClosure(PdfDocument owner, PdfObject externalObject)
        {
            // Get transitive closure.
            PdfObject[] elements = externalObject.Owner.Internals.GetClosure(externalObject);
            int count = elements.Length;
#if DEBUG_
            for (int idx = 0; idx < count; idx++)
            {
                Debug.Assert(elements[idx].XRef != null);
                Debug.Assert(elements[idx].XRef.Document != null);
                Debug.Assert(elements[idx].Document != null);
                if (elements[idx].ObjectID.ObjectNumber == 12)
                    GetType();
            }
#endif
            // 1st loop. Replace all objects by their clones.
            PdfImportedObjectTable iot = new PdfImportedObjectTable(owner, externalObject.Owner);
            for (int idx = 0; idx < count; idx++)
            {
                PdfObject obj = elements[idx];
                PdfObject clone = obj.Clone();
                Debug.Assert(clone.Reference == null);
                clone.Document = owner;
                if (obj.Reference != null)
                {
                    // Case: The cloned object was an indirect object.
                    // Add clone to new owner document.
                    owner._irefTable.Add(clone);
                    // The clone gets an iref by adding it to its new owner.
                    Debug.Assert(clone.Reference != null);
                    // Save an association from old object identifier to new iref.
                    iot.Add(obj.ObjectID, clone.Reference);
                }
                else
                {
                    // Case: The cloned object was an direct object.
                    // Only the root object can be a direct object.
                    Debug.Assert(idx == 0);
                }
                // Replace external object by its clone.
                elements[idx] = clone;
            }
#if DEBUG_
            for (int idx = 0; idx < count; idx++)
            {
                Debug.Assert(elements[idx]._iref != null);
                Debug.Assert(elements[idx]._iref.Document != null);
                Debug.Assert(resources[idx].Document != null);
                if (elements[idx].ObjectID.ObjectNumber == 12)
                    GetType();
            }
#endif

            // 2nd loop. Fix up all indirect references that still refers to the import document.
            for (int idx = 0; idx < count; idx++)
            {
                PdfObject obj = elements[idx];
                Debug.Assert(obj.Owner == owner);
                FixUpObject(iot, owner, obj);
            }

            // Return the clone of the former root object.
            return elements[0];
        }

        ///// <summary>
        ///// Imports an object and its transitive closure to the specified document.
        ///// </summary>
        /// <param name="importedObjectTable">The imported object table of the owner for the external document.</param>
        /// <param name="owner">The document that owns the cloned objects.</param>
        /// <param name="externalObject">The root object to be cloned.</param>
        /// <returns>The clone of the root object</returns>
        internal static PdfObject ImportClosure(PdfImportedObjectTable importedObjectTable, PdfDocument owner, PdfObject externalObject)
        {
            Debug.Assert(ReferenceEquals(importedObjectTable.Owner, owner), "importedObjectTable does not belong to the owner.");
            Debug.Assert(ReferenceEquals(importedObjectTable.ExternalDocument, externalObject.Owner),
                "The ExternalDocument of the importedObjectTable does not belong to the owner of object to be imported.");

            // Get transitive closure of external object.
            PdfObject[] elements = externalObject.Owner.Internals.GetClosure(externalObject);
            int count = elements.Length;
#if DEBUG_
            for (int idx = 0; idx < count; idx++)
            {
                Debug.Assert(elements[idx].XRef != null);
                Debug.Assert(elements[idx].XRef.Document != null);
                Debug.Assert(elements[idx].Document != null);
                if (elements[idx].ObjectID.ObjectNumber == 12)
                    GetType();
            }
#endif
            // 1st loop. Already imported objects are reused and new ones are cloned.
            for (int idx = 0; idx < count; idx++)
            {
                PdfObject obj = elements[idx];
                Debug.Assert(!ReferenceEquals(obj.Owner, owner));

                if (importedObjectTable.Contains(obj.ObjectID))
                {
#if DEBUG_
                    if (obj.ObjectID.ObjectNumber == 5894)
                        obj.GetType();
#endif
                    // Case: External object was already imported.
                    PdfReference iref = importedObjectTable[obj.ObjectID];
                    Debug.Assert(iref != null);
                    Debug.Assert(iref.Value != null);
                    Debug.Assert(iref.Document == owner);
                    // Replace external object by the already cloned counterpart.
                    elements[idx] = iref.Value;
                }
                else
                {
                    // Case: External object was not yet imported earlier and must be cloned.
                    PdfObject clone = obj.Clone();
                    Debug.Assert(clone.Reference == null);
                    clone.Document = owner;
                    if (obj.Reference != null)
                    {
                        // Case: The cloned object was an indirect object.
                        // Add clone to new owner document.
                        owner._irefTable.Add(clone);
                        Debug.Assert(clone.Reference != null);
                        // Save an association from old object identifier to new iref.
                        importedObjectTable.Add(obj.ObjectID, clone.Reference);
                    }
                    else
                    {
                        // Case: The cloned object was a direct object.
                        // Only the root object can be a direct object.
                        Debug.Assert(idx == 0);
                    }
                    // Replace external object by its clone.
                    elements[idx] = clone;
                }
            }
#if DEBUG_
            for (int idx = 0; idx < count; idx++)
            {
                //Debug.Assert(elements[idx].Reference != null);
                //Debug.Assert(elements[idx].Reference.Document != null);
                Debug.Assert(elements[idx].IsIndirect == false);
                Debug.Assert(elements[idx].Owner != null);
                //if (elements[idx].ObjectID.ObjectNumber == 12)
                //    GetType();
            }
#endif
            // 2nd loop. Fix up indirect references that still refers to the external document.
            for (int idx = 0; idx < count; idx++)
            {
                PdfObject obj = elements[idx];
                Debug.Assert(owner != null);
                FixUpObject(importedObjectTable, importedObjectTable.Owner, obj);
            }

            // Return the imported root object.
            return elements[0];
        }

        /// <summary>
        /// Replace all indirect references to external objects by their cloned counterparts
        /// owned by the importer document.
        /// </summary>
        static void FixUpObject(PdfImportedObjectTable iot, PdfDocument owner, PdfObject value)
        {
            Debug.Assert(ReferenceEquals(iot.Owner, owner));

            PdfDictionary dict;
            PdfArray array;
            if ((dict = value as PdfDictionary) != null)
            {
                // Case: The object is a dictionary.
                // Set document for cloned direct objects.
                if (dict.Owner == null)
                {
                    // If the dictionary has not yet an owner set the owner to the importing document.
                    dict.Document = owner;
                }
                else
                {
                    // If the dictionary already has an owner it must be the importing document.
                    Debug.Assert(dict.Owner == owner);
                }

                // Search for indirect references in all dictionary elements.
                PdfName[] names = dict.Elements.KeyNames;
                foreach (PdfName name in names)
                {
                    PdfItem item = dict.Elements[name];
                    Debug.Assert(item != null, "A dictionary element cannot be null.");

                    // Is item an iref?
                    PdfReference iref = item as PdfReference;
                    if (iref != null)
                    {
                        // Case: The item is a reference.
                        // Does the iref already belongs to the new owner?
                        if (iref.Document == owner)
                        {
                            // Yes: fine. Happens when an already cloned object is reused.
                            continue;
                        }

                        //Debug.Assert(iref.Document == iot.Document);
                        // No: Replace with iref of cloned object.
                        PdfReference newXRef = iot[iref.ObjectID];  // TODO: Explain this line of code in all details.
                        Debug.Assert(newXRef != null);
                        Debug.Assert(newXRef.Document == owner);
                        dict.Elements[name] = newXRef;
                    }
                    else
                    {
                        // Case: The item is not a reference.
                        // If item is an object recursively fix its inner items.
                        PdfObject pdfObject = item as PdfObject;
                        if (pdfObject != null)
                        {
                            // Fix up inner objects, i.e. recursively walk down the object tree.
                            FixUpObject(iot, owner, pdfObject);
                        }
                        else
                        {
                            // The item is something else, e.g. a name.
                            // Nothing to do.

                            // ...but let's double check this case in DEBUG build.
                            DebugCheckNonObjects(item);
                        }
                    }
                }
            }
            else if ((array = value as PdfArray) != null)
            {
                // Case: The object is an array.
                // Set document for cloned direct objects.
                if (array.Owner == null)
                {
                    // If the array has not yet an owner set the owner to the importing document.
                    array.Document = owner;
                }
                else
                {
                    // If the array already has an owner it must be the importing document.
                    Debug.Assert(array.Owner == owner);
                }

                // Search for indirect references in all array elements.
                int count = array.Elements.Count;
                for (int idx = 0; idx < count; idx++)
                {
                    PdfItem item = array.Elements[idx];
                    Debug.Assert(item != null, "An array element cannot be null.");

                    // Is item an iref?
                    PdfReference iref = item as PdfReference;
                    if (iref != null)
                    {
                        // Case: The item is a reference.
                        // Does the iref already belongs to the owner?
                        if (iref.Document == owner)
                        {
                            // Yes: fine. Happens when an already cloned object is reused.
                            continue;
                        }

                        // No: replace with iref of cloned object.
                        Debug.Assert(iref.Document == iot.ExternalDocument);
                        PdfReference newXRef = iot[iref.ObjectID];
                        Debug.Assert(newXRef != null);
                        Debug.Assert(newXRef.Document == owner);
                        array.Elements[idx] = newXRef;
                    }
                    else
                    {
                        // Case: The item is not a reference.
                        // If item is an object recursively fix its inner items.
                        PdfObject pdfObject = item as PdfObject;
                        if (pdfObject != null)
                        {
                            // Fix up inner objects, i.e. recursively walk down the object tree.
                            FixUpObject(iot, owner, pdfObject);
                        }
                        else
                        {
                            // The item is something else, e.g. a name.
                            // Nothing to do.

                            // ...but let's double check this case in DEBUG build.
                            DebugCheckNonObjects(item);
                        }
                    }
                }
            }
            else
            {
                // Case: The item is some other indirect object.
                // Indirect integers, booleans, etc. are allowed, but PDFsharp do not create them.
                // If such objects occur in imported PDF files from other producers, nothing more is to do.
                // The owner was already set, which is double checked by the assertions below.
                if (value is PdfNameObject || value is PdfStringObject || value is PdfBooleanObject || value is PdfIntegerObject || value is PdfNumberObject)
                {
                    Debug.Assert(value.IsIndirect);
                    Debug.Assert(value.Owner == owner);
                }
                else
                    Debug.Assert(false, "Should not come here. Object is neither a dictionary nor an array.");
            }
        }

        /// <summary>
        /// Ensure for future versions of PDFsharp not to forget code for a new kind of PdfItem.
        /// </summary>
        /// <param name="item">The item.</param>
        [Conditional("DEBUG")]
        static void DebugCheckNonObjects(PdfItem item)
        {
            if (item is PdfName)
                return;
            if (item is PdfBoolean)
                return;
            if (item is PdfInteger)
                return;
            if (item is PdfNumber)
                return;
            if (item is PdfString)
                return;
            if (item is PdfRectangle)
                return;
            if (item is PdfNull)
                return;

            Type type = item.GetType();
            Debug.Assert(type != null, string.Format("CheckNonObjects: Add {0} to the list.", type.Name));
        }

        /// <summary>
        /// Gets the indirect reference of this object. If the value is null, this object is a direct object.
        /// </summary>
        public PdfReference Reference
        {
            get { return _iref; }

            // Setting the reference outside PDFsharp is not considered as a valid operation.
            internal set { _iref = value; }
        }
        PdfReference _iref;
    }
}