<?php

use Kigkonsult\Icalcreator\Util\UtilRecur;

/**
 * Event instance management model.
 *
 *
 * @author       Time.ly Network, Inc.
 * @since        2.0
 * @package      Ai1EC
 * @subpackage   Ai1EC.Model
 */
class Ai1ec_Event_Instance extends Ai1ec_Base {

    /**
     * @var Ai1ec_Dbi Instance of database abstraction.
     */
    protected $_dbi = null;

    /**
     * DBI utils.
     *
     * @var Ai1ec_Dbi_Utils
     */
    protected $_dbi_utils;

    /**
     * Store locally instance of Ai1ec_Dbi.
     *
     * @param Ai1ec_Registry_Object $registry Injected object registry.
     *
     */
    public function __construct( Ai1ec_Registry_Object $registry ) {
        parent::__construct( $registry );
        $this->_dbi       = $this->_registry->get( 'dbi.dbi' );
        $this->_dbi_utils = $this->_registry->get( 'dbi.dbi-utils' );
    }

    /**
     * Remove entries for given post. Optionally delete particular instance.
     *
     * @param int      $post_id     Event ID to remove instances for.
     * @param int|null $instance_id Instance ID, or null for all.
     *
     * @return int|bool Number of entries removed, or false on failure.
     */
    public function clean( $post_id, $instance_id = null ) {
        $where  = array( 'post_id' => $post_id );
        $format = array( '%d' );
        if ( null !== $instance_id ) {
            $where['id'] = $instance_id;
            $format[]    = '%d';
        }
        return $this->_dbi->delete( 'ai1ec_event_instances', $where, $format );
    }

    /**
     * Remove and then create instance entries for given event.
     *
     * @param Ai1ec_Event $event Instance of event to recreate entries for.
     *
     * @return bool Success.
     */
    public function recreate( Ai1ec_Event $event ) {
        $old_instances = $this->_load_instances( $event->get( 'post_id' ) );
        $instances     = $this->_create_instances_collection( $event );
        $insert        = array();
        foreach ( $instances as $instance ) {
            if ( ! isset( $old_instances[$instance['start'] . ':' . $instance['end']] ) ) {
                $insert[] = $instance;
                continue;
            }
            unset( $old_instances[$instance['start'] . ':' . $instance['end']] );
        }
        $this->_remove_instances_by_ids( array_values( $old_instances ) );
        $this->_add_instances( $insert );
        return true;
    }

    /**
     * Create list of recurrent instances.
     *
     * @param Ai1ec_Event $event          Event to generate instances for.
     * @param array       $event_instance First instance contents.
     * @param int         $_start         Timestamp of first occurence.
     * @param int         $duration       Event duration in seconds.
     * @param string      $timezone       Target timezone.
     *
     * @return array List of event instances.
     */
    public function create_instances_by_recurrence(
        Ai1ec_Event $event,
        array $event_instance,
        $_start,
        $duration,
        $timezone
    ) {
        $restore_timezone  = date_default_timezone_get();
        $recurrence_parser = $this->_registry->get( 'recurrence.rule' );
        $events            = array();

        $start             = $event_instance['start'];
        $wdate             = $startdate = $enddate
            = $this->_parsed_date_array( $_start, $timezone );
        $enddate['year']   = $enddate['year'] + 10;
        $exclude_dates     = array();
        $exception_rules   = $event->get( 'exception_dates' );
        $recurrence_dates  = array();
        $recurrence_rules  = $event->get( 'recurrence_dates' );
        if ( $recurrence_rules ) {
            $recurrence_dates  = $this->_populate_recurring_dates(
                $recurrence_rules,
                $startdate,
                $timezone
            );
        }
        if ( $exception_rules ) {
            $exclude_dates = $this->_populate_recurring_dates(
                $exception_rules,
                $startdate,
                $timezone
            );
        }
        if ( $event->get( 'exception_rules' ) ) {
            // creat an array for the rules
            $exception_rules = $recurrence_parser
                ->build_recurrence_rules_array(
                    $event->get( 'exception_rules' )
                );
            unset($exception_rules['EXDATE']);
            if ( ! empty( $exception_rules ) ) {
                $exception_rules = UtilRecur::setRexrule(
                    $exception_rules
                );
                $result = array();
                date_default_timezone_set( $timezone );
                // The first array is the result and it is passed by reference
                UtilRecur::recur2date(
                    $exclude_dates,
                    $exception_rules,
                    $wdate,
                    $startdate,
                    $enddate
                );
                // Get start date time
                $startHour   = isset( $startdate['hour'] ) ? sprintf( "%02d", $startdate['hour'] ) : '00';
                $startMinute = isset( $startdate['min'] )  ? sprintf( "%02d", $startdate['min'] )  : '00';
                $startSecond = isset( $startdate['sec'] )  ? sprintf( "%02d", $startdate['sec'] )  : '00';
                $startTime   = $startHour . $startMinute . $startSecond;
                // Convert to timestamp
                if ( is_array( $exclude_dates ) ) {
                    $new_exclude_dates = array();
                    foreach ( $exclude_dates as $key => $value ) {
                        $timestamp = strtotime( $key . 'T' . $startTime );
                        $new_exclude_dates[$timestamp] = $value;
                    }
                    $exclude_dates = $new_exclude_dates;
                }
                date_default_timezone_set( $restore_timezone );
            }
        }
        $recurrence_rules = $recurrence_parser
            ->build_recurrence_rules_array(
                $event->get( 'recurrence_rules' )
            );

        $recurrence_rules = UtilRecur::setRexrule( $recurrence_rules );
        if ( $recurrence_rules ) {
            date_default_timezone_set( $timezone );
            UtilRecur::recur2date(
                $recurrence_dates,
                $recurrence_rules,
                $wdate,
                $startdate,
                $enddate
            );

            // Get start date time
            $startHour   = isset( $startdate['hour'] ) ? sprintf( "%02d", $startdate['hour'] ) : '00';
            $startMinute = isset( $startdate['min'] )  ? sprintf( "%02d", $startdate['min'] )  : '00';
            $startSecond = isset( $startdate['sec'] )  ? sprintf( "%02d", $startdate['sec'] )  : '00';
            $startTime   = $startHour . $startMinute . $startSecond;
            // Convert to timestamp
            if ( is_array( $recurrence_dates ) ) {
                $new_recurrence_dates = array();
                foreach ( $recurrence_dates as $key => $value ) {
                    $timestamp = strtotime( $key . 'T' . $startTime );
                    $new_recurrence_dates[$timestamp] = $value;
                }
                $recurrence_dates = $new_recurrence_dates;
            }
            date_default_timezone_set( $restore_timezone );
        }

        if ( ! is_array( $recurrence_dates ) ) {
            $recurrence_dates = array();
        }
        $recurrence_dates = array_keys( $recurrence_dates );
        // Add the instances
        foreach ( $recurrence_dates as $timestamp ) {
            // The arrays are in the form timestamp => true so an isset call is what we need
            if ( ! isset( $exclude_dates[$timestamp] ) ) {
                $event_instance['start'] = $timestamp;
                $event_instance['end']   = $timestamp + $duration;
                $events[$timestamp]      = $event_instance;
            }
        }

        return $events;
    }

    /**
     * Generate and store instance entries in database for given event.
     *
     * @param Ai1ec_Event $event Instance of event to create entries for.
     *
     * @return bool Success.
     */
    public function create( Ai1ec_Event $event ) {
        $instances = $this->_create_instances_collection( $event );
        $this->_add_instances( $instances );
        return true;
    }

    /**
     * Check if given date match dates in EXDATES rule.
     *
     * @param string $date     Date to check.
     * @param string $ics_rule ICS EXDATES rule.
     * @param string $timezone Timezone to evaluate value in.
     *
     * @return bool True if given date is in rule.
     */
    public function date_match_exdates( $date, $ics_rule, $timezone ) {
        $ranges = $this->_get_date_ranges( $ics_rule, $timezone );
        foreach ( $ranges as $interval ) {
            if ( $date >= $interval[0] && $date <= $interval[1] ) {
                return true;
            }
            if ( $date <= $interval[0] ) {
                break;
            }
        }
        return false;
    }

    /**
     * Prepare date range list for fast exdate search.
     *
     * NOTICE: timezone is relevant in only first run.
     *
     * @param string $date_list ICS list provided from data model.
     * @param string $timezone  Timezone in which to evaluate.
     *
     * @return array List of date ranges, sorted in increasing order.
     */
    protected function _get_date_ranges( $date_list, $timezone ) {
        static $ranges = array();
        if ( ! isset( $ranges[$date_list] ) ) {
            $ranges[$date_list] = array();
            $exploded = explode( ',', $date_list );
            sort( $exploded );
            foreach ( $exploded as $date ) {
                // COMMENT on `rtrim( $date, 'Z' )`:
                // user selects exclusion date in event timezone thus it
                // must be parsed as such as opposed to UTC which happen
                // when 'Z' is preserved.
                $date = $this->_registry
                    ->get( 'date.time', rtrim( $date, 'Z' ), $timezone )
                    ->format_to_gmt();
                $ranges[$date_list][] = array(
                    $date,
                    $date + (24 * 60 * 60) - 1
                );
            }
        }
        return $ranges[$date_list];
    }

    protected function _populate_recurring_dates( $rule, array $start_struct, $timezone ) {
        $start = clone $start_struct['_dt'];
        $dates = array();
        foreach ( explode( ',', $rule ) as $date ) {
            $i_date = clone $start;
            $spec   = sscanf( $date, '%04d%02d%02d' );
            $i_date->set_date(
                $spec[0],
                $spec[1],
                $spec[2]
            );
            $dates[$i_date->format_to_gmt()] = $i_date;
        }
        return $dates;
    }

    protected function _parsed_date_array( $startdate, $timezone ) {
        $datetime = $this->_registry->get( 'date.time', $startdate, $timezone );
        $parsed   = array(
            'year'  => intval( $datetime->format( 'Y' ) ),
            'month' => intval( $datetime->format( 'm' ) ),
            'day'   => intval( $datetime->format( 'd' ) ),
            'hour'  => intval( $datetime->format( 'H' ) ),
            'min'   => intval( $datetime->format( 'i' ) ),
            'sec'   => intval( $datetime->format( 's' ) ),
            'tz'    => $datetime->get_timezone(),
            '_dt'   => $datetime,
        );
        return $parsed;
    }

    /**
     * Returns current instances map.
     *
     * @param int post_id Post ID.
     *
     * @return array Array of data.
     */
    protected function _load_instances( $post_id ) {
        $query = $this->_dbi->prepare(
            'SELECT `id`, `start`, `end` FROM ' .
            $this->_dbi->get_table_name( 'ai1ec_event_instances' ) .
            ' WHERE post_id = %d',
            $post_id
        );
        $results   = $this->_dbi->get_results( $query );
        $instances = array();
        foreach ( $results as $result ) {
            $instances[(int)$result->start . ':' . (int)$result->end] = (int)$result->id;
        }
        return $instances;
    }

    /**
     * Generate and store instance entries in database for given event.
     *
     * @param Ai1ec_Event $event Instance of event to create entries for.
     *
     * @return bool Success.
     */
    protected function _create_instances_collection( Ai1ec_Event $event ) {
        $events     = array();
        $event_item = array(
            'post_id' => $event->get( 'post_id' ),
            'start'   => $event->get( 'start'   )->format_to_gmt(),
            'end'     => $event->get( 'end'     )->format_to_gmt(),
        );
        $duration = $event->get( 'end' )->diff_sec( $event->get( 'start' ) );

        $_start = $event->get( 'start' )->format_to_gmt();
        $_end   = $event->get( 'end'   )->format_to_gmt();

        // Always cache initial instance
        $events[$_start] = $event_item;

        if ( $event->get( 'recurrence_rules' ) || $event->get( 'recurrence_dates' ) ) {
            $start_timezone = $this->_registry->get( 'model.option' )
                                              ->get( 'timezone_string' );
            if ( empty( $start_timezone ) ) {
                $start_timezone = $this->_registry->get( 'date.timezone' )->get_default_timezone();
            }

            $events += $this->create_instances_by_recurrence(
                $event,
                $event_item,
                $_start,
                $duration,
                $start_timezone
            );
        }

        $search_helper = $this->_registry->get( 'model.search' );
        foreach ( $events as &$event_item ) {
            // Find out if this event instance is already accounted for by an
            // overriding 'RECURRENCE-ID' of the same iCalendar feed (by comparing the
            // UID, start date, recurrence). If so, then do not create duplicate
            // instance of event.
            $start             = $event_item['start'];
            $matching_event_id = null;
            if ( $event->get( 'ical_uid' ) ) {
                $matching_event_id = $search_helper->get_matching_event_id(
                    $event->get( 'ical_uid' ),
                    $event->get( 'ical_feed_url' ),
                    $event->get( 'start' ),
                    false,
                    $event->get( 'post_id' )
                );
            }

            // If no other instance was found
            if ( null !== $matching_event_id ) {
                $event_item = false;
            }
        }

        return array_filter( $events );
    }

    /**
     * Removes ai1ec_event_instances entries using their IDS.
     *
     * @param array $ids Collection of IDS.
     *
     * @return bool Result.
     */
    protected function _remove_instances_by_ids( array $ids ) {
        if ( empty( $ids ) ) {
            return false;
        }
        $query  = 'DELETE FROM ' . $this->_dbi->get_table_name(
                'ai1ec_event_instances'
            ) . ' WHERE id IN (';
        $ids    = array_filter( array_map( 'intval', $ids ) );
        $query .= implode( ',', $ids ) . ')';
        $this->_dbi->query( $query );
        return true;
    }

    /**
     * Adds new instances collection.
     *
     * @param array $instances Collection of instances.
     *
     * @return void
     */
    protected function _add_instances( array $instances ) {
        $chunks    = array_chunk( $instances, 50 );
        foreach ( $chunks as $chunk ) {
            $query = 'INSERT INTO ' . $this->_dbi->get_table_name(
                    'ai1ec_event_instances'
                ) . '(`post_id`, `start`, `end`) VALUES';
            $chunk  = array_map(
                array( $this->_dbi_utils, 'array_value_to_sql_value' ),
                $chunk
            );
            $query .= implode( ',', $chunk );
            $this->_dbi->query( $query );
        }
    }
}
