<?php
#BEGIN_LICENSE
#-------------------------------------------------------------------------
# Module: cms_route_manager (c) 2010 by Robert Campbell
#         (calguy1000@cmsmadesimple.org)
#  A caching tree for CMSMS content objects.
#
#-------------------------------------------------------------------------
# CMS - CMS Made Simple is (c) 2005 by Ted Kulp (wishy@cmsmadesimple.org)
# Visit our homepage at: http://www.cmsmadesimple.org
#
#-------------------------------------------------------------------------
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# However, as a special exception to the GPL, this software is distributed
# as an addon module to CMS Made Simple.  You may not use this software
# in any Non GPL version of CMS Made simple, or in any version of CMS
# Made simple that does not indicate clearly and obviously in its admin
# section that the site was built with CMS Made simple.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
# Or read it online: http://www.gnu.org/licenses/licenses.html#GPL
#
#-------------------------------------------------------------------------
#END_LICENSE

/**
 * Classes, functions and utilities for managing CMSMS routes.
 * @package CMS
 * @author  Robert Campbell
 * @license GPL
 */

if( !function_exists('__internal_cmp_routes') ) {
    /**
     * @internal
     * @ignore
     */
    function __internal_cmp_routes($a,$b) {
        return strcmp($a['term'],$b['term']);
    }
}

/**
 * A singleton class to manage all recognized routes in the system.
 *
 * This class is used internally to allow managing routes.  It should not be used by the public.
 *
 * @deprecated This class will be renamed/reworied in a subsequent version
 * @internal
 * @package CMS
 * @license GPL
 * @author Robert Campbell <calguy1000@cmsmadesimple.org>
 * @since  1.9
 */
final class cms_route_manager
{
    /**
     * @ignore
     */
    private function __construct()
    {
    }

    /**
     * @ignore
     */
    private static $_routes_loaded = FALSE;

    /**
     * @ignore
     */
    private static $_routes;

    /**
     * @ignore
     */
    private static $_dynamic_routes;

    /**
     * @ignore
     */
    static private function _find_match($needle,$haystack,$exact)
    {
        // split the haystack into an array of 'absolute' or 'regex' matches
        $absolute = array();
        $regex = array();
        foreach( $haystack as $rec ) {
            if( $exact || (isset($rec['absolute']) && $rec['absolute']) ) {
                $absolute[$rec['term']] = $rec;
            }
            else {
                $regex[] = $rec;
            }
        }

        if( isset( $absolute[$needle] ) ) return $absolute[$needle];

        // do the linear regex thing.
        for( $i = 0, $n = count($regex); $i < $n; $i++ ) {
            $rec = $regex[$i];
            if( $rec->matches($needle) ) return $rec;
        }
        return FALSE;
    }

    /**
     * @ignore
     */
    static private function route_binarySearch($needle,$haystack,$comparator)
    {
        if( count($haystack) == 0 ) return FALSE;

        // credits: temporal dot pl at gmail dot com
        // reference: http://php.net/manual/en/function.array-search.php
        $high = count( $haystack ) -1;
        $low = 0;
        while ( $high >= $low ) {
            $probe = (int)floor( ( $high + $low ) / 2 );
            $comparison = $comparator( $haystack[$probe]['term'], $needle );
            if ( $comparison < 0 ) {
                $low = $probe +1;
            }
            elseif ( $comparison > 0 ) {
                $high = $probe -1;
            }
            else {
                return $probe;
            }
        }

        //The loop ended without a match
        //Compensate for needle greater than highest haystack element
        if($comparator($haystack[count($haystack)-1]['term'], $needle) < 0) $probe = count($haystack);
        return FALSE;
    }

    /**
     * Test wether the specified route exists.
     *
     * @param CmsRoute $route The route object
     * @param bool     $static_only A flag indicating that only static routes should be checked.
     * @return bool
     */
    static public function route_exists(CmsRoute $route,$static_only = FALSE)
    {
        self::_load_static_routes();
        if( is_array(self::$_routes) ) {
            if( isset(self::$_routes[$route->signature()]) ) return TRUE;
        }

        if( $static_only ) return FALSE;

        if( is_array(self::$_dynamic_routes) ) {
            if( isset(self::$_dynamic_routes[$route->signature()]) ) return TRUE;
        }
        return FALSE;
    }


    /**
     * Find a route that matches the specified string
     *
     * @param string $str The string to test against (usually an incoming url request)
     * @param bool $exact Perform an exact string match rather than a regex match.
     * @param bool $static_only A flag indicating that only static routes should be checked.
     * @return CmsRoute the matching route, or null.
     */
    static public function find_match($str,$exact = false,$static_only = FALSE)
    {
        self::_load_static_routes();

        if( is_array(self::$_routes) ) {
            $res = self::_find_match($str,self::$_routes,$exact);
            if( is_object($res) ) return $res;
        }

        if( $static_only ) return;

        if( is_array(self::$_dynamic_routes) ) {
            $res = self::_find_match($str,self::$_dynamic_routes,$exact);
            if( is_object($res) ) return $res;
        }
    }


    /**
     * Add a static route.
     * This method will return TRUE, and do nothing if the route already exists.
     * The route cache will be removed if the route is successfully added to the database.
     *
     * @author Robert Campbell <calguy1000@cmsmadesimple.org>
     * @since 1.11
     * @param CmsRoute $route The route to add.
     * @return bool
     */
    public static function add_static(CmsRoute& $route)
    {
        self::_load_static_routes();
        if( self::route_exists($route) ) return TRUE;

        $query = 'INSERT INTO '.CMS_DB_PREFIX.'routes (term,key1,key2,key3,data,created) VALUES (?,?,?,?,?,NOW())';

        $db = CmsApp::get_instance()->GetDb();
        $dbr = $db->Execute($query,array($route['term'], $route['key1'], $route['key2'], $route['key3'], serialize($route)));
        if( !$dbr ) return FALSE;

        self::_clear_cache();
        return TRUE;
    }


    /**
     * Delete a static route.
     * The route cache will be removed if the route is successfully removed from the database.
     *
     * @author Robert Campbell <calguy1000@cmsmadesimple.org>
     * @since 1.11
     * @param string $term The term to search for
     * @param string $key1
     * @param string $key2
     * @param string $key3
     * @return bool
     */
    public static function del_static($term,$key1 = null,$key2 = null,$key3 = null)
    {
        $query = 'DELETE FROM '.CMS_DB_PREFIX.'routes WHERE ';
        $where = array();
        $parms = array();
        if( $term ) {
            $where[] = 'term = ?';
            $parms[] = $term;
        }

        if( !is_null($key1) ) {
            $where[] = 'key1 = ?';
            $parms[] = $key1;

            if( !is_null($key2) ) {
                $where[] = 'key2 = ?';
                $parms[] = $key2;

                if( !is_null($key3) ) {
                    $where[] = 'key3 = ?';
                    $parms[] = $key3;
                }
            }
        }

        if( count($where) == 0 ) return FALSE;

        $db = CmsApp::get_instance()->GetDb();
        $query .= implode(' AND ',$where);
        $dbr = $db->Execute($query,$parms);
        if( $dbr ) {
            self::_clear_cache();
            return TRUE;
        }

        return FALSE;
    }


    /**
     * Add a dynamic route.
     * Dynamic routes are not stored to the database, and are checked after static routes when searching for a match.
     * This method will return TRUE if the route already exists (static, or dynamic)
     *
     * @author Robert Campbell <calguy1000@cmsmadesimple.org>
     * @since 1.11
     * @param CmsRoute $route The dynamic route object to add
     * @return bool.
     */
    public static function add_dynamic(CmsRoute $route)
    {
        if( self::route_exists($route) ) return FALSE;
        if( !is_array(self::$_dynamic_routes) ) self::$_dynamic_routes = array();

        self::$_dynamic_routes[$route->signature()] = $route;
        return TRUE;
    }


    /**
     * Register a new route.
     * This is just an alias (for compatibility reasons) to the add_dynamc method.
     *
     * @see add_dynamic
     * @param CmsRoute $route The route to register
     * @return bool
     */
    static public function register(CmsRoute $route)
    {
        return self::add_dynamic($route);
    }


    /**
     * Load dynamic routes from the modules.
     * typically called by modules or places where static urls are added
     * this method will load all modules and call setparameters to ensure
     * that their dynamic routes are created.
     *
     * @deprecated
     */
    public static function load_routes()
    {
    }

    /**
     * Reset the static route table.
     *
     * @since 1.11
     * @author Robert Campbell
     * @internal
     */
    public static function rebuild_static_routes()
    {
        // clear the route table and cache
        self::_clear_cache();
        $db = CmsApp::get_instance()->GetDb();
        $query = 'TRUNCATE TABLE '.CMS_DB_PREFIX.'routes';
        $db->Execute($query);

        // get content routes
        $query = 'SELECT content_id,page_url FROM '.CMS_DB_PREFIX."content WHERE active=1 AND COALESCE(page_url,'') != ''";
        $tmp = $db->GetArray($query);
        if( is_array($tmp) && count($tmp) ) {
            for( $i = 0, $n = count($tmp); $i < $n; $i++ ) {
                $route = CmsRoute::new_builder($tmp[$i]['page_url'],'__CONTENT__',$tmp[$i]['content_id'],'',TRUE);
                cms_route_manager::add_static($route);
            }
        }

        // get the module routes
        $installed = ModuleOperations::get_instance()->GetInstalledModules();
        foreach( $installed as $module_name ) {
            $modobj = cms_utils::get_module($module_name);
            if( !$modobj ) continue;
            $routes = $modobj->CreateStaticRoutes();
        }
    }

    /**
     * Load existing static routes from the cache
     * This method will also refresh the cache from the database if the cache cannot be found.
     * Note: It should not be necessary to load routes, as this method is called internally.
     * @internal
     */
    private static function _load_static_routes()
    {
        if( self::$_routes_loaded ) return;

        $data = self::_get_routes_from_cache();
        if( is_array($data) && count($data) ) {
            self::$_routes = [];
            for( $i = 0, $n = count($data); $i < $n; $i++ ) {
                $obj = unserialize($data[$i]['data']);
                self::$_routes[$obj->signature()] = $obj;
            }
            self::$_routes_loaded = TRUE;
        }
    }

    /**
     * @ignore
     */
    private static function _get_cache_driver()
    {
        return CmsApp::get_instance()->get_cache_driver();
    }

    /**
     * @ignore
     */
    private static function _get_routes_from_cache()
    {
        $driver = self::_get_cache_driver();
        if( $driver->exists(__CLASS__,__CLASS__) ) {
            $data = $driver->get(__CLASS__,__CLASS__);
            self::$_routes_loaded = TRUE;
            return $data;
        }
        else {
            $db = CmsApp::get_instance()->GetDb();
            $query = 'SELECT * FROM '.CMS_DB_PREFIX.'routes';
            $data = $db->GetArray($query);
            $driver->set(__CLASS__, $data, __CLASS__);
            self::$_routes_loaded = TRUE;
            return $data;
        }
    }

    /**
     * @ignore
     */
    private static function _clear_cache()
    {
        self::_get_cache_driver()->clear(__CLASS__);
        self::$_routes = null;
        self::$_routes_loaded = FALSE;
        // note: dynamic routes don't get cleared.
    }
} // end of class

