<?php
/**
 * Class to fake a document tree for CSS matching.
 *
 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
 * @author     LarsDW223
 */

/** Include ecm_interface */
require_once DOKU_INC.'lib/plugins/odt/helper/ecm_interface.php';

/**
 * Class css_doc_element
 * 
 * @package    CSS\CSSDocElement
 */
class css_doc_element implements iElementCSSMatchable {
    /** var Reference to corresponding cssdocument */
    public $doc = NULL;
    /** var Index of this element in the corresponding cssdocument */
    public $index = 0;

    /**
     * Get the name of this element.
     * 
     * @return    string
     */
    public function iECSSM_getName() {
        return $this->doc->entries [$this->index]['element'];
    }

    /**
     * Get the attributes of this element.
     * 
     * @return    array
     */
    public function iECSSM_getAttributes() {
        if(isset($this->index['attributes_array'])) {
            return $this->doc->entries [$this->index]['attributes_array'];
        }
    }

    /**
     * Get the parent of this element.
     * 
     * @return    css_doc_element
     */
    public function iECSSM_getParent() {
        $index = $this->doc->findParent($this->index);
        if ($index == -1 ) {
            return NULL;
        }
        $element = new css_doc_element();
        $element->doc = $this->doc;
        $element->index = $index;
        return $element;
    }

    /**
     * Get the preceding sibling of this element.
     * 
     * @return    css_doc_element
     */
    public function iECSSM_getPrecedingSibling() {
        $index = $this->doc->getPrecedingSibling($this->index);
        if ($index == -1 ) {
            return NULL;
        }
        $element = new css_doc_element();
        $element->doc = $this->doc;
        $element->index = $index;
        return $element;
    }

    /**
     * Does this element belong to pseudo class $class?
     * 
     * @param     string  $class
     * @return    boolean
     */
    public function iECSSM_has_pseudo_class($class) {
        if (!isset($this->doc->entries [$this->index]['pseudo_classes'])) {
            return false;
        }
        $result = array_search($class, 
            $this->doc->entries [$this->index]['pseudo_classes']);
        if ($result === false) {
            return false;
        }
        return true;
    }

    /**
     * Does this element match the pseudo element $element?
     * 
     * @param     string  $element
     * @return    boolean
     */
    public function iECSSM_has_pseudo_element($element) {
        if (!isset($this->doc->entries [$this->index]['pseudo_elements'])) {
            return false;
        }
        $result = array_search($element, 
            $this->doc->entries [$this->index]['pseudo_elements']);
        if ($result === false) {
            return false;
        }
        return true;
    }

    /**
     * Return the CSS properties assigned to this element.
     * (from extern via setProperties())
     * 
     * @return    array
     */
    public function getProperties () {
	    if(isset($this->index['properties'])) {
	        return $this->doc->entries [$this->index]['properties'];
	    }
    }

    /**
     * Set/assign the CSS properties for this element.
     * 
     * @param     array $properties
     */
    public function setProperties (array &$properties) {
        $this->doc->entries [$this->index]['properties'] = $properties;
    }
}

/**
 * Class cssdocument.
 * 
 * @package    CSS\CSSDocument
 */
class cssdocument {
    /** var Current size, Index for next entry */
    public $size = 0;
    /** var Current nesting level */
    public $level = 0;
    /** var Array of entries, see open() */
    public $entries = array ();
    /** var Root index, see saveRootIndex() */
    protected $rootIndex = 0;
    /** var Root level, see saveRootIndex() */
    protected $rootLevel = 0;

    /**
     * Internal function to get the value of an attribute.
     * 
     * @param     string  $value Value of the attribute
     * @param     string  $input Code to parse
     * @param     integer $pos   Current position in $input
     * @param     integer $max   End of $input
     * @return    integer Position at which the attribute ends
     */
    protected function collect_attribute_value (&$value, $input, $pos, $max) {
        $value = '';
        $in_quotes = false;
        $quote = '';
        while ($pos < $max) {
            $sign = $input [$pos];
            $pos++;

            if ($in_quotes == false) {
                if ($sign == '"' || $sign == "'") {
                    $quote = $sign;
                    $in_quotes = true;
                }
            } else {
                if ($sign == $quote) {
                    break;
                }
                $value .= $sign;
            }
        }

        if ($in_quotes == false || $sign != $quote) {
            // No proper quotes, delete value
            $value = NULL;
        }
        
        return $pos;
    }

    /**
     * Internal function to parse $attributes for key="value" pairs
     * and store the result in an array.
     * 
     * @param     string  $attributes Code to parse
     * @return    array Array of attributes
     */
    protected function get_attributes_array ($attributes) {
        if (!isset($attributes)) {
            return NULL;
        }
        
        $result = array();
        $pos = 0;
        $max = strlen($attributes);
        while ($pos < $max) {
            $equal_sign = strpos ($attributes, '=', $pos);
            if ($equal_sign === false) {
                break;
            }
            $att_name = substr ($attributes, $pos, $equal_sign-$pos);
            $att_name = trim ($att_name, ' ');

            $att_end = $this->collect_attribute_value($att_value, $attributes, $equal_sign+1, $max);

            // Add a attribute to array
            $result [$att_name] = $att_value;
            $pos = $att_end + 1;
        }
        return $result;
    }

    /**
     * Save the current position as the root index of the document.
     * It is guaranteed that elements below the root index will not be
     * discarded from the cssdocument.
     */
    public function saveRootIndex () {
        $this->rootIndex = $this->getIndexLastOpened ();
        $this->rootLevel = $this->level-1;
    }

    /**
     * Shrinks/cuts the cssdocument down to its root index.
     */
    public function restoreToRoot () {
        for ($index = $this->size-1 ; $index > $this->rootIndex ; $index--) {
            $this->entries [$index] = NULL;
        }
        $this->size = $this->rootIndex + 1;
        $this->level = $this->rootLevel + 1;
    }

    /**
     * Get the current state of the cssdocument.
     * 
     * @param    array $state    Returned state information
     */
    public function getState (array &$state) {
        $state ['index'] = $this->size-1;
        $state ['level'] = $this->level;
    }

    /**
     * Shrinks/cuts the cssdocument down to the given $state.
     * ($state must be retrieved by calling getState())
     * 
     * @param    array $state    State information
     */
    public function restoreState (array $state) {
        for ($index = $this->size-1 ; $index > $state ['index'] ; $index--) {
            $this->entries [$index] = NULL;
        }
        $this->size = $state ['index'] + 1;
        $this->level = $state ['level'];
    }

    /**
     * Open a new element in the cssdocument.
     * 
     * @param    string $element         The element's name
     * @param    string $attributes      The element's attributes
     * @param    string $pseudo_classes  The element's pseudo classes
     * @param    string $pseudo_elements The element's pseudo elements
     */
    public function open ($element, $attributes=NULL, $pseudo_classes=NULL, $pseudo_elements=NULL) {
        $this->entries [$this->size]['level'] = $this->level;
        $this->entries [$this->size]['state'] = 'open';
        $this->entries [$this->size]['element'] = $element;
        $this->entries [$this->size]['attributes'] = $attributes;
        if (!empty($pseudo_classes)) {
            $this->entries [$this->size]['pseudo_classes'] = explode(' ', $pseudo_classes);
        }
        if (!empty($pseudo_elements)) {
            $this->entries [$this->size]['pseudo_elements'] = explode(' ', $pseudo_elements);
        }
        
        // Build attribute array/parse attributes
        if (isset($attributes)) {
            $this->entries [$this->size]['attributes_array'] =
                $this->get_attributes_array ($attributes);
        }

        $this->size++;
        $this->level++;
    }

    /**
     * Close $element in the cssdocument.
     * 
     * @param    string $element         The element's name
     */
    public function close ($element) {
        $this->level--;
        $this->entries [$this->size]['level'] = $this->level;
        $this->entries [$this->size]['state'] = 'close';
        $this->entries [$this->size]['element'] = $element;
        $this->size++;
    }

    /**
     * Get the current element.
     * 
     * @return css_doc_element
     */
    public function getCurrentElement() {
        $index = $this->getIndexLastOpened ();
        if ($index == -1) {
            return NULL;
        }
        $element = new css_doc_element();
        $element->doc = $this;
        $element->index = $index;
        return $element;
    }
    
    /**
     * Get the entry of internal array $entries at $index.
     * 
     * @param  integer $index
     * @return array
     */
    public function getEntry ($index) {
        if ($index >= $this->size ) {
            return NULL;
        }
        return $this->entries [$index];
    }

    /**
     * Get the current entry of internal array $entries.
     * 
     * @return array
     */
    public function getCurrentEntry () {
        if ($this->size == 0) {
            return NULL;
        }
        return $this->entries [$this->size-1];
    }

    /**
     * Get the index of the 'open' entry of the latest opened element.
     * 
     * @return integer
     */
    public function getIndexLastOpened () {
        if ($this->size == 0) {
            return -1;
        }
        for ($index = $this->size-1 ; $index >= 0 ; $index--) {
            if ($this->entries [$index]['state'] == 'open') {
                return $index;
            }
        }
        return -1;
    }
    
    /**
     * Find the parent for the entry at index $start.
     * 
     * @param    integer $start    Starting point
     */
    public function findParent ($start) {
        if ($this->size == 0 || $start >= $this->size) {
            return -1;
        }
        $start_level = $this->entries [$start]['level'];
        if ($start_level == 0) {
            return -1;
        }
        for ($index = $start-1 ; $index >= 0 ; $index--) {
            if ($this->entries [$index]['state'] == 'open'
                &&
                $this->entries [$index]['level'] == $start_level-1) {
                return $index;
            }
        }
        return -1;
    }

    /**
     * Find the preceding sibling for the entry at index $current.
     * 
     * @param    integer $current    Starting point
     */
    public function getPrecedingSibling ($current) {
        if ($this->size == 0 || $current >= $this->size || $current == 0) {
            return -1;
        }
        $current_level = $this->entries [$current]['level'];
        if ($this->entries [$current-1]['level'] == $current_level) {
            return ($current-1);
        }
        return -1;
    }
    
    /**
     * Dump the current elements/entries in this cssdocument.
     * Only for debugging purposes.
     */
    public function getDump () {
        $dump = '';
        $dump .= 'RootLevel: '.$this->rootLevel.', RootIndex: '.$this->rootIndex."\n";
        for ($index = 0 ; $index < $this->size ; $index++) {
            $element = $this->entries [$index];
            $dump .= str_repeat(' ', $element ['level'] * 2);
            if ($this->entries [$index]['state'] == 'open') {
                $dump .= '<'.$element ['element'];
                $dump .= ' '.$element ['attributes'].'>';
            } else {
                $dump .= '</'.$element ['element'].'>';
            }
            $dump .= ' (Level: '.$element ['level'].')';
            $dump .= "\n";
        }
        return $dump;
    }

    /**
     * Remove the current entry.
     */
    public function removeCurrent () {
        $index = $this->size-1;
        if ($index <= $this->rootIndex) {
            // Do not remove root elements!
            return;
        }
        $this->level = $this->entries [$index]['level'];
        $this->entries [$index] = NULL;
        $this->size--;
    }
}
