<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Pdf
 * @subpackage Actions
 * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id$
 */


/** Internally used classes */
require_once 'Zend/Pdf/Element.php';
require_once 'Zend/Pdf/Element/Array.php';


/** Zend_Pdf_Target */
require_once 'Zend/Pdf/Target.php';


/**
 * Abstract PDF action representation class
 *
 * @package    Zend_Pdf
 * @subpackage Actions
 * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
abstract class Zend_Pdf_Action extends Zend_Pdf_Target implements RecursiveIterator, Countable
{
    /**
     * Array of child outlines (array of Zend_Pdf_Outline objects)
     * @var array
     */
    public $childOutlines = [];

    /**
     * Action dictionary
     *
     * @var Zend_Pdf_Element_Dictionary|Zend_Pdf_Element_Object|Zend_Pdf_Element_Reference
     */
    protected $_actionDictionary;

    /**
     * An original list of chained actions
     *
     * @var array  Array of Zend_Pdf_Action objects
     */
    protected $_originalNextList;

    /**
     * A list of next actions in actions tree (used for actions chaining)
     *
     * @var array  Array of Zend_Pdf_Action objects
     */
    public $next = [];

    /**
     * Object constructor
     *
     * @param Zend_Pdf_Element_Dictionary $dictionary
     * @param SplObjectStorage            $processedActions  list of already processed action dictionaries, used to avoid cyclic references
     * @throws Zend_Pdf_Exception
     */
    public function __construct(Zend_Pdf_Element $dictionary, SplObjectStorage $processedActions)
    {
        require_once 'Zend/Pdf/Element.php';
        if ($dictionary->getType() != Zend_Pdf_Element::TYPE_DICTIONARY) {
            require_once 'Zend/Pdf/Exception.php';
            throw new Zend_Pdf_Exception('$dictionary mast be a direct or an indirect dictionary object.');
        }

        $this->_actionDictionary = $dictionary;

        if ($dictionary->Next !== null) {
            if ($dictionary->Next instanceof Zend_Pdf_Element_Dictionary) {
                // Check if dictionary object is not already processed
                if (!$processedActions->contains($dictionary->Next)) {
                    $processedActions->attach($dictionary->Next);
                    $this->next[] = Zend_Pdf_Action::load($dictionary->Next, $processedActions);
                }
            } else if ($dictionary->Next instanceof Zend_Pdf_Element_Array) {
                foreach ($dictionary->Next->items as $chainedActionDictionary) {
                    // Check if dictionary object is not already processed
                    if (!$processedActions->contains($chainedActionDictionary)) {
                        $processedActions->attach($chainedActionDictionary);
                        $this->next[] = Zend_Pdf_Action::load($chainedActionDictionary, $processedActions);
                    }
                }
            } else {
                require_once 'Zend/Pdf/Exception.php';
                throw new Zend_Pdf_Exception('PDF Action dictionary Next entry must be a dictionary or an array.');
            }
        }

        $this->_originalNextList = $this->next;
    }

    /**
     * Load PDF action object using specified dictionary
     *
     * @internal
     * @param Zend_Pdf_Element $dictionary (It's actually Dictionary or Dictionary Object or Reference to a Dictionary Object)
     * @param SplObjectStorage $processedActions  list of already processed action dictionaries, used to avoid cyclic references
     * @return Zend_Pdf_Action
     * @throws Zend_Pdf_Exception
     */
    public static function load(Zend_Pdf_Element $dictionary, SplObjectStorage $processedActions = null)
    {
        if ($processedActions === null) {
            $processedActions = new SplObjectStorage();
        }

        require_once 'Zend/Pdf/Element.php';
        if ($dictionary->getType() != Zend_Pdf_Element::TYPE_DICTIONARY) {
            require_once 'Zend/Pdf/Exception.php';
            throw new Zend_Pdf_Exception('$dictionary mast be a direct or an indirect dictionary object.');
        }
        if (isset($dictionary->Type)  &&  $dictionary->Type->value != 'Action') {
            require_once 'Zend/Pdf/Exception.php';
            throw new Zend_Pdf_Exception('Action dictionary Type entry must be set to \'Action\'.');
        }

        if ($dictionary->S === null) {
            require_once 'Zend/Pdf/Exception.php';
            throw new Zend_Pdf_Exception('Action dictionary must contain S entry');
        }

        switch ($dictionary->S->value) {
            case 'GoTo':
                require_once 'Zend/Pdf/Action/GoTo.php';
                return new Zend_Pdf_Action_GoTo($dictionary, $processedActions);
                break;

            case 'GoToR':
                require_once 'Zend/Pdf/Action/GoToR.php';
                return new Zend_Pdf_Action_GoToR($dictionary, $processedActions);
                break;

            case 'GoToE':
                require_once 'Zend/Pdf/Action/GoToE.php';
                return new Zend_Pdf_Action_GoToE($dictionary, $processedActions);
                break;

            case 'Launch':
                require_once 'Zend/Pdf/Action/Launch.php';
                return new Zend_Pdf_Action_Launch($dictionary, $processedActions);
                break;

            case 'Thread':
                require_once 'Zend/Pdf/Action/Thread.php';
                return new Zend_Pdf_Action_Thread($dictionary, $processedActions);
                break;

            case 'URI':
                require_once 'Zend/Pdf/Action/URI.php';
                return new Zend_Pdf_Action_URI($dictionary, $processedActions);
                break;

            case 'Sound':
                require_once 'Zend/Pdf/Action/Sound.php';
                return new Zend_Pdf_Action_Sound($dictionary, $processedActions);
                break;

            case 'Movie':
                require_once 'Zend/Pdf/Action/Movie.php';
                return new Zend_Pdf_Action_Movie($dictionary, $processedActions);
                break;

            case 'Hide':
                require_once 'Zend/Pdf/Action/Hide.php';
                return new Zend_Pdf_Action_Hide($dictionary, $processedActions);
                break;

            case 'Named':
                require_once 'Zend/Pdf/Action/Named.php';
                return new Zend_Pdf_Action_Named($dictionary, $processedActions);
                break;

            case 'SubmitForm':
                require_once 'Zend/Pdf/Action/SubmitForm.php';
                return new Zend_Pdf_Action_SubmitForm($dictionary, $processedActions);
                break;

            case 'ResetForm':
                require_once 'Zend/Pdf/Action/ResetForm.php';
                return new Zend_Pdf_Action_ResetForm($dictionary, $processedActions);
                break;

            case 'ImportData':
                require_once 'Zend/Pdf/Action/ImportData.php';
                return new Zend_Pdf_Action_ImportData($dictionary, $processedActions);
                break;

            case 'JavaScript':
                require_once 'Zend/Pdf/Action/JavaScript.php';
                return new Zend_Pdf_Action_JavaScript($dictionary, $processedActions);
                break;

            case 'SetOCGState':
                require_once 'Zend/Pdf/Action/SetOCGState.php';
                return new Zend_Pdf_Action_SetOCGState($dictionary, $processedActions);
                break;

            case 'Rendition':
                require_once 'Zend/Pdf/Action/Rendition.php';
                return new Zend_Pdf_Action_Rendition($dictionary, $processedActions);
                break;

            case 'Trans':
                require_once 'Zend/Pdf/Action/Trans.php';
                return new Zend_Pdf_Action_Trans($dictionary, $processedActions);
                break;

            case 'GoTo3DView':
                require_once 'Zend/Pdf/Action/GoTo3DView.php';
                return new Zend_Pdf_Action_GoTo3DView($dictionary, $processedActions);
                break;

            default:
                require_once 'Zend/Pdf/Action/Unknown.php';
                return new Zend_Pdf_Action_Unknown($dictionary, $processedActions);
                break;
        }
    }

    /**
     * Get resource
     *
     * @internal
     * @return Zend_Pdf_Element
     */
    public function getResource()
    {
        return $this->_actionDictionary;
    }

    /**
     * Dump Action and its child actions into PDF structures
     *
     * Returns dictionary indirect object or reference
     *
     * @internal
     * @param Zend_Pdf_ElementFactory $factory   Object factory for newly created indirect objects
     * @param SplObjectStorage $processedActions  list of already processed actions (used to prevent infinity loop caused by cyclic references)
     * @return Zend_Pdf_Element_Object|Zend_Pdf_Element_Reference   Dictionary indirect object
     */
    public function dumpAction(Zend_Pdf_ElementFactory_Interface $factory, SplObjectStorage $processedActions = null)
    {
        if ($processedActions === null) {
            $processedActions = new SplObjectStorage();
        }
        if ($processedActions->contains($this)) {
            require_once 'Zend/Pdf/Exception.php';
            throw new Zend_Pdf_Exception('Action chain cyclyc reference is detected.');
        }
        $processedActions->attach($this);

        $childListUpdated = false;
        if (count($this->_originalNextList) != count($this->next)) {
            // If original and current children arrays have different size then children list was updated
            $childListUpdated = true;
        } else if ( !(array_keys($this->_originalNextList) === array_keys($this->next)) ) {
            // If original and current children arrays have different keys (with a glance to an order) then children list was updated
            $childListUpdated = true;
        } else {
            foreach ($this->next as $key => $childAction) {
                if ($this->_originalNextList[$key] !== $childAction) {
                    $childListUpdated = true;
                    break;
                }
            }
        }

        if ($childListUpdated) {
            $this->_actionDictionary->touch();
            switch (count($this->next)) {
                case 0:
                    $this->_actionDictionary->Next = null;
                    break;

                case 1:
                    $child = reset($this->next);
                    $this->_actionDictionary->Next = $child->dumpAction($factory, $processedActions);
                    break;

                default:
                    require_once 'Zend/Pdf/Element/Array.php';
                    $pdfChildArray = new Zend_Pdf_Element_Array();
                    foreach ($this->next as $child) {

                        $pdfChildArray->items[] = $child->dumpAction($factory, $processedActions);
                    }
                    $this->_actionDictionary->Next = $pdfChildArray;
                    break;
            }
        } else {
            foreach ($this->next as $child) {
                $child->dumpAction($factory, $processedActions);
            }
        }

        if ($this->_actionDictionary instanceof Zend_Pdf_Element_Dictionary) {
            // It's a newly created action. Register it within object factory and return indirect object
            return $factory->newObject($this->_actionDictionary);
        } else {
            // It's a loaded object
            return $this->_actionDictionary;
        }
    }


    ////////////////////////////////////////////////////////////////////////
    //  RecursiveIterator interface methods
    //////////////

    /**
     * Returns current child action.
     *
     * @return Zend_Pdf_Action
     */
    #[\ReturnTypeWillChange]
    public function current()
    {
        return current($this->next);
    }

    /**
     * Returns current iterator key
     *
     * @return integer
     */
    #[\ReturnTypeWillChange]
    public function key()
    {
        return key($this->next);
    }

    /**
     * Go to next child
     */
    #[\ReturnTypeWillChange]
    public function next()
    {
        return next($this->next);
    }

    /**
     * Rewind children
     */
    #[\ReturnTypeWillChange]
    public function rewind()
    {
        return reset($this->next);
    }

    /**
     * Check if current position is valid
     *
     * @return boolean
     */
    public function valid(): bool
    {
        return current($this->next) !== false;
    }

    /**
     * Returns the child action.
     *
     * @return Zend_Pdf_Action|null
     */
    public function getChildren(): ?\RecursiveIterator
    {
        return current($this->next);
    }

    /**
     * Implements RecursiveIterator interface.
     *
     * @return bool  whether container has any pages
     */
    public function hasChildren(): bool
    {
        return count($this->next) > 0;
    }


    ////////////////////////////////////////////////////////////////////////
    //  Countable interface methods
    //////////////

    /**
     * count()
     *
     * @return int
     */
    public function count(): int
    {
        return count($this->childOutlines);
    }
}
