<?php
/**
 * phpQuery is a server-side, chainable, CSS3 selector driven
 * Document Object Model (DOM) API based on jQuery JavaScript Library.
 *
 * @version 0.9.5
 * @link http://code.google.com/p/phpquery/
 * @link http://phpquery-library.blogspot.com/
 * @link http://jquery.com/
 * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
 * @license http://www.opensource.org/licenses/mit-license.php MIT License
 * @package phpQuery
 */

// class names for instanceof
// TODO move them as class constants into phpQuery
define('DOMDOCUMENT', 'DOMDocument');
define('DOMELEMENT', 'DOMElement');
define('DOMNODELIST', 'DOMNodeList');
define('DOMNODE', 'DOMNode');
require_once (dirname(__FILE__) . '/phpQuery/DOMEvent.php');
require_once (dirname(__FILE__) . '/phpQuery/DOMDocumentWrapper.php');
require_once (dirname(__FILE__) . '/phpQuery/phpQueryEvents.php');
require_once (dirname(__FILE__) . '/phpQuery/Callback.php');
require_once (dirname(__FILE__) . '/phpQuery/phpQueryObject.php');
require_once (dirname(__FILE__) . '/phpQuery/compat/mbstring.php');

/**
 * Static namespace for phpQuery functions.
 *
 * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
 * @package phpQuery
 */
abstract class phpQuery
{

    /**
     * XXX: Workaround for mbstring problems
     *
     * @var bool
     */
    public static $mbstringSupport = true;

    public static $debug = false;

    public static $documents = array();

    public static $defaultDocumentID = null;
    // public static $defaultDoctype = 'html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"';
    /**
     * Applies only to HTML.
     *
     * @var unknown_type
     */
    public static $defaultDoctype = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">';

    public static $defaultCharset = 'UTF-8';

    /**
     * Static namespace for plugins.
     *
     * @var object
     */
    public static $plugins = array();

    /**
     * List of loaded plugins.
     *
     * @var unknown_type
     */
    public static $pluginsLoaded = array();

    public static $pluginsMethods = array();

    public static $pluginsStaticMethods = array();

    public static $extendMethods = array();

    /**
     * @TODO implement
     */
    public static $extendStaticMethods = array();

    /**
     * Hosts allowed for AJAX connections.
     * Dot '.' means $_SERVER['HTTP_HOST'] (if any).
     *
     * @var array
     */
    public static $ajaxAllowedHosts = array(
        '.'
    );

    /**
     * AJAX settings.
     *
     * @var array XXX should it be static or not ?
     */
    public static $ajaxSettings = array(
        'url' => '', // TODO
        'global' => true,
        'type' => "GET",
        'timeout' => null,
        'contentType' => "application/x-www-form-urlencoded",
        'processData' => true,
        // 'async' => true,
        'data' => null,
        'username' => null,
        'password' => null,
        'dataType' => null,
        'ifModified' => null,
        'accepts' => array(
            'xml' => "application/xml, text/xml",
            'html' => "text/html",
            'script' => "text/javascript, application/javascript",
            'json' => "application/json, text/javascript",
            'text' => "text/plain",
            '_default' => "*/*"
        )
    );

    public static $lastModified = null;

    public static $active = 0;

    public static $dumpCount = 0;

    /**
     * Multi-purpose function.
     * Use pq() as shortcut.
     *
     * In below examples, $pq is any result of pq(); function.
     *
     * 1. Import markup into existing document (without any attaching):
     * - Import into selected document:
     * pq('<div/>') // DOESNT accept text nodes at beginning of input string !
     * - Import into document with ID from $pq->getDocumentID():
     * pq('<div/>', $pq->getDocumentID())
     * - Import into same document as DOMNode belongs to:
     * pq('<div/>', DOMNode)
     * - Import into document from phpQuery object:
     * pq('<div/>', $pq)
     *
     * 2. Run query:
     * - Run query on last selected document:
     * pq('div.myClass')
     * - Run query on document with ID from $pq->getDocumentID():
     * pq('div.myClass', $pq->getDocumentID())
     * - Run query on same document as DOMNode belongs to and use node(s)as root for query:
     * pq('div.myClass', DOMNode)
     * - Run query on document from phpQuery object
     * and use object's stack as root node(s) for query:
     * pq('div.myClass', $pq)
     *
     * @param string|DOMNode|DOMNodeList|array $arg1
     *            HTML markup, CSS Selector, DOMNode or array of DOMNodes
     * @param string|phpQueryObject|DOMNode $context
     *            DOM ID from $pq->getDocumentID(), phpQuery object (determines also query root) or DOMNode (determines also query root)
     *            
     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery|QueryTemplatesPhpQuery|false phpQuery object or false in case of error.
     */
    public static function pq($arg1, $context = null)
    {
        if ($arg1 instanceof DOMNODE && ! isset($context)) {
            foreach (phpQuery::$documents as $documentWrapper) {
                $compare = $arg1 instanceof DOMDocument ? $arg1 : $arg1->ownerDocument;
                if ($documentWrapper->document->isSameNode($compare))
                    $context = $documentWrapper->id;
            }
        }
        if (! $context) {
            $domId = self::$defaultDocumentID;
            if (! $domId)
                throw new Exception("Can't use last created DOM, because there isn't any. Use phpQuery::newDocument() first.");
            // } else if (is_object($context) && ($context instanceof PHPQUERY || is_subclass_of($context, 'phpQueryObject')))
        } else 
            if (is_object($context) && $context instanceof phpQueryObject)
                $domId = $context->getDocumentID();
            else 
                if ($context instanceof DOMDOCUMENT) {
                    $domId = self::getDocumentID($context);
                    if (! $domId) {
                        // throw new Exception('Orphaned DOMDocument');
                        $domId = self::newDocument($context)->getDocumentID();
                    }
                } else 
                    if ($context instanceof DOMNODE) {
                        $domId = self::getDocumentID($context);
                        if (! $domId) {
                            throw new Exception('Orphaned DOMNode');
                            // $domId = self::newDocument($context->ownerDocument);
                        }
                    } else
                        $domId = $context;
        if ($arg1 instanceof phpQueryObject) {
            // if (is_object($arg1) && (get_class($arg1) == 'phpQueryObject' || $arg1 instanceof PHPQUERY || is_subclass_of($arg1, 'phpQueryObject'))) {
            /**
             * Return $arg1 or import $arg1 stack if document differs:
             * pq(pq('<div/>'))
             */
            if ($arg1->getDocumentID() == $domId)
                return $arg1;
            $class = get_class($arg1);
            // support inheritance by passing old object to overloaded constructor
            $phpQuery = $class != 'phpQuery' ? new $class($arg1, $domId) : new phpQueryObject($domId);
            $phpQuery->elements = array();
            foreach ($arg1->elements as $node)
                $phpQuery->elements[] = $phpQuery->document->importNode($node, true);
            return $phpQuery;
        } else 
            if ($arg1 instanceof DOMNODE || (is_array($arg1) && isset($arg1[0]) && $arg1[0] instanceof DOMNODE)) {
                /*
                 * Wrap DOM nodes with phpQuery object, import into document when needed:
                 * pq(array($domNode1, $domNode2))
                 */
                $phpQuery = new phpQueryObject($domId);
                if (! ($arg1 instanceof DOMNODELIST) && ! is_array($arg1))
                    $arg1 = array(
                        $arg1
                    );
                $phpQuery->elements = array();
                foreach ($arg1 as $node) {
                    $sameDocument = $node->ownerDocument instanceof DOMDOCUMENT && ! $node->ownerDocument->isSameNode($phpQuery->document);
                    $phpQuery->elements[] = $sameDocument ? $phpQuery->document->importNode($node, true) : $node;
                }
                return $phpQuery;
            } else 
                if (self::isMarkup($arg1)) {
                    /**
                     * Import HTML:
                     * pq('<div/>')
                     */
                    $phpQuery = new phpQueryObject($domId);
                    return $phpQuery->newInstance($phpQuery->documentWrapper->import($arg1));
                } else {
                    /**
                     * Run CSS query:
                     * pq('div.myClass')
                     */
                    $phpQuery = new phpQueryObject($domId);
                    // if ($context && ($context instanceof PHPQUERY || is_subclass_of($context, 'phpQueryObject')))
                    if ($context && $context instanceof phpQueryObject)
                        $phpQuery->elements = $context->elements;
                    else 
                        if ($context && $context instanceof DOMNODELIST) {
                            $phpQuery->elements = array();
                            foreach ($context as $node)
                                $phpQuery->elements[] = $node;
                        } else 
                            if ($context && $context instanceof DOMNODE)
                                $phpQuery->elements = array(
                                    $context
                                );
                    return $phpQuery->find($arg1);
                }
    }

    /**
     * Sets default document to $id.
     * Document has to be loaded prior
     * to using this method.
     * $id can be retrived via getDocumentID() or getDocumentIDRef().
     *
     * @param unknown_type $id            
     */
    public static function selectDocument($id)
    {
        $id = self::getDocumentID($id);
        self::debug("Selecting document '$id' as default one");
        self::$defaultDocumentID = self::getDocumentID($id);
    }

    /**
     * Returns document with id $id or last used as phpQueryObject.
     * $id can be retrived via getDocumentID() or getDocumentIDRef().
     * Chainable.
     *
     * @see phpQuery::selectDocument()
     * @param unknown_type $id            
     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
     */
    public static function getDocument($id = null)
    {
        if ($id)
            phpQuery::selectDocument($id);
        else
            $id = phpQuery::$defaultDocumentID;
        return new phpQueryObject($id);
    }

    /**
     * Creates new document from markup.
     * Chainable.
     *
     * @param unknown_type $markup            
     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
     */
    public static function newDocument($markup = null, $contentType = null)
    {
        if (! $markup)
            $markup = '';
        $documentID = phpQuery::createDocumentWrapper($markup, $contentType);
        return new phpQueryObject($documentID);
    }

    /**
     * Creates new document from markup.
     * Chainable.
     *
     * @param unknown_type $markup            
     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
     */
    public static function newDocumentHTML($markup = null, $charset = null)
    {
        $contentType = $charset ? ";charset=$charset" : '';
        return self::newDocument($markup, "text/html{$contentType}");
    }

    /**
     * Creates new document from markup.
     * Chainable.
     *
     * @param unknown_type $markup            
     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
     */
    public static function newDocumentXML($markup = null, $charset = null)
    {
        $contentType = $charset ? ";charset=$charset" : '';
        return self::newDocument($markup, "text/xml{$contentType}");
    }

    /**
     * Creates new document from markup.
     * Chainable.
     *
     * @param unknown_type $markup            
     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
     */
    public static function newDocumentXHTML($markup = null, $charset = null)
    {
        $contentType = $charset ? ";charset=$charset" : '';
        return self::newDocument($markup, "application/xhtml+xml{$contentType}");
    }

    /**
     * Creates new document from markup.
     * Chainable.
     *
     * @param unknown_type $markup            
     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
     */
    public static function newDocumentPHP($markup = null, $contentType = "text/html")
    {
        // TODO pass charset to phpToMarkup if possible (use DOMDocumentWrapper function)
        $markup = phpQuery::phpToMarkup($markup, self::$defaultCharset);
        return self::newDocument($markup, $contentType);
    }

    public static function phpToMarkup($php, $charset = 'utf-8')
    {
        $regexes = array(
            '@(<(?!\\?)(?:[^>]|\\?>)+\\w+\\s*=\\s*)(\')([^\']*)<' . '?php?(.*?)(?:\\?>)([^\']*)\'@s',
            '@(<(?!\\?)(?:[^>]|\\?>)+\\w+\\s*=\\s*)(")([^"]*)<' . '?php?(.*?)(?:\\?>)([^"]*)"@s'
        );
        foreach ($regexes as $regex)
            while (preg_match($regex, $php, $matches)) {
                $php = preg_replace_callback($regex, 
                    // create_function('$m, $charset = "'.$charset.'"',
                    // 'return $m[1].$m[2]
                    // .htmlspecialchars("<"."?php".$m[4]."?".">", ENT_QUOTES|ENT_NOQUOTES, $charset)
                    // .$m[5].$m[2];'
                    // ),
                    array(
                        'phpQuery',
                        '_phpToMarkupCallback'
                    ), $php);
            }
        $regex = '@(^|>[^<]*)+?(<\?php(.*?)(\?>))@s';
        // preg_match_all($regex, $php, $matches);
        // var_dump($matches);
        $php = preg_replace($regex, '\\1<php><!-- \\3 --></php>', $php);
        return $php;
    }

    public static function _phpToMarkupCallback($php, $charset = 'utf-8')
    {
        return $m[1] . $m[2] . htmlspecialchars("<" . "?php" . $m[4] . "?" . ">", ENT_QUOTES | ENT_NOQUOTES, $charset) . $m[5] . $m[2];
    }

    public static function _markupToPHPCallback($m)
    {
        return "<" . "?php " . htmlspecialchars_decode($m[1]) . " ?" . ">";
    }

    /**
     * Converts document markup containing PHP code generated by phpQuery::php()
     * into valid (executable) PHP code syntax.
     *
     * @param string|phpQueryObject $content            
     * @return string PHP code.
     */
    public static function markupToPHP($content)
    {
        if ($content instanceof phpQueryObject)
            $content = $content->markupOuter();
            /* <php>...</php> to <?php...? > */
        $content = preg_replace_callback('@<php>\s*<!--(.*?)-->\s*</php>@s', 
            // create_function('$m',
            // 'return "<'.'?php ".htmlspecialchars_decode($m[1])." ?'.'>";'
            // ),
            array(
                'phpQuery',
                '_markupToPHPCallback'
            ), $content);
        /* <node attr='< ?php ? >'> extra space added to save highlighters */
        $regexes = array(
            '@(<(?!\\?)(?:[^>]|\\?>)+\\w+\\s*=\\s*)(\')([^\']*)(?:&lt;|%3C)\\?(?:php)?(.*?)(?:\\?(?:&gt;|%3E))([^\']*)\'@s',
            '@(<(?!\\?)(?:[^>]|\\?>)+\\w+\\s*=\\s*)(")([^"]*)(?:&lt;|%3C)\\?(?:php)?(.*?)(?:\\?(?:&gt;|%3E))([^"]*)"@s'
        );
        foreach ($regexes as $regex)
            while (preg_match($regex, $content))
                $content = preg_replace_callback($regex, create_function('$m', 'return $m[1].$m[2].$m[3]."<?php "
							.str_replace(
								array("%20", "%3E", "%09", "&#10;", "&#9;", "%7B", "%24", "%7D", "%22", "%5B", "%5D"),
								array(" ", ">", "	", "\n", "	", "{", "$", "}", \'"\', "[", "]"),
								htmlspecialchars_decode($m[4])
							)
							." ?>".$m[5].$m[2];'), $content);
        return $content;
    }

    /**
     * Creates new document from file $file.
     * Chainable.
     *
     * @param string $file
     *            URLs allowed. See File wrapper page at php.net for more supported sources.
     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
     */
    public static function newDocumentFile($file, $contentType = null)
    {
        $documentID = self::createDocumentWrapper(file_get_contents($file), $contentType);
        return new phpQueryObject($documentID);
    }

    /**
     * Creates new document from markup.
     * Chainable.
     *
     * @param unknown_type $markup            
     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
     */
    public static function newDocumentFileHTML($file, $charset = null)
    {
        $contentType = $charset ? ";charset=$charset" : '';
        return self::newDocumentFile($file, "text/html{$contentType}");
    }

    /**
     * Creates new document from markup.
     * Chainable.
     *
     * @param unknown_type $markup            
     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
     */
    public static function newDocumentFileXML($file, $charset = null)
    {
        $contentType = $charset ? ";charset=$charset" : '';
        return self::newDocumentFile($file, "text/xml{$contentType}");
    }

    /**
     * Creates new document from markup.
     * Chainable.
     *
     * @param unknown_type $markup            
     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
     */
    public static function newDocumentFileXHTML($file, $charset = null)
    {
        $contentType = $charset ? ";charset=$charset" : '';
        return self::newDocumentFile($file, "application/xhtml+xml{$contentType}");
    }

    /**
     * Creates new document from markup.
     * Chainable.
     *
     * @param unknown_type $markup            
     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
     */
    public static function newDocumentFilePHP($file, $contentType = null)
    {
        return self::newDocumentPHP(file_get_contents($file), $contentType);
    }

    /**
     * Reuses existing DOMDocument object.
     * Chainable.
     *
     * @param $document DOMDocument            
     * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery @TODO support DOMDocument
     */
    public static function loadDocument($document)
    {
        // TODO
        die('TODO loadDocument');
    }

    /**
     * Enter description here...
     *
     * @param unknown_type $html            
     * @param unknown_type $domId            
     * @return unknown New DOM ID
     * @todo support PHP tags in input
     * @todo support passing DOMDocument object from self::loadDocument
     */
    protected static function createDocumentWrapper($html, $contentType = null, $documentID = null)
    {
        if (function_exists('domxml_open_mem'))
            throw new Exception("Old PHP4 DOM XML extension detected. phpQuery won't work until this extension is enabled.");
            // $id = $documentID
            // ? $documentID
            // : md5(microtime());
        $document = null;
        if ($html instanceof DOMDOCUMENT) {
            if (self::getDocumentID($html)) {
                // document already exists in phpQuery::$documents, make a copy
                $document = clone $html;
            } else {
                // new document, add it to phpQuery::$documents
                $wrapper = new DOMDocumentWrapper($html, $contentType, $documentID);
            }
        } else {
            $wrapper = new DOMDocumentWrapper($html, $contentType, $documentID);
        }
        // $wrapper->id = $id;
        // bind document
        phpQuery::$documents[$wrapper->id] = $wrapper;
        // remember last loaded document
        phpQuery::selectDocument($wrapper->id);
        return $wrapper->id;
    }

    /**
     * Extend class namespace.
     *
     * @param string|array $target            
     * @param array $source
     *            @TODO support string $source
     * @return unknown_type
     */
    public static function extend($target, $source)
    {
        switch ($target) {
            case 'phpQueryObject':
                $targetRef = &self::$extendMethods;
                $targetRef2 = &self::$pluginsMethods;
                break;
            case 'phpQuery':
                $targetRef = &self::$extendStaticMethods;
                $targetRef2 = &self::$pluginsStaticMethods;
                break;
            default:
                throw new Exception("Unsupported \$target type");
        }
        if (is_string($source))
            $source = array(
                $source => $source
            );
        foreach ($source as $method => $callback) {
            if (isset($targetRef[$method])) {
                // throw new Exception
                self::debug("Duplicate method '{$method}', can\'t extend '{$target}'");
                continue;
            }
            if (isset($targetRef2[$method])) {
                // throw new Exception
                self::debug("Duplicate method '{$method}' from plugin '{$targetRef2[$method]}'," . " can\'t extend '{$target}'");
                continue;
            }
            $targetRef[$method] = $callback;
        }
        return true;
    }

    /**
     * Extend phpQuery with $class from $file.
     *
     * @param string $class
     *            Extending class name. Real class name can be prepended phpQuery_.
     * @param string $file
     *            Filename to include. Defaults to "{$class}.php".
     */
    public static function plugin($class, $file = null)
    {
        // TODO $class checked agains phpQuery_$class
        // if (strpos($class, 'phpQuery') === 0)
        // $class = substr($class, 8);
        if (in_array($class, self::$pluginsLoaded))
            return true;
        if (! $file)
            $file = $class . '.php';
        $objectClassExists = class_exists('phpQueryObjectPlugin_' . $class);
        $staticClassExists = class_exists('phpQueryPlugin_' . $class);
        if (! $objectClassExists && ! $staticClassExists)
            require_once ($file);
        self::$pluginsLoaded[] = $class;
        // static methods
        if (class_exists('phpQueryPlugin_' . $class)) {
            $realClass = 'phpQueryPlugin_' . $class;
            $vars = get_class_vars($realClass);
            $loop = isset($vars['phpQueryMethods']) && ! is_null($vars['phpQueryMethods']) ? $vars['phpQueryMethods'] : get_class_methods($realClass);
            foreach ($loop as $method) {
                if ($method == '__initialize')
                    continue;
                if (! is_callable(array(
                    $realClass,
                    $method
                )))
                    continue;
                if (isset(self::$pluginsStaticMethods[$method])) {
                    throw new Exception("Duplicate method '{$method}' from plugin '{$c}' conflicts with same method from plugin '" . self::$pluginsStaticMethods[$method] . "'");
                    return;
                }
                self::$pluginsStaticMethods[$method] = $class;
            }
            if (method_exists($realClass, '__initialize'))
                call_user_func_array(array(
                    $realClass,
                    '__initialize'
                ), array());
        }
        // object methods
        if (class_exists('phpQueryObjectPlugin_' . $class)) {
            $realClass = 'phpQueryObjectPlugin_' . $class;
            $vars = get_class_vars($realClass);
            $loop = isset($vars['phpQueryMethods']) && ! is_null($vars['phpQueryMethods']) ? $vars['phpQueryMethods'] : get_class_methods($realClass);
            foreach ($loop as $method) {
                if (! is_callable(array(
                    $realClass,
                    $method
                )))
                    continue;
                if (isset(self::$pluginsMethods[$method])) {
                    throw new Exception("Duplicate method '{$method}' from plugin '{$c}' conflicts with same method from plugin '" . self::$pluginsMethods[$method] . "'");
                    continue;
                }
                self::$pluginsMethods[$method] = $class;
            }
        }
        return true;
    }

    /**
     * Unloades all or specified document from memory.
     *
     * @param mixed $documentID            
     * @see phpQuery::getDocumentID() for supported types.
     */
    public static function unloadDocuments($id = null)
    {
        if (isset($id)) {
            if ($id = self::getDocumentID($id))
                unset(phpQuery::$documents[$id]);
        } else {
            foreach (phpQuery::$documents as $k => $v) {
                unset(phpQuery::$documents[$k]);
            }
        }
    }

    /**
     * Parses phpQuery object or HTML result against PHP tags and makes them active.
     *
     * @param phpQuery|string $content            
     * @deprecated
     *
     * @return string
     */
    public static function unsafePHPTags($content)
    {
        return self::markupToPHP($content);
    }

    public static function DOMNodeListToArray($DOMNodeList)
    {
        $array = array();
        if (! $DOMNodeList)
            return $array;
        foreach ($DOMNodeList as $node)
            $array[] = $node;
        return $array;
    }

    /**
     * Checks if $input is HTML string, which has to start with '<'.
     *
     * @deprecated
     *
     * @param String $input            
     * @return Bool
     * @todo still used ?
     */
    public static function isMarkup($input)
    {
        return ! is_array($input) && substr(trim($input), 0, 1) == '<';
    }

    public static function debug($text)
    {
        if (self::$debug)
            print var_dump($text);
    }

    /**
     * Make an AJAX request.
     *
     * @param
     *            array See $options http://docs.jquery.com/Ajax/jQuery.ajax#toptions
     *            Additional options are:
     *            'document' - document for global events, @see phpQuery::getDocumentID()
     *            'referer' - implemented
     *            'requested_with' - TODO; not implemented (X-Requested-With)
     * @return Zend_Http_Client
     * @link http://docs.jquery.com/Ajax/jQuery.ajax
     *      
     *       @TODO $options['cache']
     *       @TODO $options['processData']
     *       @TODO $options['xhr']
     *       @TODO $options['data'] as string
     *       @TODO XHR interface
     */
    public static function ajax($options = array(), $xhr = null)
    {
        $options = array_merge(self::$ajaxSettings, $options);
        $documentID = isset($options['document']) ? self::getDocumentID($options['document']) : null;
        if ($xhr) {
            // reuse existing XHR object, but clean it up
            $client = $xhr;
            // $client->setParameterPost(null);
            // $client->setParameterGet(null);
            $client->setAuth(false);
            $client->setHeaders("If-Modified-Since", null);
            $client->setHeaders("Referer", null);
            $client->resetParameters();
        } else {
            // create new XHR object
            require_once ('Zend/Http/Client.php');
            $client = new Zend_Http_Client();
            $client->setCookieJar();
        }
        if (isset($options['timeout']))
            $client->setConfig(array(
                'timeout' => $options['timeout']
            ));
            // 'maxredirects' => 0,
        foreach (self::$ajaxAllowedHosts as $k => $host)
            if ($host == '.' && isset($_SERVER['HTTP_HOST']))
                self::$ajaxAllowedHosts[$k] = $_SERVER['HTTP_HOST'];
        $host = parse_url($options['url'], PHP_URL_HOST);
        if (! in_array($host, self::$ajaxAllowedHosts)) {
            throw new Exception("Request not permitted, host '$host' not present in " . "phpQuery::\$ajaxAllowedHosts");
        }
        // JSONP
        $jsre = "/=\\?(&|$)/";
        if (isset($options['dataType']) && $options['dataType'] == 'jsonp') {
            $jsonpCallbackParam = $options['jsonp'] ? $options['jsonp'] : 'callback';
            if (strtolower($options['type']) == 'get') {
                if (! preg_match($jsre, $options['url'])) {
                    $sep = strpos($options['url'], '?') ? '&' : '?';
                    $options['url'] .= "$sep$jsonpCallbackParam=?";
                }
            } else 
                if ($options['data']) {
                    $jsonp = false;
                    foreach ($options['data'] as $n => $v) {
                        if ($v == '?')
                            $jsonp = true;
                    }
                    if (! $jsonp) {
                        $options['data'][$jsonpCallbackParam] = '?';
                    }
                }
            $options['dataType'] = 'json';
        }
        if (isset($options['dataType']) && $options['dataType'] == 'json') {
            $jsonpCallback = 'json_' . md5(microtime());
            $jsonpData = $jsonpUrl = false;
            if ($options['data']) {
                foreach ($options['data'] as $n => $v) {
                    if ($v == '?')
                        $jsonpData = $n;
                }
            }
            if (preg_match($jsre, $options['url']))
                $jsonpUrl = true;
            if ($jsonpData !== false || $jsonpUrl) {
                // remember callback name for httpData()
                $options['_jsonp'] = $jsonpCallback;
                if ($jsonpData !== false)
                    $options['data'][$jsonpData] = $jsonpCallback;
                if ($jsonpUrl)
                    $options['url'] = preg_replace($jsre, "=$jsonpCallback\\1", $options['url']);
            }
        }
        $client->setUri($options['url']);
        $client->setMethod(strtoupper($options['type']));
        if (isset($options['referer']) && $options['referer'])
            $client->setHeaders('Referer', $options['referer']);
        $client->setHeaders(array(
            // 'content-type' => $options['contentType'],
            'User-Agent' => 'Mozilla/5.0 (X11; U; Linux x86; en-US; rv:1.9.0.5) Gecko' . '/2008122010 Firefox/3.0.5',
            // TODO custom charset
            'Accept-Charset' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
            // 'Connection' => 'keep-alive',
            // 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language' => 'en-us,en;q=0.5'
        ));
        if ($options['username'])
            $client->setAuth($options['username'], $options['password']);
        if (isset($options['ifModified']) && $options['ifModified'])
            $client->setHeaders("If-Modified-Since", self::$lastModified ? self::$lastModified : "Thu, 01 Jan 1970 00:00:00 GMT");
        $client->setHeaders("Accept", isset($options['dataType']) && isset(self::$ajaxSettings['accepts'][$options['dataType']]) ? self::$ajaxSettings['accepts'][$options['dataType']] . ", */*" : self::$ajaxSettings['accepts']['_default']);
        // TODO $options['processData']
        if ($options['data'] instanceof phpQueryObject) {
            $serialized = $options['data']->serializeArray($options['data']);
            $options['data'] = array();
            foreach ($serialized as $r)
                $options['data'][$r['name']] = $r['value'];
        }
        if (strtolower($options['type']) == 'get') {
            $client->setParameterGet($options['data']);
        } else 
            if (strtolower($options['type']) == 'post') {
                $client->setEncType($options['contentType']);
                $client->setParameterPost($options['data']);
            }
        if (self::$active == 0 && $options['global'])
            phpQueryEvents::trigger($documentID, 'ajaxStart');
        self::$active ++;
        // beforeSend callback
        if (isset($options['beforeSend']) && $options['beforeSend'])
            phpQuery::callbackRun($options['beforeSend'], array(
                $client
            ));
            // ajaxSend event
        if ($options['global'])
            phpQueryEvents::trigger($documentID, 'ajaxSend', array(
                $client,
                $options
            ));
        if (phpQuery::$debug) {
            self::debug("{$options['type']}: {$options['url']}\n");
            self::debug("Options: <pre>" . var_export($options, true) . "</pre>\n");
            // if ($client->getCookieJar())
            // self::debug("Cookies: <pre>".var_export($client->getCookieJar()->getMatchingCookies($options['url']), true)."</pre>\n");
        }
        // request
        $response = $client->request();
        if (phpQuery::$debug) {
            self::debug('Status: ' . $response->getStatus() . ' / ' . $response->getMessage());
            self::debug($client->getLastRequest());
            self::debug($response->getHeaders());
        }
        if ($response->isSuccessful()) {
            // XXX tempolary
            self::$lastModified = $response->getHeader('Last-Modified');
            $data = self::httpData($response->getBody(), $options['dataType'], $options);
            if (isset($options['success']) && $options['success'])
                phpQuery::callbackRun($options['success'], array(
                    $data,
                    $response->getStatus(),
                    $options
                ));
            if ($options['global'])
                phpQueryEvents::trigger($documentID, 'ajaxSuccess', array(
                    $client,
                    $options
                ));
        } else {
            if (isset($options['error']) && $options['error'])
                phpQuery::callbackRun($options['error'], array(
                    $client,
                    $response->getStatus(),
                    $response->getMessage()
                ));
            if ($options['global'])
                phpQueryEvents::trigger($documentID, 'ajaxError', array(
                    $client, /*$response->getStatus(),*/$response->getMessage(),
                    $options
                ));
        }
        if (isset($options['complete']) && $options['complete'])
            phpQuery::callbackRun($options['complete'], array(
                $client,
                $response->getStatus()
            ));
        if ($options['global'])
            phpQueryEvents::trigger($documentID, 'ajaxComplete', array(
                $client,
                $options
            ));
        if ($options['global'] && ! -- self::$active)
            phpQueryEvents::trigger($documentID, 'ajaxStop');
        return $client;
        // if (is_null($domId))
        // $domId = self::$defaultDocumentID ? self::$defaultDocumentID : false;
        // return new phpQueryAjaxResponse($response, $domId);
    }

    protected static function httpData($data, $type, $options)
    {
        if (isset($options['dataFilter']) && $options['dataFilter'])
            $data = self::callbackRun($options['dataFilter'], array(
                $data,
                $type
            ));
        if (is_string($data)) {
            if ($type == "json") {
                if (isset($options['_jsonp']) && $options['_jsonp']) {
                    $data = preg_replace('/^\s*\w+\((.*)\)\s*$/s', '$1', $data);
                }
                $data = self::parseJSON($data);
            }
        }
        return $data;
    }

    /**
     * Enter description here...
     *
     * @param array|phpQuery $data            
     *
     */
    public static function param($data)
    {
        return http_build_query($data, null, '&');
    }

    public static function get($url, $data = null, $callback = null, $type = null)
    {
        if (! is_array($data)) {
            $callback = $data;
            $data = null;
        }
        // TODO some array_values on this shit
        return phpQuery::ajax(array(
            'type' => 'GET',
            'url' => $url,
            'data' => $data,
            'success' => $callback,
            'dataType' => $type
        ));
    }

    public static function post($url, $data = null, $callback = null, $type = null)
    {
        if (! is_array($data)) {
            $callback = $data;
            $data = null;
        }
        return phpQuery::ajax(array(
            'type' => 'POST',
            'url' => $url,
            'data' => $data,
            'success' => $callback,
            'dataType' => $type
        ));
    }

    public static function getJSON($url, $data = null, $callback = null)
    {
        if (! is_array($data)) {
            $callback = $data;
            $data = null;
        }
        // TODO some array_values on this shit
        return phpQuery::ajax(array(
            'type' => 'GET',
            'url' => $url,
            'data' => $data,
            'success' => $callback,
            'dataType' => 'json'
        ));
    }

    public static function ajaxSetup($options)
    {
        self::$ajaxSettings = array_merge(self::$ajaxSettings, $options);
    }

    public static function ajaxAllowHost($host1, $host2 = null, $host3 = null)
    {
        $loop = is_array($host1) ? $host1 : func_get_args();
        foreach ($loop as $host) {
            if ($host && ! in_array($host, phpQuery::$ajaxAllowedHosts)) {
                phpQuery::$ajaxAllowedHosts[] = $host;
            }
        }
    }

    public static function ajaxAllowURL($url1, $url2 = null, $url3 = null)
    {
        $loop = is_array($url1) ? $url1 : func_get_args();
        foreach ($loop as $url)
            phpQuery::ajaxAllowHost(parse_url($url, PHP_URL_HOST));
    }

    /**
     * Returns JSON representation of $data.
     *
     * @static
     *
     * @param mixed $data            
     * @return string
     */
    public static function toJSON($data)
    {
        if (function_exists('json_encode'))
            return json_encode($data);
        require_once ('Zend/Json/Encoder.php');
        return Zend_Json_Encoder::encode($data);
    }

    /**
     * Parses JSON into proper PHP type.
     *
     * @static
     *
     * @param string $json            
     * @return mixed
     */
    public static function parseJSON($json)
    {
        if (function_exists('json_decode')) {
            $return = json_decode(trim($json), true);
            // json_decode and UTF8 issues
            if (isset($return))
                return $return;
        }
        require_once ('Zend/Json/Decoder.php');
        return Zend_Json_Decoder::decode($json);
    }

    /**
     * Returns source's document ID.
     *
     * @param $source DOMNode|phpQueryObject            
     * @return string
     */
    public static function getDocumentID($source)
    {
        if ($source instanceof DOMDOCUMENT) {
            foreach (phpQuery::$documents as $id => $document) {
                if ($source->isSameNode($document->document))
                    return $id;
            }
        } else 
            if ($source instanceof DOMNODE) {
                foreach (phpQuery::$documents as $id => $document) {
                    if ($source->ownerDocument->isSameNode($document->document))
                        return $id;
                }
            } else 
                if ($source instanceof phpQueryObject)
                    return $source->getDocumentID();
                else 
                    if (is_string($source) && isset(phpQuery::$documents[$source]))
                        return $source;
    }

    /**
     * Get DOMDocument object related to $source.
     * Returns null if such document doesn't exist.
     *
     * @param $source DOMNode|phpQueryObject|string            
     * @return string
     */
    public static function getDOMDocument($source)
    {
        if ($source instanceof DOMDOCUMENT)
            return $source;
        $source = self::getDocumentID($source);
        return $source ? self::$documents[$id]['document'] : null;
    }
    
    // UTILITIES
    // http://docs.jquery.com/Utilities
    
    /**
     *
     * @return unknown_type
     * @link http://docs.jquery.com/Utilities/jQuery.makeArray
     */
    public static function makeArray($obj)
    {
        $array = array();
        if (is_object($object) && $object instanceof DOMNODELIST) {
            foreach ($object as $value)
                $array[] = $value;
        } else 
            if (is_object($object) && ! ($object instanceof Iterator)) {
                foreach (get_object_vars($object) as $name => $value)
                    $array[0][$name] = $value;
            } else {
                foreach ($object as $name => $value)
                    $array[0][$name] = $value;
            }
        return $array;
    }

    public static function inArray($value, $array)
    {
        return in_array($value, $array);
    }

    /**
     *
     * @param
     *            $object
     * @param
     *            $callback
     * @return unknown_type
     * @link http://docs.jquery.com/Utilities/jQuery.each
     */
    public static function each($object, $callback, $param1 = null, $param2 = null, $param3 = null)
    {
        $paramStructure = null;
        if (func_num_args() > 2) {
            $paramStructure = func_get_args();
            $paramStructure = array_slice($paramStructure, 2);
        }
        if (is_object($object) && ! ($object instanceof Iterator)) {
            foreach (get_object_vars($object) as $name => $value)
                phpQuery::callbackRun($callback, array(
                    $name,
                    $value
                ), $paramStructure);
        } else {
            foreach ($object as $name => $value)
                phpQuery::callbackRun($callback, array(
                    $name,
                    $value
                ), $paramStructure);
        }
    }

    /**
     *
     * @link http://docs.jquery.com/Utilities/jQuery.map
     */
    public static function map($array, $callback, $param1 = null, $param2 = null, $param3 = null)
    {
        $result = array();
        $paramStructure = null;
        if (func_num_args() > 2) {
            $paramStructure = func_get_args();
            $paramStructure = array_slice($paramStructure, 2);
        }
        foreach ($array as $v) {
            $vv = phpQuery::callbackRun($callback, array(
                $v
            ), $paramStructure);
            // $callbackArgs = $args;
            // foreach($args as $i => $arg) {
            // $callbackArgs[$i] = $arg instanceof CallbackParam
            // ? $v
            // : $arg;
            // }
            // $vv = call_user_func_array($callback, $callbackArgs);
            if (is_array($vv)) {
                foreach ($vv as $vvv)
                    $result[] = $vvv;
            } else 
                if ($vv !== null) {
                    $result[] = $vv;
                }
        }
        return $result;
    }

    /**
     *
     * @param $callback Callback            
     * @param
     *            $params
     * @param
     *            $paramStructure
     * @return unknown_type
     */
    public static function callbackRun($callback, $params = array(), $paramStructure = null)
    {
        if (! $callback)
            return;
        if ($callback instanceof CallbackParameterToReference) {
            // TODO support ParamStructure to select which $param push to reference
            if (isset($params[0]))
                $callback->callback = $params[0];
            return true;
        }
        if ($callback instanceof Callback) {
            $paramStructure = $callback->params;
            $callback = $callback->callback;
        }
        if (! $paramStructure)
            return call_user_func_array($callback, $params);
        $p = 0;
        foreach ($paramStructure as $i => $v) {
            $paramStructure[$i] = $v instanceof CallbackParam ? $params[$p ++] : $v;
        }
        return call_user_func_array($callback, $paramStructure);
    }

    /**
     * Merge 2 phpQuery objects.
     *
     * @param array $one            
     * @param array $two
     *            @protected
     * @todo node lists, phpQueryObject
     */
    public static function merge($one, $two)
    {
        $elements = $one->elements;
        foreach ($two->elements as $node) {
            $exists = false;
            foreach ($elements as $node2) {
                if ($node2->isSameNode($node))
                    $exists = true;
            }
            if (! $exists)
                $elements[] = $node;
        }
        return $elements;
        // $one = $one->newInstance();
        // $one->elements = $elements;
        // return $one;
    }

    /**
     *
     * @param
     *            $array
     * @param
     *            $callback
     * @param
     *            $invert
     * @return unknown_type
     * @link http://docs.jquery.com/Utilities/jQuery.grep
     */
    public static function grep($array, $callback, $invert = false)
    {
        $result = array();
        foreach ($array as $k => $v) {
            $r = call_user_func_array($callback, array(
                $v,
                $k
            ));
            if ($r === ! (bool) $invert)
                $result[] = $v;
        }
        return $result;
    }

    public static function unique($array)
    {
        return array_unique($array);
    }

    /**
     *
     * @param
     *            $function
     * @return unknown_type @TODO there are problems with non-static methods, second parameter pass it
     *         but doesnt verify is method is really callable
     */
    public static function isFunction($function)
    {
        return is_callable($function);
    }

    public static function trim($str)
    {
        return trim($str);
    }

    /* PLUGINS NAMESPACE */
    /**
     *
     * @param
     *            $url
     * @param
     *            $callback
     * @param
     *            $param1
     * @param
     *            $param2
     * @param
     *            $param3
     * @return phpQueryObject
     */
    public static function browserGet($url, $callback, $param1 = null, $param2 = null, $param3 = null)
    {
        if (self::plugin('WebBrowser')) {
            $params = func_get_args();
            return self::callbackRun(array(
                self::$plugins,
                'browserGet'
            ), $params);
        } else {
            self::debug('WebBrowser plugin not available...');
        }
    }

    /**
     *
     * @param
     *            $url
     * @param
     *            $data
     * @param
     *            $callback
     * @param
     *            $param1
     * @param
     *            $param2
     * @param
     *            $param3
     * @return phpQueryObject
     */
    public static function browserPost($url, $data, $callback, $param1 = null, $param2 = null, $param3 = null)
    {
        if (self::plugin('WebBrowser')) {
            $params = func_get_args();
            return self::callbackRun(array(
                self::$plugins,
                'browserPost'
            ), $params);
        } else {
            self::debug('WebBrowser plugin not available...');
        }
    }

    /**
     *
     * @param
     *            $ajaxSettings
     * @param
     *            $callback
     * @param
     *            $param1
     * @param
     *            $param2
     * @param
     *            $param3
     * @return phpQueryObject
     */
    public static function browser($ajaxSettings, $callback, $param1 = null, $param2 = null, $param3 = null)
    {
        if (self::plugin('WebBrowser')) {
            $params = func_get_args();
            return self::callbackRun(array(
                self::$plugins,
                'browser'
            ), $params);
        } else {
            self::debug('WebBrowser plugin not available...');
        }
    }

    /**
     *
     * @param
     *            $code
     * @return string
     */
    public static function php($code)
    {
        return self::code('php', $code);
    }

    /**
     *
     * @param
     *            $type
     * @param
     *            $code
     * @return string
     */
    public static function code($type, $code)
    {
        return "<$type><!-- " . trim($code) . " --></$type>";
    }

    public static function __callStatic($method, $params)
    {
        return call_user_func_array(array(
            phpQuery::$plugins,
            $method
        ), $params);
    }

    protected static function dataSetupNode($node, $documentID)
    {
        // search are return if alredy exists
        foreach (phpQuery::$documents[$documentID]->dataNodes as $dataNode) {
            if ($node->isSameNode($dataNode))
                return $dataNode;
        }
        // if doesn't, add it
        phpQuery::$documents[$documentID]->dataNodes[] = $node;
        return $node;
    }

    protected static function dataRemoveNode($node, $documentID)
    {
        // search are return if alredy exists
        foreach (phpQuery::$documents[$documentID]->dataNodes as $k => $dataNode) {
            if ($node->isSameNode($dataNode)) {
                unset(self::$documents[$documentID]->dataNodes[$k]);
                unset(self::$documents[$documentID]->data[$dataNode->dataID]);
            }
        }
    }

    public static function data($node, $name, $data, $documentID = null)
    {
        if (! $documentID)
            // TODO check if this works
            $documentID = self::getDocumentID($node);
        $document = phpQuery::$documents[$documentID];
        $node = self::dataSetupNode($node, $documentID);
        if (! isset($node->dataID))
            $node->dataID = ++ phpQuery::$documents[$documentID]->uuid;
        $id = $node->dataID;
        if (! isset($document->data[$id]))
            $document->data[$id] = array();
        if (! is_null($data))
            $document->data[$id][$name] = $data;
        if ($name) {
            if (isset($document->data[$id][$name]))
                return $document->data[$id][$name];
        } else
            return $id;
    }

    public static function removeData($node, $name, $documentID)
    {
        if (! $documentID)
            // TODO check if this works
            $documentID = self::getDocumentID($node);
        $document = phpQuery::$documents[$documentID];
        $node = self::dataSetupNode($node, $documentID);
        $id = $node->dataID;
        if ($name) {
            if (isset($document->data[$id][$name]))
                unset($document->data[$id][$name]);
            $name = null;
            foreach ($document->data[$id] as $name)
                break;
            if (! $name)
                self::removeData($node, $name, $documentID);
        } else {
            self::dataRemoveNode($node, $documentID);
        }
    }
}

/**
 * Plugins static namespace class.
 *
 * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
 * @package phpQuery
 * @todo move plugin methods here (as statics)
 */
class phpQueryPlugins
{

    public function __call($method, $args)
    {
        if (isset(phpQuery::$extendStaticMethods[$method])) {
            $return = call_user_func_array(phpQuery::$extendStaticMethods[$method], $args);
        } else 
            if (isset(phpQuery::$pluginsStaticMethods[$method])) {
                $class = phpQuery::$pluginsStaticMethods[$method];
                $realClass = "phpQueryPlugin_$class";
                $return = call_user_func_array(array(
                    $realClass,
                    $method
                ), $args);
                return isset($return) ? $return : $this;
            } else
                throw new Exception("Method '{$method}' doesnt exist");
    }
}

/**
 * Shortcut to phpQuery::pq($arg1, $context)
 * Chainable.
 *
 * @see phpQuery::pq()
 * @return phpQueryObject|QueryTemplatesSource|QueryTemplatesParse|QueryTemplatesSourceQuery
 * @author Tobiasz Cudnik <tobiasz.cudnik/gmail.com>
 * @package phpQuery
 */
function pq($arg1, $context = null)
{
    $args = func_get_args();
    return call_user_func_array(array(
        'phpQuery',
        'pq'
    ), $args);
}
// add plugins dir and Zend framework to include path
set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/phpQuery/' . PATH_SEPARATOR . dirname(__FILE__) . '/phpQuery/plugins/');
// why ? no __call nor __get for statics in php...
// XXX __callStatic will be available in PHP 5.3
phpQuery::$plugins = new phpQueryPlugins();
// include bootstrap file (personal library config)
if (file_exists(dirname(__FILE__) . '/phpQuery/bootstrap.php'))
    require_once dirname(__FILE__) . '/phpQuery/bootstrap.php';