<?php

/**
 * The front controller of the plugin.
 *
 * @author     Time.ly Network Inc.
 * @since      2.0
 *
 * @package    AI1EC
 * @subpackage AI1EC.Controller
 */
class Ai1ec_Front_Controller {

    /**
     * @var Ai1ec_Registry_Object The Object registry.
     */
    protected $_registry;

    /**
     * @var bool Whether the domain has alredy been loaded or not.
     */
    protected $_domain_loaded = false;

    /**
     * @var string The pagebase used by Ai1ec_Href_Helper.
     */
    protected $_pagebase_for_href;

    /**
     * @var Ai1ec_Request_Parser Instance of the request pa
     */
    protected $_request;

    /**
     * @var array
     */
    protected $_default_theme;

    /**
     * Initializes the default theme property.
     */
    public function __construct() {
        // Initialize default theme.
        $this->_default_theme = array(
            'theme_dir'  => AI1EC_DEFAULT_THEME_PATH,
            'theme_root' => AI1EC_DEFAULT_THEME_ROOT,
            'theme_url'  => AI1EC_THEMES_URL . '/' . AI1EC_DEFAULT_THEME_NAME,
            'stylesheet' => AI1EC_DEFAULT_THEME_NAME,
            'legacy'     => false,
        );
    }

    /**
     * Initialize the controller.
     *
     * @param Ai1ec_Loader $ai1ec_loader Instance of Ai1EC classes loader
     *
     * @return void
     */
    public function initialize( $ai1ec_loader ) {
        ai1ec_start();
        $this->_init( $ai1ec_loader );
        $this->_initialize_dispatcher();
        $lessphp = $this->_registry->get( 'less.lessphp' );
        $lessphp->initialize_less_variables_if_not_set();
        $this->_registry->get( 'controller.shutdown' )
            ->register( 'ai1ec_stop' );
        add_action( 'plugins_loaded', array( $this, 'register_extensions' ), 1 );
        add_action( 'after_setup_theme', array( $this, 'register_themes' ), 1 );
        add_action( 'init', array( $lessphp, 'invalidate_css_cache_if_requested' ) );
    }

    /**
     * Let other objects access default theme
     *
     * @return array
     */
    public function get_default_theme() {
        return $this->_default_theme;
    }

    /**
     * Remove unwanted menus
     */
    public function admin_menu() {
        remove_submenu_page(
            'edit.php?post_type=ai1ec_event',
            'edit-tags.php?taxonomy=events_tags&amp;post_type=ai1ec_event'
        );
    }

    /**
     * Notify extensions and pass them instance of objects registry.
     *
     * @return void
     */
    public function register_extensions() {
        do_action( 'ai1ec_loaded', $this->_registry );
    }

    /**
     * Notify themes and pass them instance of objects registry.
     *
     * @return void
     */
    public function register_themes() {
        do_action( 'ai1ec_after_themes_setup', $this->_registry );
    }

    /**
     * Returns the registry object
     *
     * @param mixed $discard not used. Always return the registry.
     *
     * @return Ai1ec_Registry_Object
     */
    public function return_registry( $discard ) {
        return $this->_registry;
    }

    /**
     * If WIDGET_PARAMETER is set.
     *
     * @return boolean
     */
    protected function is_widget() {
        return isset(
            $_GET[Ai1ec_Controller_Javascript_Widget::WIDGET_PARAMETER]
        );
    }

    /**
     * If Advanced JS cache enabled.
     *
     * @return boolean
     */
    protected function if_js_cache_enabled() {
        $settings = $this->_registry->get( 'model.settings' );
        return $settings->get( 'cache_dynamic_js' );
    }

    /**
     * If LEGACY_WIDGET_PARAMETER is set.
     *
     * @return boolean
     */
    protected function is_legacy_widget() {
        return isset(
            $_GET[Ai1ec_Controller_Javascript_Widget::LEGACY_WIDGET_PARAMETER]
        );
    }

    /**
     * Execute commands if our plugin must handle the request.
     *
     * @wp_hook init
     *
     * @return void
     */
    public function route_request() {
        $this->_process_request();
        // get the resolver
        $resolver = $this->_registry->get(
            'command.resolver',
            $this->_request
        );
        // get the command
        $commands = $resolver->get_commands();
        // if we have a command
        if ( ! empty( $commands ) ) {
            foreach( $commands as $command ) {
                $result = $command->execute();
                if ( $command->stop_execution() ) {
                    return $result;
                }
            }
        }
    }

    /**
     * Initializes the URL router used by our plugin.
     *
     * @wp_hook init
     *
     * @return void
     */
    public function initialize_router() {
        /* @var $cal_state Ai1ec_Calendar_State */
        $cal_state              = $this->_registry->get( 'calendar.state' );
        $cal_state->set_routing_initialization( true );
        $settings               = $this->_registry->get( 'model.settings' );
        $cal_page               = $settings->get( 'calendar_page_id' );

        if (
            ! $cal_page ||
            $cal_page < 1
        ) { // Routing may not be affected in any way if no calendar page exists.
            $cal_state->set_routing_initialization( false );
            return null;
        }
        $router              = $this->_registry->get( 'routing.router' );
        $localization_helper = $this->_registry->get( 'p28n.wpml' );
        $page_base          = '';
        $clang              = '';

        if ( $localization_helper->is_wpml_active() ) {
            $trans = $localization_helper
                ->get_wpml_translations_of_page(
                    $cal_page,
                    true
                );
            $clang = $localization_helper->get_language();
            if ( isset( $trans[$clang] ) ) {
                $cal_page = $trans[$clang];
            }
        }
        $template_link_helper = $this->_registry->get( 'template.link.helper' );

        if ( ! get_post( $cal_page ) ) {
            $cal_state->set_routing_initialization( false );
            return null;
        }

        $page_base = $template_link_helper->get_page_link(
            $cal_page
        );

        $page_base = Ai1ec_Wp_Uri_Helper::get_pagebase( $page_base );
        $page_link = 'index.php?page_id=' .
            $cal_page;
        $pagebase_for_href = Ai1ec_Wp_Uri_Helper::get_pagebase_for_links(
            get_page_link( $cal_page ),
            $clang
        );

        // save the pagebase to set up the factory later
        $application = $this->_registry->get( 'bootstrap.registry.application' );
        $application->set( 'calendar_base_page', $pagebase_for_href );
        $option = $this->_registry->get( 'model.option' );

        // If the calendar is set as the front page, disable permalinks.
        // They would not be legal under a Windows server. See:
        // https://issues.apache.org/bugzilla/show_bug.cgi?id=41441

        if (
            $option->get( 'permalink_structure' ) &&
            ( int ) get_option( 'page_on_front' ) !==
            ( int ) $cal_page
        ) {
            $application->set( 'permalinks_enabled', true );
        }

        $router->asset_base( $page_base )
            ->register_rewrite( $page_link );
        $cal_state->set_routing_initialization( false );
    }

    /**
     * Initialize the system.
     *
     * Perform all the inizialization needed for the system.
     * Throws some uncatched exception for critical failures.
     * Plugin will be disabled by the exception handler on those failures.
     *
     * @param Ai1ec_Loader $ai1ec_loader Instance of Ai1EC classes loader
     *
     * @throws Ai1ec_Constants_Not_Set_Exception
     * @throws Ai1ec_Database_Update_Exception
     * @throws Ai1ec_Database_Schema_Exception
     *
     * @return void Method does not return
     */
    protected function _init( $ai1ec_loader ) {
        $exception = null;
        // Load the textdomain
        add_action( 'plugins_loaded', array( $this, 'load_textdomain' ) );
        try {
            // Initialize the registry object
            $this->_initialize_registry( $ai1ec_loader );
            $this->_registry->get( 'event.dispatcher' )->register_filter(
                'ai1ec_perform_scheme_update',
                array( 'database.datetime-migration', 'filter_scheme_update' )
            );
            // Procedures to take when upgrading plugin version
            $this->_plugin_upgrade_procedures();
            // Load the css if needed
            $this->_load_css_if_needed();
            // Initialize the crons
            $this->_install_crons();
            // Register the activation hook
            $this->_initialize_schema();
            // set the default theme if not set
            $this->_add_default_theme_if_not_set();
        } catch ( Ai1ec_Constants_Not_Set_Exception $e ) {
            // This is blocking, throw it and disable the plugin
            $exception = $e;
        } catch ( Ai1ec_Database_Update_Exception $e ) {
            // Blocking throw it so that the plugin is disabled
            $exception = $e;
        } catch ( Ai1ec_Database_Schema_Exception $e ) {
            // Blocking throw it so that the plugin is disabled
            $exception = $e;
        } catch ( Ai1ec_Scheduling_Exception $e ) {
            // not blocking
        }

        if ( null !== $exception ) {
            throw $exception;
        }
    }

    /**
     * Set the default theme if no theme is set, or populate theme info array if
     * insufficient information is currently being stored.
     *
     * @uses apply_filters() Calls 'ai1ec_pre_save_current_theme' hook to allow
     *       overwriting of theme information before being stored.
     */
    protected function _add_default_theme_if_not_set() {
        $option = $this->_registry->get( 'model.option' );
        $theme  = $option->get( 'ai1ec_current_theme', array() );
        $update = false;
        // Theme setting is undefined; default to Vortex.
        if ( empty( $theme ) ) {
            $theme  = $this->_default_theme;
            $update = true;
        }
        // Legacy settings; in 1.x the active theme was stored as a bare string,
        // and they were located in a different folder than they are now.
        else if ( is_string( $theme ) ) {
            $theme_name  = strtolower( $theme );
            $core_themes = explode( ',', AI1EC_CORE_THEMES );
            $legacy      = ! in_array( $theme_name, $core_themes );

            if ( $legacy ) {
                $root = WP_CONTENT_DIR . DIRECTORY_SEPARATOR . AI1EC_THEME_FOLDER;
                $url  = WP_CONTENT_URL . '/' . AI1EC_THEME_FOLDER . '/' . $theme_name;
            } else {
                $root = AI1EC_DEFAULT_THEME_ROOT;
                $url  = AI1EC_THEMES_URL . '/' . $theme_name;
            }
            // if it's from 1.x, move folders to avoid confusion
            if ( apply_filters( 'ai1ec_move_themes_to_backup', true ) ) {
                $this->_registry->get( 'theme.search' )
                    ->move_themes_to_backup( $core_themes );
            }
            // Ensure existence of theme directory.
            if ( ! is_dir( $root . DIRECTORY_SEPARATOR . $theme_name ) ) {
                // It's missing; something is wrong with this theme. Reset theme to
                // Vortex and warn the user accordingly.
                $option->set( 'ai1ec_current_theme', $this->_default_theme );
                $notification = $this->_registry->get( 'notification.admin' );
                $notification->store(
                    sprintf(
                        Ai1ec_I18n::__(
                            'Your active calendar theme could not be properly initialized. The default theme has been activated instead. Please visit %s and try reactivating your theme manually.'
                        ),
                        '<a href="' . ai1ec_admin_url( AI1EC_THEME_SELECTION_BASE_URL ) . '">' .
                        Ai1ec_I18n::__( 'Calendar Themes' ) . '</a>'
                    ),
                    'error',
                    1
                );
            }

            $theme = array(
                'theme_dir'  => $root . DIRECTORY_SEPARATOR . $theme_name,
                'theme_root' => $root,
                'theme_url'  => $url,
                'stylesheet' => $theme_name,
                'legacy'     => $legacy,
            );
            $update = true;
        }
        // Ensure 'theme_url' is defined, as this property was added after the first
        // public beta release.
        else if ( ! isset( $theme['theme_url'] ) ) {
            if ( $theme['legacy'] ) {
                $theme['theme_url'] = WP_CONTENT_URL . '/' . AI1EC_THEME_FOLDER . '/' .
                    $theme['stylesheet'];
            } else {
                $theme['theme_url'] = AI1EC_THEMES_URL . '/' . $theme['stylesheet'];
            }
            $update = true;
        }

        if ( $update ) {
            $theme = apply_filters( 'ai1ec_pre_save_current_theme', $theme );
            $option->set( 'ai1ec_current_theme', $theme );
        }
    }

    /**
     * Adds actions handled by the front controller.
     */
    protected function _add_front_controller_actions() {
        // Initialize router. I use add_action as the dispatcher would just add
        // overhead.
        add_action(
            'init',
            array( $this, 'initialize_router' ),
            PHP_INT_MAX - 1
        );
        add_action(
            'widgets_init',
            array( 'Ai1ec_View_Admin_Widget', 'register_widget' )
        );

        if (
            $this->is_widget() ||
            $this->is_legacy_widget()
        ) {
            $this->_registry->get( 'event.dispatcher' )->register_action(
                'init',
                array( 'controller.javascript-widget', 'render_js_widget' ),
                PHP_INT_MAX
            );
        }

        // Route the request.
        $action = 'template_redirect';
        if ( is_admin() ) {
            $action = 'init';
            add_action( 'admin_menu', array( $this, 'admin_menu' ) );
        }
        add_action( $action, array( $this, 'route_request' ) );
        add_filter( 'ai1ec_registry', array( $this, 'return_registry' ) );
    }

    /**
     * Initialize the dispatcher.
     *
     * Complete this when writing the dispatcher.
     *
     * @return void
     */
    protected function _initialize_dispatcher() {
        $dispatcher = $this->_registry->get( 'event.dispatcher' );
        $dispatcher->register_action(
            'init',
            array( 'post.custom-type', 'register' )
        );
        $this->_add_front_controller_actions();
        if ( isset( $_GET[Ai1ec_Javascript_Controller::LOAD_JS_PARAMETER] ) ) {
            $dispatcher->register_action(
                'wp_loaded',
                array( 'controller.javascript', 'render_js' )
            );
        }
        $dispatcher->register_action(
            'before_delete_post',
            array( 'model.event.trashing', 'before_delete_post' ),
            0,
            3
        );
        $dispatcher->register_action(
            'delete_post',
            array( 'model.event.trashing', 'delete' )
        );
        $dispatcher->register_action(
            'wp_trash_post',
            array( 'model.event.trashing', 'trash_post' )
        );
        $dispatcher->register_action(
            'trashed_post',
            array( 'model.event.trashing', 'trashed_post' )
        );
        $dispatcher->register_action(
            'untrash_post',
            array( 'model.event.trashing', 'untrash_post' )
        );
        $dispatcher->register_action(
            'untrashed_post',
            array( 'model.event.trashing', 'untrashed_post' )
        );
        $dispatcher->register_action(
            'pre_http_request',
            array( 'http.request', 'pre_http_request' ),
            10,
            3
        );
        $dispatcher->register_action(
            'http_request_args',
            array( 'http.request', 'init_certificate' ),
            10,
            2
        );
        // add the filter to let the organize page work
        $dispatcher->register_action(
            'admin_init',
            array( 'view.admin.organize', 'add_taxonomy_actions' ),
            10000
        );
        $dispatcher->register_action(
            'plugins_loaded',
            array( 'theme.loader', 'clean_cache_on_upgrade' ),
            PHP_INT_MAX
        );
        $dispatcher->register_filter(
            'get_the_excerpt',
            array( 'view.event.content', 'event_excerpt' ),
            11
        );
        remove_filter( 'the_excerpt', 'wpautop', 10 );
        $dispatcher->register_filter(
            'the_excerpt',
            array( 'view.event.content', 'event_excerpt_noautop' ),
            11
        );
        $dispatcher->register_filter(
            'robots_txt',
            array( 'robots.helper', 'rules' ),
            10,
            2
        );
        $dispatcher->register_filter(
            'ai1ec_dbi_debug',
            array( 'http.request', 'debug_filter' )
        );
        $dispatcher->register_filter(
            'ai1ec_dbi_debug',
            array( 'compatibility.cli', 'disable_db_debug' )
        );
        // editing a child instance
        if ( isset( $_SERVER['SCRIPT_NAME'] ) && basename( $_SERVER['SCRIPT_NAME'] ) === 'post.php' ) {
            $dispatcher->register_action(
                'admin_action_editpost',
                array( 'model.event.parent', 'admin_init_post' )
            );
            $dispatcher->register_filter(
                'user_has_cap',
                array( 'content.filter', 'display_trash_link' ),
                10,
                2
            );
        }
        // post row action for parent/child
        $dispatcher->register_action(
            'post_row_actions',
            array( 'model.event.parent', 'post_row_actions' ),
            100,
            2
        );
        // Category colors
        $dispatcher->register_action(
            'events_categories_add_form_fields',
            array( 'view.admin.event-category', 'events_categories_add_form_fields' )
        );
        $dispatcher->register_action(
            'events_categories_edit_form_fields',
            array( 'view.admin.event-category', 'events_categories_edit_form_fields' )
        );
        $dispatcher->register_action(
            'created_events_categories',
            array( 'view.admin.event-category', 'created_events_categories' )
        );
        $dispatcher->register_action(
            'edited_events_categories',
            array( 'view.admin.event-category', 'edited_events_categories' )
        );
        $dispatcher->register_action(
            'manage_edit-events_categories_columns',
            array( 'view.admin.event-category', 'manage_event_categories_columns' )
        );
        $dispatcher->register_action(
            'manage_events_categories_custom_column',
            array( 'view.admin.event-category', 'manage_events_categories_custom_column' ),
            10,
            3
        );

        // register ICS cron action
        $dispatcher->register_action(
            Ai1ecIcsConnectorPlugin::HOOK_NAME,
            array( 'calendar-feed.ics', 'cron' )
        );
        $dispatcher->register_shortcode(
            'ai1ec',
            array( 'view.calendar.shortcode', 'shortcode' )
        );
        $dispatcher->register_action(
            'updated_option',
            array( 'model.settings', 'wp_options_observer' ),
            PHP_INT_MAX - 1,
            3
        );

        $dispatcher->register_action(
            'ai1ec_settings_updated',
            array( 'compatibility.check', 'ai1ec_settings_observer' ),
            PHP_INT_MAX - 1,
            2
        );
        if ( $this->if_js_cache_enabled() ) {
            $dispatcher->register_action(
                'ai1ec_settings_updated',
                array( 'controller.javascript', 'revalidate_cache' ),
                PHP_INT_MAX - 1
            );
            $dispatcher->register_action(
                'ai1ec_settings_updated',
                array( 'controller.javascript-widget', 'revalidate_cache' ),
                PHP_INT_MAX - 1
            );
        }

        if ( is_admin() ) {
            // Import suggested event
            $dispatcher->register_action(
                'wp_ajax_ai1ec_import_suggested_event',
                array( 'calendar-feed.ics', 'add_discover_events_feed_subscription' )
            );
            // Remove suggested event
            $dispatcher->register_action(
                'wp_ajax_ai1ec_remove_suggested_event',
                array( 'calendar-feed.ics', 'delete_individual_event_subscription' )
            );
            // Search for events
            $dispatcher->register_action(
                'wp_ajax_ai1ec_search_events',
                array( 'calendar-feed.suggested', 'search_events' )
            );
            // get the repeat box
            $dispatcher->register_action(
                'wp_ajax_ai1ec_get_repeat_box',
                array( 'view.admin.get-repeat-box', 'get_repeat_box' )
            );
            // get the tax options box
            $dispatcher->register_action(
                'wp_ajax_ai1ec_get_tax_box',
                array( 'view.admin.get-tax-box', 'get_tax_box' )
            );
            // add dismissable notice handler
            $dispatcher->register_action(
                'wp_ajax_ai1ec_dismiss_notice',
                array( 'notification.admin', 'dismiss_notice' )
            );
            // save rrurle and convert it to text
            $dispatcher->register_action(
                'wp_ajax_ai1ec_rrule_to_text',
                array( 'view.admin.get-repeat-box', 'convert_rrule_to_text' )
            );
            // display ticketing details in the events list
            $dispatcher->register_action(
                'wp_ajax_ai1ec_show_ticket_details',
                array( 'view.admin.all-events', 'show_ticket_details' )
            );
            // display attendees list
            $dispatcher->register_action(
                'wp_ajax_ai1ec_show_attendees',
                array( 'view.admin.all-events', 'show_attendees' )
            );
            // CSS and templates for ticketing options
            $dispatcher->register_action(
                'restrict_manage_posts',
                array( 'view.admin.all-events', 'add_ticketing_styling' )
            );
            // taxonomy filter
            $dispatcher->register_action(
                'restrict_manage_posts',
                array( 'view.admin.all-events', 'taxonomy_filter_restrict_manage_posts' )
            );
            $dispatcher->register_action(
                'parse_query',
                array( 'view.admin.all-events', 'taxonomy_filter_post_type_request' )
            );
            $dispatcher->register_action(
                'admin_menu',
                array( 'view.admin.calendar-feeds', 'add_page' )
            );
            $dispatcher->register_action(
                'current_screen',
                array( 'view.admin.calendar-feeds', 'add_meta_box' )
            );
            $dispatcher->register_action(
                'admin_menu',
                array( 'view.admin.add-ons', 'add_page' )
            );
            $dispatcher->register_action(
                'admin_menu',
                array( 'view.admin.theme-switching', 'add_page' )
            );
            $dispatcher->register_action(
                'admin_menu',
                array( 'view.admin.theme-options', 'add_page' )
            );
            $dispatcher->register_action(
                'current_screen',
                array( 'view.admin.theme-options', 'add_meta_box' )
            );
            $dispatcher->register_action(
                'admin_menu',
                array( 'view.admin.settings', 'add_page' )
            );
            $dispatcher->register_action(
                'current_screen',
                array( 'view.admin.settings', 'add_meta_box' )
            );
            $dispatcher->register_action(
                'init',
                array( 'controller.javascript', 'load_admin_js' )
            );
            $dispatcher->register_action(
                'admin_menu',
                array( 'view.admin.samples', 'add_page' ),
                100,
                1
            );
            $dispatcher->register_action(
                'wp_ajax_ai1ec_add_ics',
                array( 'calendar-feed.ics', 'add_ics_feed' )
            );
            $dispatcher->register_action(
                'wp_ajax_ai1ec_delete_ics',
                array( 'calendar-feed.ics', 'delete_feeds_and_events' )
            );
            $dispatcher->register_action(
                'wp_ajax_ai1ec_update_ics',
                array( 'calendar-feed.ics', 'update_ics_feed' )
            );
            $dispatcher->register_action(
                'wp_ajax_ai1ec_feeds_page_post',
                array( 'calendar-feed.ics', 'handle_feeds_page_post' )
            );
            $dispatcher->register_action(
                'wp_ajax_ai1ec_send_feedback_message',
                array( 'model.review', 'send_feedback_message' )
            );
            $dispatcher->register_action(
                'wp_ajax_ai1ec_save_feedback_review',
                array( 'model.review', 'save_feedback_review' )
            );
            $dispatcher->register_action(
                'network_admin_notices',
                array( 'notification.admin', 'send' )
            );
            $dispatcher->register_action(
                'admin_notices',
                array( 'notification.admin', 'send' )
            );
            $dispatcher->register_action(
                'admin_footer-edit.php',
                array( 'clone.renderer-helper', 'duplicate_custom_bulk_admin_footer' )
            );
            $dispatcher->register_filter(
                'post_row_actions',
                array( 'clone.renderer-helper', 'ai1ec_duplicate_post_make_duplicate_link_row' ),
                100,
                2
            );
            $dispatcher->register_action(
                'add_meta_boxes',
                array( 'view.admin.add-new-event', 'event_meta_box_container' )
            );
            $dispatcher->register_action(
                'edit_form_after_title',
                array( 'view.admin.add-new-event', 'event_inline_alert' )
            );
            $dispatcher->register_action(
                'save_post',
                array( 'model.event.creating', 'save_post' ),
                10,
                3
            );
            $dispatcher->register_action(
                'pre_post_update',
                array( 'model.event.creating', 'pre_post_update' ),
                0,
                2
            );
            $dispatcher->register_action(
                'wp_insert_post_data',
                array( 'model.event.creating', 'wp_insert_post_data' )
            );
            $dispatcher->register_action(
                'manage_ai1ec_event_posts_custom_column',
                array( 'view.admin.all-events', 'custom_columns' ),
                10,
                2
            );
            $dispatcher->register_filter(
                'manage_ai1ec_event_posts_columns',
                array( 'view.admin.all-events', 'change_columns' )
            );
            $dispatcher->register_filter(
                'manage_edit-ai1ec_event_sortable_columns',
                array( 'view.admin.all-events', 'sortable_columns' )
            );
            $dispatcher->register_filter(
                'posts_orderby',
                array( 'view.admin.all-events', 'orderby' ),
                10,
                2
            );
            $dispatcher->register_filter(
                'ai1ec_count_future_events',
                array( 'view.admin.all-events', 'count_future_events' ),
                10,
                1
            );
            $dispatcher->register_filter(
                'post_updated_messages',
                array( 'view.event.post', 'post_updated_messages' )
            );
            add_action( 'admin_head', array( $this, 'admin_head' ) );
            $dispatcher->register_action(
                'plugin_action_links_' . AI1EC_PLUGIN_BASENAME,
                array( 'view.admin.nav', 'plugin_action_links' )
            );
            $dispatcher->register_action(
                'wp_ajax_ai1ec_rescan_cache',
                array( 'twig.cache', 'rescan' )
            );
            $dispatcher->register_action(
                'admin_init',
                array( 'environment.check', 'run_checks' )
            );
            $dispatcher->register_action(
                'activated_plugin',
                array( 'environment.check', 'check_addons_activation' )
            );
            $dispatcher->register_filter(
                'upgrader_post_install',
                array( 'environment.check', 'check_bulk_addons_activation' )
            );
            if ( $this->if_js_cache_enabled() ) {
                $dispatcher->register_action(
                    'activated_plugin',
                    array( 'controller.javascript', 'revalidate_cache' )
                );
                $dispatcher->register_action(
                    'deactivated_plugin',
                    array( 'controller.javascript', 'revalidate_cache' )
                );
                $dispatcher->register_action(
                    'upgrader_post_install',
                    array( 'controller.javascript', 'revalidate_cache' )
                );
                $dispatcher->register_action(
                    'activated_plugin',
                    array( 'controller.javascript-widget', 'revalidate_cache' )
                );
                $dispatcher->register_action(
                    'deactivated_plugin',
                    array( 'controller.javascript-widget', 'revalidate_cache' )
                );
                $dispatcher->register_action(
                    'upgrader_post_install',
                    array( 'controller.javascript-widget', 'revalidate_cache' )
                );

            }
            // Widget Creator
            $dispatcher->register_action(
                'admin_enqueue_scripts',
                array( 'css.admin', 'admin_enqueue_scripts' )
            );
            $dispatcher->register_action(
                'current_screen',
                array( 'view.admin.widget-creator', 'add_meta_box' )
            );
            $dispatcher->register_action(
                'admin_menu',
                array( 'view.admin.widget-creator', 'add_page' )
            );
            $dispatcher->register_filter(
                'pre_set_site_transient_update_plugins',
                array( 'calendar.updates', 'check_updates' )
            );
            $dispatcher->register_filter(
                'plugins_api',
                array( 'calendar.updates', 'plugins_api_filter' ),
                10,
                3
            );
            $dispatcher->register_action(
                'admin_menu',
                array( 'view.admin.tickets', 'add_page' )
            );
            $dispatcher->register_action(
                'admin_menu',
                array( 'view.admin.activate', 'add_page' )
            );
        } else { // ! is_admin()
            $dispatcher->register_action(
                'after_setup_theme',
                array( 'theme.loader', 'execute_theme_functions' )
            );
            $dispatcher->register_action(
                'the_post',
                array( 'post.content', 'check_content' ),
                PHP_INT_MAX
            );
            $dispatcher->register_action(
                'send_headers',
                array( 'request.redirect', 'handle_categories_and_tags' )
            );
            $dispatcher->register_action(
                'wp_head',
                array( 'view.event.single', 'add_meta_tags' )
            );
        }
    }
    /**
     * Outputs menu icon between head tags
     */
    public function admin_head() {
        global $wp_version;
        $argv = array(
            'before_font_icons'    => version_compare( $wp_version, '3.8', '<' ),
            'admin_theme_img_url'  => AI1EC_ADMIN_THEME_IMG_URL,
            'admin_theme_font_url' => AI1EC_ADMIN_THEME_FONT_URL,
        );
        $this->_registry->get( 'theme.loader' )
            ->get_file( 'timely-menu-icon.twig', $argv, true )
            ->render();
    }

    /**
     * _add_defaults method
     *
     * Add (merge) default options to given query variable.
     *
     * @param string settingsquery variable to ammend
     *
     * @return string|NULL Modified variable values or NULL on failure
     *
     * @global    Ai1ec_Settings $ai1ec_settings Instance of settings object
     *                                           to pull data from
     * @staticvar array          $mapper         Mapping of query names to
     *                                           default in settings
     */
    protected function _add_defaults( $name ) {
        $settings = $this->_registry->get( 'model.settings' );
        static $mapper = array(
            'cat' => 'categories',
            'tag' => 'tags',
        );
        $rq_name = 'ai1ec_' . $name . '_ids';
        if (
            ! isset( $mapper[$name] ) ||
            ! array_key_exists( $rq_name, $this->_request )
        ) {
            return NULL;
        }
        $options  = explode( ',', $this->_request[$rq_name] );
        $property = 'default_' . $mapper[$name];
        $options  = array_merge(
            $options,
            $settings->get( $property )
        );
        $filtered = array();
        foreach ( $options as $item ) { // avoid array_filter + is_numeric
            $item = (int)$item;
            if ( $item > 0 ) {
                $filtered[] = $item;
            }
        }
        unset( $options );
        if ( empty( $filtered ) ) {
            return NULL;
        }
        return implode( ',', $filtered );
    }

    /**
     * Process_request function.
     *
     * Initialize/validate custom request array, based on contents of $_REQUEST,
     * to keep track of this component's request variables.
     *
     * @return void
     **/
    protected function _process_request() {
        $settings       = $this->_registry->get( 'model.settings' );
        $this->_request = $this->_registry->get( 'http.request.parser' );
        $aco            = $this->_registry->get( 'acl.aco' );
        $page_id        = $settings->get( 'calendar_page_id' );
        if (
            ! $aco->is_admin() &&
            $page_id &&
            is_page( $page_id )
        ) {
            foreach ( array( 'cat', 'tag' ) as $name ) {
                $implosion = $this->_add_defaults( $name );
                if ( $implosion ) {
                    $this->request['ai1ec_' . $name . '_ids'] = $implosion;
                    $_REQUEST['ai1ec_' . $name . '_ids']      = $implosion;
                }
            }
        }
    }

    /**
     * Initialize cron functions.
     *
     * @throws Ai1ec_Scheduling_Exception
     *
     * @return void
     */
    protected function _install_crons() {
        $scheduling = $this->_registry->get( 'scheduling.utility' );
        $hook_name  = 'ai1ec_n_cron';
        $scheduling->delete( $hook_name );
    }

    /**
     * Initialize the registry object.
     *
     * @param Ai1ec_Loader $ai1ec_loader Instance of Ai1EC classes loader
     *
     * @return void Method does not return
     */
    protected function _initialize_registry( $ai1ec_loader ) {
        global $ai1ec_registry;
        $this->_registry = new Ai1ec_Registry_Object( $ai1ec_loader );
        Ai1ec_Time_Utility::set_registry( $this->_registry );
        $ai1ec_registry  = $this->_registry;
    }

    /**
     * Loads the CSS for the plugin
     *
     */
    protected function _load_css_if_needed() {
        // ==================================
        // = Add the hook to render the css =
        // ==================================
        if ( isset( $_GET[Ai1ec_Css_Frontend::QUERY_STRING_PARAM] ) ) {
            // we need to wait for the extension to be registered if the css
            // needs to be compiled. Will find a better way when compiling css.
            $css_controller = $this->_registry->get( 'css.frontend' );
            add_action( 'plugins_loaded', array( $css_controller, 'render_css' ), 2 );
        }
    }

    /**
     * Load the texdomain for the plugin.
     *
     * @wp_hook plugins_loaded
     *
     * @return void
     */
    public function load_textdomain() {
        if ( false === $this->_domain_loaded ) {
            load_plugin_textdomain(
                AI1EC_PLUGIN_NAME, false, AI1EC_LANGUAGE_PATH
            );
            $this->_domain_loaded = true;
        }
    }

    /**
     * Check if the schema is up to date.
     *
     * @throws Ai1ec_Database_Schema_Exception
     * @throws Ai1ec_Database_Update_Exception
     *
     * @return void
     */
    protected function _initialize_schema() {
        $option     = $this->_registry->get( 'model.option' );
        $schema_sql = $this->get_current_db_schema();
        $version    = sha1( $schema_sql );
        // If existing DB version is not consistent with current plugin's version,
        // or does not exist, then create/update table structure using dbDelta().
        if ( $option->get( 'ai1ec_db_version' ) != $version ) {

            $errors = $this->_registry->get( 'database.applicator' )
                ->check_db_consistency_for_date_migration() ;
            if ( ! empty( $errors ) ) {
                $message = Ai1ec_I18n::__(
                    'Your database is found to be corrupt. Likely previous update has failed. Please restore All-in-One Event Calendar tables from a backup and retry.<br>Following errors were found:<br>%s'
                );
                $message = sprintf( $message, implode( $errors, '<br>' ) );
                throw new Ai1ec_Database_Update_Exception( $message );
            }
            $this->_registry->get( 'database.applicator' )
                ->remove_instance_duplicates();

            if (
                apply_filters( 'ai1ec_perform_scheme_update', true ) &&
                $this->_registry->get( 'database.helper' )->apply_delta(
                    $schema_sql
                )
            ) {
                $option->set( 'ai1ec_db_version', $version );
            } else {
                throw new Ai1ec_Database_Update_Exception();
            }

            // If the schema structure upgrade is complete move contents
            $categories_key = 'ai1ec_category_meta_ported';
            if ( ! $option->get( $categories_key ) ) {
                $this->_migrate_categories_meta();
                $option->set( $categories_key, true );
            }
        }
    }

    /**
     * Transform categories meta information.
     *
     * Use new `meta` table instead of legacy `colors` table.
     *
     * @return void Method does not return.
     */
    protected  function _migrate_categories_meta() {
        $db         = $this->_registry->get( 'dbi.dbi' );
        $table_name = $db->get_table_name( 'ai1ec_event_category_colors' );
        $db_h       = $this->_registry->get( 'database.helper' );
        if ( $db_h->table_exists( $table_name ) ) { // if old table exists otherwise ignore it
            // Migrate color information
            $dest_table = $db->get_table_name( 'ai1ec_event_category_meta' );
            $colors     = $db->select(
                $table_name,
                array( 'term_id', 'term_color'),
                ARRAY_A
            );
            if ( ! empty( $colors ) ) {
                foreach ( $colors as $color ) {
                    $db->insert( $dest_table, $color );
                }
            }
            // Drop the old table
            $db->query( 'DROP TABLE IF EXISTS ' . $table_name );
        }
    }

    /**
     * Procedures to take when upgrading plugin version
     *
     * @return void
     */
    protected function _plugin_upgrade_procedures() {
        $option     = $this->_registry->get( 'model.option' );
        $version    = AI1EC_VERSION;

        if ( $option->get( 'ai1ec_version' ) != $version ) {
            try {
                // Force regeneration of JS cache
                $this->_registry->get( 'controller.javascript' )->revalidate_cache();
                $this->_registry->get( 'controller.javascript-widget' )->revalidate_cache();

                // Run upgrade commands
                $settings = $this->_registry->get( 'model.settings' );
                $settings->perform_upgrade_actions();
            } catch ( Exception $e ) {
            }

            // Update plugin version
            $option->set( 'ai1ec_version', $version );
        }
    }

    /**
     * Get current database schema as a multi SQL statement.
     *
     * @return string Multiline SQL statement.
     */
    public function get_current_db_schema() {
        $dbi = $this->_registry->get( 'dbi.dbi' );
        // =======================
        // = Create table events =
        // =======================
        $table_name = $dbi->get_table_name( 'ai1ec_events' );
        $sql = "CREATE TABLE $table_name (
                post_id bigint(20) NOT NULL,
                start int(10) UNSIGNED NOT NULL,
                end int(10) UNSIGNED,
                timezone_name varchar(50),
                allday tinyint(1) NOT NULL,
                instant_event tinyint(1) NOT NULL DEFAULT 0,
                recurrence_rules longtext,
                exception_rules longtext,
                recurrence_dates longtext,
                exception_dates longtext,
                venue varchar(255),
                country varchar(255),
                address varchar(255),
                city varchar(255),
                province varchar(255),
                postal_code varchar(32),
                show_map tinyint(1),
                contact_name varchar(255),
                contact_phone varchar(32),
                contact_email varchar(128),
                contact_url varchar(255),
                cost varchar(255),
                ticket_url varchar(255),
                ical_feed_url varchar(255),
                ical_source_url varchar(255),
                ical_organizer varchar(255),
                ical_contact varchar(255),
                ical_uid varchar(255),
                show_coordinates tinyint(1),
                latitude decimal(20,15),
                longitude decimal(20,15),
                force_regenerate tinyint(1) NOT NULL DEFAULT 0,
                PRIMARY KEY  (post_id),
                KEY feed_source (ical_feed_url)
                ) CHARACTER SET utf8;";

        // ==========================
        // = Create table instances =
        // ==========================
        $table_name = $dbi->get_table_name( 'ai1ec_event_instances' );
        $sql .= "CREATE TABLE $table_name (
                id bigint(20) NOT NULL AUTO_INCREMENT,
                post_id bigint(20) NOT NULL,
                start int(10) UNSIGNED NOT NULL,
                end int(10) UNSIGNED NOT NULL,
                PRIMARY KEY  (id),
                UNIQUE KEY evt_instance (post_id,start)
                ) CHARACTER SET utf8;";

        // ================================
        // = Create table category colors =
        // ================================
        $table_name = $dbi->get_table_name( 'ai1ec_event_category_meta' );
        $sql .= "CREATE TABLE $table_name (
            term_id bigint(20) NOT NULL,
            term_color varchar(255) NOT NULL,
            term_image varchar(254) NULL DEFAULT NULL,
            PRIMARY KEY  (term_id)
            ) CHARACTER SET utf8;";

        return $sql;
    }
}
