<?php

/**
 * Search Event.
 *
 * @author     Time.ly Network Inc.
 * @since      2.0
 *
 * @package    AI1EC
 * @subpackage AI1EC.search
 */
class Ai1ec_Event_Search extends Ai1ec_Base {

    /**
     * @var Ai1ec_Dbi instance
     */
    private $_dbi = null;

    /**
     * Caches the ids of the last 'between' query
     *
     * @var array
     */
    protected $_ids_between_cache = array();

    /**
     * Creates local DBI instance.
     */
    public function __construct( Ai1ec_Registry_Object $registry ){
        parent::__construct( $registry );
        $this->_dbi = $this->_registry->get( 'dbi.dbi' );
    }

    /**
     * @return array
     */
    public function get_cached_between_ids() {
        return $this->_ids_between_cache;
    }

    /**
     * Fetches the event object with the given post ID.
     *
     * Uses the WP cache to make this more efficient if possible.
     *
     * @param int      $post_id     The ID of the post associated.
     * @param bool|int $instance_id Instance ID, to fetch post details for.
     *
     * @return Ai1ec_Event The associated event object.
     */
    public function get_event( $post_id, $instance_id = false ) {
        $post_id     = (int)$post_id;
        $instance_id = (int)$instance_id;
        if ( $instance_id < 1 ) {
            $instance_id = false;
        }
        return $this->_registry->get( 'model.event', $post_id, $instance_id );
    }

    /**
     * Return events falling within some time range.
     *
     * Return all events starting after the given start time and before the
     * given end time that the currently logged in user has permission to view.
     * If $spanning is true, then also include events that span this
     * period. All-day events are returned first.
     *
     * @param Ai1ec_Date_Time $start Limit to events starting after this.
     * @param Ai1ec_Date_Time $end   Limit to events starting before this.
     * @param array $filter          Array of filters for the events returned:
     *                                   ['cat_ids']      => list of category IDs;
     *                                   ['tag_ids']      => list of tag IDs;
     *                                   ['post_ids']     => list of post IDs;
     *                                   ['auth_ids']     => list of author IDs;
     *                                   ['instance_ids'] => list of events
     *                                                       instance ids;
     * @param bool $spanning         Also include events that span this period.
     * @param bool $single_day       This parameter is added for oneday view.
     *                               Query should find events lasting in
     *                               particular day instead of checking dates
     *                               range. If you need to call this method
     *                               with $single_day set to true consider
     *                               using method get_events_for_day. This
     *                               parameter matters only if $spanning is set
     *                               to false.
     *
     * @return array List of matching event objects.
     */
    public function get_events_between(
        Ai1ec_Date_Time $start,
        Ai1ec_Date_Time $end,
        array $filter = array(),
        $spanning     = false,
        $single_day   = false
    ) {
        // Query arguments
        $args = array(
            $start->format_to_gmt(),
            $end->format_to_gmt(),
        );

        // Get post status Where snippet and associated SQL arguments
        $where_parameters  = $this->_get_post_status_sql();
        $post_status_where = $where_parameters['post_status_where'];
        $args              = array_merge( $args, $where_parameters['args'] );

        // Get the Join (filter_join) and Where (filter_where) statements based
        // on $filter elements specified
        $filter = $this->_get_filter_sql( $filter );

        $ai1ec_localization_helper = $this->_registry->get( 'p28n.wpml' );

        $wpml_join_particle = $ai1ec_localization_helper
            ->get_wpml_table_join( 'p.ID' );

        $wpml_where_particle = $ai1ec_localization_helper
            ->get_wpml_table_where();

        if ( $spanning ) {
            $spanning_string = 'i.end > %d AND i.start < %d ';
        } elseif ( $single_day ) {
            $spanning_string = 'i.end >= %d AND i.start <= %d ';
        } else {
            $spanning_string = 'i.start BETWEEN %d AND %d ';
        }

        $sql = '
            SELECT
                `p`.*,
                `e`.`post_id`,
                `i`.`id` AS `instance_id`,
                `i`.`start` AS `start`,
                `i`.`end` AS `end`,
                `e`.`timezone_name` AS `timezone_name`,
                `e`.`allday` AS `event_allday`,
                `e`.`recurrence_rules`,
                `e`.`exception_rules`,
                `e`.`recurrence_dates`,
                `e`.`exception_dates`,
                `e`.`venue`,
                `e`.`country`,
                `e`.`address`,
                `e`.`city`,
                `e`.`province`,
                `e`.`postal_code`,
                `e`.`instant_event`,
                `e`.`show_map`,
                `e`.`contact_name`,
                `e`.`contact_phone`,
                `e`.`contact_email`,
                `e`.`contact_url`,
                `e`.`cost`,
                `e`.`ticket_url`,
                `e`.`ical_feed_url`,
                `e`.`ical_source_url`,
                `e`.`ical_organizer`,
                `e`.`ical_contact`,
                `e`.`ical_uid`,
                `e`.`longitude`,
                `e`.`latitude`
            FROM
                ' . $this->_dbi->get_table_name( 'ai1ec_events' ) . ' e
                INNER JOIN
                    ' . $this->_dbi->get_table_name( 'posts' ) . ' p
                        ON ( `p`.`ID` = `e`.`post_id` )
                ' . $wpml_join_particle . '
                INNER JOIN
                    ' . $this->_dbi->get_table_name( 'ai1ec_event_instances' ) . ' i
                    ON ( `e`.`post_id` = `i`.`post_id` )
                ' . $filter['filter_join'] . '
            WHERE
                post_type = \'' . AI1EC_POST_TYPE . '\'
                ' . $wpml_where_particle . '
            AND
                ' . $spanning_string . '
                ' . $filter['filter_where'] . '
                ' . $post_status_where . '
            GROUP BY
                `i`.`id`
            ORDER BY
                `e` . `allday`     DESC,
                `i` . `start`      ASC,
                `p` . `post_title` ASC';

        $query  = $this->_dbi->prepare( $sql, $args );
        $events = $this->_dbi->get_results( $query, ARRAY_A );

        $id_list = array();
        $id_instance_list = array();
        foreach ( $events as $event ) {

            if ( ! in_array( $event['post_id'], $id_list, true ) ) {
                $id_list[] = $event['post_id'];
            }

            $id_instance_list[] = array(
                'id'          => $event['post_id'],
                'instance_id' => $event['instance_id'],
            );
        }

        if ( ! empty( $id_list ) ) {
            update_meta_cache( 'post', $id_list );
            $this->_ids_between_cache = $id_instance_list;
        }

        foreach ( $events as &$event ) {
            $event['allday'] = $this->_is_all_day( $event );
            $event           = $this->_registry->get( 'model.event', $event );
        }

        return $events;
    }

    /**
     * get_events_relative_to function
     *
     * Return all events starting after the given reference time, limiting the
     * result set to a maximum of $limit items, offset by $page_offset. A
     * negative $page_offset can be provided, which will return events *before*
     * the reference time, as expected.
     *
     * @param int $time           limit to events starting after this (local) UNIX time
     * @param int $limit          return a maximum of this number of items
     * @param int $page_offset    offset the result set by $limit times this number
     * @param array $filter       Array of filters for the events returned.
     *                            ['cat_ids']      => non-associatative array of category IDs
     *                            ['tag_ids']      => non-associatative array of tag IDs
     *                            ['post_ids']     => non-associatative array of post IDs
     *                            ['auth_ids']     => non-associatative array of author IDs
     *                            ['instance_ids'] => non-associatative array of author IDs
     * @param int $last_day       Last day (time), that was displayed.
     *                            NOTE FROM NICOLA: be careful, if you want a query with events
     *                            that have a start date which is greater than today, pass 0 as
     *                            this parameter. If you pass false ( or pass nothing ) you end up with a query
     *                            with events that finish before today. I don't know the rationale
     *                            behind this but that's how it works
     * @param bool $unique        Whether display only unique events and don't
     *                            duplicate results with other instances or not.
     *
     * @return array              five-element array:
     *                              ['events'] an array of matching event objects
     *                              ['prev'] true if more previous events
     *                              ['next'] true if more next events
     *                              ['date_first'] UNIX timestamp (date part) of first event
     *                              ['date_last'] UNIX timestamp (date part) of last event
     */
    public function get_events_relative_to(
        $time,
        $limit       = 0,
        $page_offset = 0,
        $filter      = array(),
        $last_day    = false,
        $unique      = false
    ) {
        $localization_helper = $this->_registry->get( 'p28n.wpml' );
        $settings = $this->_registry->get( 'model.settings' );


        // Even if there ARE more than 5 times the limit results - we shall not
        // try to fetch and display these, as it would crash system
        $limit = preg_replace('/\D/', '', $limit);
        $upper_boundary = $limit;
        if (
            $settings->get( 'agenda_include_entire_last_day' ) &&
        ( false !== $last_day )
        ) {
            $upper_boundary *= 5;
        }

        // Convert timestamp to GMT time
        $time = $this->_registry->get(
            'date.system'
        )->get_current_rounded_time();
        // Get post status Where snippet and associated SQL arguments
        $where_parameters  = $this->_get_post_status_sql();
        $post_status_where = $where_parameters['post_status_where'];

        // Get the Join (filter_join) and Where (filter_where) statements based
        // on $filter elements specified
        $filter = $this->_get_filter_sql( $filter );

        // Query arguments
        $args = array( $time );
        $args = array_merge( $args, $where_parameters['args'] );

        if( $page_offset >= 0 ) {
            $first_record = $page_offset * $limit;
        } else {
            $first_record = ( -$page_offset - 1 ) * $limit;
        }


        $wpml_join_particle  = $localization_helper
            ->get_wpml_table_join( 'p.ID' );

        $wpml_where_particle = $localization_helper
            ->get_wpml_table_where();

        $filter_date_clause = ( $page_offset >= 0 )
            ? 'i.end >= %d '
            : 'i.start < %d ';
        $order_direction    = ( $page_offset >= 0 ) ? 'ASC' : 'DESC';
        if ( false !== $last_day ) {
            if ( 0 == $last_day ) {
                $last_day = $time;
            }
            $filter_date_clause = ' i.end ';
            if ( $page_offset < 0 ) {
                $filter_date_clause .= '<';
                $order_direction     = 'DESC';
            } else {
                $filter_date_clause .= '>';
                $order_direction     = 'ASC';
            }
            $filter_date_clause .= ' %d ';
            $args[0]             = $last_day;
            $first_record        = 0;
        }
        $query = $this->_dbi->prepare(
            'SELECT DISTINCT p.*, e.post_id, i.id AS instance_id, ' .
            'i.start AS start, ' .
            'i.end AS end, ' .
            'e.allday AS event_allday, ' .
            'e.recurrence_rules, e.exception_rules, e.ticket_url, e.instant_event, e.recurrence_dates, e.exception_dates, ' .
            'e.venue, e.country, e.address, e.city, e.province, e.postal_code, ' .
            'e.show_map, e.contact_name, e.contact_phone, e.contact_email, e.cost, ' .
            'e.ical_feed_url, e.ical_source_url, e.ical_organizer, e.ical_contact, e.ical_uid, e.timezone_name, e.longitude, e.latitude ' .
            'FROM ' . $this->_dbi->get_table_name( 'ai1ec_events' ) . ' e ' .
            'INNER JOIN ' . $this->_dbi->get_table_name( 'posts' ) . ' p ON e.post_id = p.ID ' .
            $wpml_join_particle .
            ' INNER JOIN ' . $this->_dbi->get_table_name( 'ai1ec_event_instances' ) . ' i ON e.post_id = i.post_id ' .
            $filter['filter_join'] .
            " WHERE post_type = '" . AI1EC_POST_TYPE . "' " .
            ' AND ' . $filter_date_clause .
            $wpml_where_particle .
            $filter['filter_where'] .
            $post_status_where .
            ( $unique ? ' GROUP BY e.post_id' : '' ) .
            // Reverse order when viewing negative pages, to get correct set of
            // records. Then reverse results later to order them properly.
            ' ORDER BY i.start ' . $order_direction .
            ', post_title ' . $order_direction .
            ' LIMIT ' . $first_record . ', ' . $upper_boundary,
            $args
        );

        $events = $this->_dbi->get_results( $query, ARRAY_A );

        // Limit the number of records to convert to data-object
        $events = $this->_limit_result_set(
            $events,
            $limit,
            ( false !== $last_day )
        );

        // Reorder records if in negative page offset
        if( $page_offset < 0 ) {
            $events = array_reverse( $events );
        }

        $date_first = $date_last = NULL;

        foreach ( $events as &$event ) {
            $event['allday'] = $this->_is_all_day( $event );
            $event           = $this->_registry->get( 'model.event', $event );
            if ( null === $date_first ) {
                $date_first = $event->get( 'start' );
            }
            $date_last = $event->get( 'start' );
        }
        $date_first = $this->_registry->get( 'date.time', $date_first );
        $date_last  = $this->_registry->get( 'date.time', $date_last );
        // jus show next/prev links, in case no event found is shown.
        $next = true;
        $prev = true;

        return array(
            'events'     => $events,
            'prev'       => $prev,
            'next'       => $next,
            'date_first' => $date_first,
            'date_last'  => $date_last,
        );
    }

    /**
     * get_events_relative_to_reference function
     *
     * Return all events starting after the given date reference, limiting the
     * result set to a maximum of $limit items, offset by $page_offset. A
     * negative $page_offset can be provided, which will return events *before*
     * the reference time, as expected.
     *
     * @param int $date_reference if page_offset is greater than or equal to zero, events with start date greater than the date_reference will be returned
     *                               otherwise events with start date less than the date_reference will be returned.
     * @param int $limit          return a maximum of this number of items
     * @param int $page_offset    offset the result set by $limit times this number
     * @param array $filter       Array of filters for the events returned.
     *                            ['cat_ids']      => non-associatative array of category IDs
     *                            ['tag_ids']      => non-associatative array of tag IDs
     *                            ['post_ids']     => non-associatative array of post IDs
     *                            ['auth_ids']     => non-associatative array of author IDs
     *                            ['instance_ids'] => non-associatative array of author IDs
     * @param bool $unique        Whether display only unique events and don't
     *                            duplicate results with other instances or not.
     *
     * @return array              five-element array:
     *                              ['events'] an array of matching event objects
     *                              ['prev'] true if more previous events
     *                              ['next'] true if more next events
     *                              ['date_first'] UNIX timestamp (date part) of first event
     *                              ['date_last'] UNIX timestamp (date part) of last event
     */
    public function get_events_relative_to_reference( $date_reference, $limit = 0, $page_offset = 0, $filter = array(), $unique = false ) {
        $localization_helper = $this->_registry->get( 'p28n.wpml' );
        $settings = $this->_registry->get( 'model.settings' );

        // Even if there ARE more than 5 times the limit results - we shall not
        // try to fetch and display these, as it would crash system
        $limit = preg_replace( '/\D/', '', $limit );

        // Convert timestamp to GMT time
        if ( 0 == $date_reference ) {
            $timezone     = $this->_registry->get( 'date.timezone' )->get( $settings->get( 'timezone_string' ) );
            $current_time = new DateTime( 'now' );
            $current_time->setTimezone( $timezone );
            $time         = $current_time->format( 'U' );
        } else {
            $time = $date_reference;
        }

        // Get post status Where snippet and associated SQL arguments
        $where_parameters = $this->_get_post_status_sql();
        $post_status_where = $where_parameters['post_status_where'];

        // Get the Join (filter_join) and Where (filter_where) statements based
        // on $filter elements specified
        $filter = $this->_get_filter_sql( $filter );

        // Query arguments
        $args = array( $time );
        $args = array_merge( $args, $where_parameters['args'] );

        if ( 0 == $date_reference ) {
            if ( $page_offset >= 0 ) {
                $filter_date_clause = 'i.end >= %d ';
                $order_direction    = 'ASC';
            } else {
                $filter_date_clause = 'i.start < %d ';
                $order_direction    = 'DESC';
            }
        } else {
            if ( $page_offset < 0 ) {
                $filter_date_clause = 'i.end < %d ';
                $order_direction    = 'DESC';
            } else {
                $filter_date_clause = 'i.end >= %d ';
                $order_direction    = 'ASC';
            }
        }
        if ( $page_offset >= 0 ) {
            $first_record       = $page_offset * $limit;
        } else {
            $first_record       = ( - $page_offset - 1 ) * $limit;
        }
        $wpml_join_particle  = $localization_helper->get_wpml_table_join( 'p.ID' );
        $wpml_where_particle = $localization_helper->get_wpml_table_where();

        $query = $this->_dbi->prepare(
            'SELECT DISTINCT p.*, e.post_id, i.id AS instance_id, ' . 'i.start AS start, ' . 'i.end AS end, ' .
                 'e.allday AS event_allday, ' .
                 'e.recurrence_rules, e.exception_rules, e.ticket_url, e.instant_event, e.recurrence_dates, e.exception_dates, ' .
                 'e.venue, e.country, e.address, e.city, e.province, e.postal_code, ' .
                 'e.show_map, e.contact_name, e.contact_phone, e.contact_email, e.cost, ' .
                 'e.ical_feed_url, e.ical_source_url, e.ical_organizer, e.ical_contact, e.ical_uid, e.timezone_name, e.longitude, e.latitude ' .
                 'FROM ' . $this->_dbi->get_table_name( 'ai1ec_events' ) . ' e ' . 'INNER JOIN ' .
                 $this->_dbi->get_table_name( 'posts' ) . ' p ON e.post_id = p.ID ' . $wpml_join_particle .
                 ' INNER JOIN ' . $this->_dbi->get_table_name( 'ai1ec_event_instances' ) . ' i ON e.post_id = i.post_id ' .
                 $filter['filter_join'] . " WHERE post_type = '" . AI1EC_POST_TYPE . "' " . ' AND ' . $filter_date_clause .
                 $wpml_where_particle . $filter['filter_where'] . $post_status_where .
                 ( $unique ? ' GROUP BY e.post_id' : '' ) .
                // Reverse order when viewing negative pages, to get correct set of
                // records. Then reverse results later to order them properly.
                ' ORDER BY i.start ' . $order_direction . ', post_title ' . $order_direction . ' LIMIT ' . $first_record .
                 ', ' . ( $limit + 1 ),
                $args );

        $events = $this->_dbi->get_results( $query, ARRAY_A );

        if ( $page_offset >= 0 ) {
            $prev = true;
            $next = ( count( $events ) > $limit );
            if ( $next ) {
                 array_pop( $events );
            }
        } else {
            $prev = ( count( $events ) > $limit );
            if ( $prev ) {
                array_pop( $events );
            }
            $next = true;
        }

        // Reorder records if in negative page offset
        if ( $page_offset < 0 ) {
            $events = array_reverse( $events );
        }

        $date_first = $date_last = NULL;

        foreach ( $events as &$event ) {
            $event['allday'] = $this->_is_all_day( $event );
            $event = $this->_registry->get( 'model.event', $event );
            if ( null === $date_first ) {
                $date_first = $event->get( 'start' );
            }
            $date_last = $event->get( 'start' );
        }
        $date_first = $this->_registry->get( 'date.time', $date_first );
        $date_last = $this->_registry->get( 'date.time', $date_last );

        return array(
            'events' => $events,
            'prev' => $prev,
            'next' => $next,
            'date_first' => $date_first,
            'date_last' => $date_last );
    }

    /**
     * Returns events for given day. Event must start before end of day and must
     * ends after beginning of day.
     *
     * @param Ai1ec_Date_Time $day    Date object.
     * @param array           $filter Search filters;
     *
     * @return array List of events.
     */
    public function get_events_for_day(
        Ai1ec_Date_Time $day,
        array $filter = array()
    ) {
        $end_of_day   = $this->_registry->get( 'date.time', $day )
            ->set_time( 23, 59, 59 );
        $start_of_day = $this->_registry->get( 'date.time', $day )
            ->set_time( 0, 0, 0 );
        return $this->get_events_between(
            $start_of_day,
            $end_of_day,
            $filter,
            false,
            true
        );
    }

    /**
     * Get ID of event in database, matching imported one.
     *
     * Return event ID by iCalendar UID, feed url, start time and whether the
     * event has recurrence rules (to differentiate between an event with a UID
     * defining the recurrence pattern, and other events with with the same UID,
     * which are just RECURRENCE-IDs).
     *
     * @param int      $uid             iCalendar UID property
     * @param string   $feed            Feed URL
     * @param int      $start           Start timestamp (GMT)
     * @param bool     $has_recurrence  Whether the event has recurrence rules
     * @param int|null $exclude_post_id Do not match against this post ID
     *
     * @return object|null ID of matching event post, or NULL if no match
     */
    public function get_matching_event_id(
        $uid,
        $feed,
        $start,
        $has_recurrence  = false,
        $exclude_post_id = null
    ) {
        $dbi        = $this->_registry->get( 'dbi.dbi' );
        $table_name = $dbi->get_table_name( 'ai1ec_events' );
        $query      = 'SELECT `post_id` FROM ' . $table_name . '
            WHERE
                    ical_feed_url   = %s
                AND ical_uid        = %s
                AND start           = %d ' .
            ( $has_recurrence ? 'AND NOT ' : 'AND ' ) .
            ' ( recurrence_rules IS NULL OR recurrence_rules = \'\' )';
        $args = array( $feed, $uid );
        if ( $start instanceof Ai1ec_Date_Time ) {
            $args[] = $start->format();
        } else {
            $args[] = (int)$start;
        }
        if ( null !== $exclude_post_id ) {
            $query .= ' AND post_id <> %d';
            $args[] = $exclude_post_id;
        }

        return $dbi->get_var( $dbi->prepare( $query, $args ) );
    }

    /**
     * Get event by UID. UID must be unique.
     *
     * NOTICE: deletes events with that UID if they have different URLs.
     *
     * @param string $uid UID from feed.
     * @param string $uid Feed URL.
     *
     * @return int|null Matching Event ID or NULL if none found.
     */
    public function get_matching_event_by_uid_and_url( $uid, $url ) {
        if ( ! isset( $uid{1} ) ) {
            return null;
        }
        $dbi        = $this->_registry->get( 'dbi.dbi' );
        $table_name = $dbi->get_table_name( 'ai1ec_events' );
        $argv       = array( $url, $uid, $url );
        // fix issue where invalid feed URLs were assigned
        $update     = 'UPDATE ' . $table_name . ' SET `ical_feed_url` = %s' .
            ' WHERE `ical_uid` = %s AND `ical_feed_url` != %s';
        $query   = $dbi->prepare( $update, $argv);
        $success = $dbi->query( $query );

        // retrieve actual feed ID if any
        $select = 'SELECT `post_id` FROM `' . $table_name .
            '` WHERE `ical_uid` = %s';
        return $dbi->get_var( $dbi->prepare( $select, array( $uid ) ) );
    }

    /**
     * Get event ids for the passed feed url
     *
     * @param string $feed_url
     */
    public function get_event_ids_for_feed( $feed_url ) {
        $dbi        = $this->_registry->get( 'dbi.dbi' );
        $table_name = $dbi->get_table_name( 'ai1ec_events' );
        $query      = 'SELECT `post_id` FROM ' . $table_name .
                        ' WHERE ical_feed_url = %s';
        return $dbi->get_col( $dbi->prepare( $query, array( $feed_url ) ) );
    }

    /**
     * Returns events instances closest to today.
     *
     * @param array $events_ids Events ids filter.
     *
     * @return array Events collection.
     * @throws Ai1ec_Bootstrap_Exception
     */
    public function get_instances_closest_to_today( array $events_ids = array() ) {
        $where_events_ids = '';
        if ( ! empty( $events_ids ) ) {
            $where_events_ids = 'i.post_id IN ('
                . implode( ',', $events_ids ) . ') AND ';
        }
        $query = 'SELECT i.id, i.post_id FROM ' .
            $this->_dbi->get_table_name( 'ai1ec_event_instances' ) .
            ' i WHERE ' .
            $where_events_ids .
            ' i.start > %d ' .
            ' GROUP BY i.post_id';
        /** @var $today Ai1ec_Date_Time */
        $today   = $this->_registry->get( 'date.time', 'now', 'sys.default' );
        $today->set_time( 0, 0, 0 );
        $query   = $this->_dbi->prepare( $query, $today->format( 'U' ) );
        $results = $this->_dbi->get_results( $query );
        $events  = array();
        foreach ( $results as $result ) {
            $events[] = $this->get_event(
                $result->post_id,
                $result->id
            );
        }

        return $events;
    }

    /**
     * Check if given event must be treated as all-day event.
     *
     * Event instances that span 24 hours are treated as all-day.
     * NOTICE: event is passed in before being transformed into
     * Ai1ec_Event object, with Ai1ec_Date_Time fields.
     *
     * @param array $event Event data returned from database.
     *
     * @return bool True if event is all-day event.
     */
    protected function _is_all_day( array $event ) {
        if ( isset( $event['event_allday'] ) && $event['event_allday'] ) {
            return true;
        }

        if ( ! isset( $event['start'] ) || ! isset( $event['end'] ) ) {
            return false;
        }

        return ( 86400 === $event['end'] - $event['start'] );
    }

    /**
     * _limit_result_set function
     *
     * Slice given number of events from list, with exception when all
     * events from last day shall be included.
     *
     * @param array $events   List of events to slice
     * @param int   $limit    Number of events to slice-off
     * @param bool  $last_day Set to true to include all events from last day ignoring {$limit}
     *
     * @return array Sliced events list
     */
    protected function _limit_result_set(
        array $events,
        $limit,
        $last_day
    ) {
        $limited_events     = array();
        $start_day_previous = 0;
        foreach ( $events as $event ) {
            $start_day = date(
                'Y-m-d',
                $event['start']
            );
            --$limit; // $limit = $limit - 1;
            if ( $limit < 0 ) {
                if ( true === $last_day ) {
                    if ( $start_day != $start_day_previous ) {
                        break;
                    }
                } else {
                    break;
                }
            }
            $limited_events[]   = $event;
            $start_day_previous = $start_day;
        }
        return $limited_events;
    }

    /**
     * _get_post_status_sql function
     *
     * Returns SQL snippet for properly matching event posts, as well as array
     * of arguments to pass to $this_dbi->prepare, in function argument
     * references.
     * Nothing is returned by the function.
     *
     * @return array An array containing post_status_where: the sql string,
     * args: the arguments for prepare()
     */
    protected function _get_post_status_sql() {
        $args = array();

        // Query the correct post status
        if (
            current_user_can( 'administrator' ) ||
            current_user_can( 'editor' ) ||
            current_user_can( 'read_private_ai1ec_events' )
        ) {
            // User has privilege of seeing all published and private
            $post_status_where = 'AND post_status IN ( %s, %s ) ';
            $args[]            = 'publish';
            $args[]            = 'private';
        } elseif ( is_user_logged_in() ) {
            // User has privilege of seeing all published and only their own
            // private posts.

            // Get user ID
            $user_id           = 0;
            if ( is_callable( 'wp_get_current_user' ) ) {
                $user          = wp_get_current_user();
                $user_id       = (int)$user->ID;
                unset( $user );
            }

            // include post_status = published
            //   OR
            // post_status = private AND post_author = userID
            $post_status_where =
                'AND ( ' .
                'post_status = %s ' .
                'OR ( post_status = %s AND post_author = %d ) ' .
                ') ';

            $args[] = 'publish';
            $args[] = 'private';
            $args[] = $user_id;
        } else {
            // User can only see published posts.
            $post_status_where = 'AND post_status = %s ';
            $args[]            = 'publish';
        }

        return array(
            'post_status_where' => $post_status_where,
            'args'              => $args
        );
    }

    /**
     * Take filter and return SQL options.
     *
     * Takes an array of filtering options and turns it into JOIN and WHERE
     * statements for running an SQL query limited to the specified options.
     *
     * @param array $filter Array of filters for the events returned:
     *                          ['cat_ids']      => list of category IDs
     *                          ['tag_ids']      => list of tag IDs
     *                          ['post_ids']     => list of event post IDs
     *                          ['auth_ids']     => list of event author IDs
     *                          ['instance_ids'] => list of event instance IDs
     *
     * @return array The modified filter array to having:
     *                   ['filter_join']  the Join statements for the SQL
     *                   ['filter_where'] the Where statements for the SQL
     */
    protected function _get_filter_sql( $filter ) {
        $filter_join = $filter_where = array();
        foreach ( $filter as $filter_type => $filter_ids ) {
            $filter_object = null;
            try {
                if ( empty( $filter_ids ) ) {
                    $filter_ids = array();
                }
                $filter_object = $this->_registry->get(
                    'model.filter.' . $filter_type,
                    $filter_ids
                );
                if ( ! ( $filter_object instanceof Ai1ec_Filter_Interface ) ) {
                    throw new Ai1ec_Bootstrap_Exception(
                        'Filter \'' . get_class( $filter_object ) .
                        '\' is not instance of Ai1ec_Filter_Interface'
                    );
                }
            } catch ( Ai1ec_Bootstrap_Exception $exception ) {
                continue;
            }
            $filter_join[]  = $filter_object->get_join();
            $filter_where[] = $filter_object->get_where();
        }

        $filter_join  = array_filter( $filter_join );
        $filter_where = array_filter( $filter_where );
        $filter_join  = join( ' ', $filter_join );
        if ( count( $filter_where ) > 0 ) {
            $operator     = $this->get_distinct_types_operator();
            $filter_where = $operator . '( ' .
                implode( ' ) ' . $operator . ' ( ', $filter_where ) .
                ' ) ';
        } else {
            $filter_where = '';
        }

        return $filter + compact( 'filter_where', 'filter_join' );
    }

    /**
     * Get operator for joining distinct filters in WHERE.
     *
     * @return string SQL operator.
     */
    public function get_distinct_types_operator() {
        static $operators = array( 'AND' => 1, 'OR' => 2 );
        $default          = 'AND';
        $where_operator   = strtoupper( trim( (string)apply_filters(
            'ai1ec_filter_distinct_types_logic',
            $default
        ) ) );
        if ( ! isset( $operators[$where_operator] ) ) {
            $where_operator = $default;
        }
        return $where_operator;
    }

}
