<?php
/**
 * Class which represnt event parent/child relationship.
 *
 * @author       Time.ly Network, Inc.
 * @since        2.0
 * @package      Ai1EC
 * @subpackage   Ai1EC.Model
 */
class Ai1ec_Event_Parent extends Ai1ec_Base {

    /**
     * event_parent method
     *
     * Get/set event parent
     *
     * @param int $event_id    ID of checked event
     * @param int $parent_id   ID of new parent [optional=NULL, acts as getter]
     * @param int $instance_id ID of old instance id
     *
     * @return int|bool Value depends on mode:
     *     Getter: {@see self::get_parent_event()} for details
     *     Setter: true on success.
     */
    public function event_parent(
        $event_id,
        $parent_id   = null,
        $instance_id = null
    ) {
        $meta_key = '_ai1ec_event_parent';
        if ( null === $parent_id ) {
            return $this->get_parent_event( $event_id );
        }
        $meta_value = json_encode( array(
            'created'  => $this->_registry->get( 'date.system' )->current_time(),
            'instance' => $instance_id,
        ) );
        return add_post_meta( $event_id, $meta_key, $meta_value, true );
    }

    /**
     * Get parent ID for given event
     *
     * @param int $current_id Current event ID
     *
     * @return int|bool ID of parent event or bool(false)
     */
    public function get_parent_event( $current_id ) {
        static $parents = null;
        if ( null === $parents ) {
            $parents = $this->_registry->get( 'cache.memory' );
        }
        $current_id = (int)$current_id;
        if ( null === ( $parent_id = $parents->get( $current_id ) ) ) {
            $db = $this->_registry->get( 'dbi.dbi' );
            /* @var $db Ai1ec_Dbi */
            $query  = '
                SELECT parent.ID, parent.post_status
                FROM
                    ' . $db->get_table_name( 'posts' ) . ' AS child
                    INNER JOIN ' . $db->get_table_name( 'posts' ) . ' AS parent
                        ON ( parent.ID = child.post_parent )
                WHERE child.ID = ' . $current_id;
            $parent = $db->get_row( $query );
            if (
                empty( $parent ) ||
                'trash' === $parent->post_status
            ) {
                $parent_id = false;
            } else {
                $parent_id = $parent->ID;
            }
            $parents->set( $current_id, $parent_id );
            unset( $query );
        }
        return $parent_id;
    }

    /**
     * Returns a list of modified (children) event objects
     *
     * @param int  $parent_id     ID of parent event
     * @param bool $include_trash Includes trashed when `true` [optional=false]
     *
     * @return array List (might be empty) of Ai1ec_Event objects
     */
    public function get_child_event_objects(
        $parent_id,
        $include_trash = false
    ) {
        $db = $this->_registry->get( 'dbi.dbi' );
        /* @var $db Ai1ec_Dbi */
        $parent_id = (int)$parent_id;
        $sql_query = 'SELECT ID FROM ' . $db->get_table_name( 'posts' ) .
            ' WHERE post_parent = ' . $parent_id;
        $children  = (array)$db->get_col( $sql_query );
        $objects   = array();
        foreach ( $children as $child_id ) {
            try {
                $instance = $this->_registry->get( 'model.event', $child_id );
                if (
                    $include_trash ||
                    'trash' !== $instance->get( 'post' )->post_status
                ) {
                    $objects[$child_id] = $instance;
                }
            } catch ( Ai1ec_Event_Not_Found_Exception $exception ) {
                // ignore
            }
        }
        return $objects;
    }

    /**
     * admin_init_post method
     *
     * Bind to admin_action_editpost action to override default save
     * method when user is editing single instance.
     * New post is created with some fields unset.
     */
    public function admin_init_post( ) {
        if (
            isset( $_POST['ai1ec_instance_id'] ) &&
            isset( $_POST['action'] ) &&
            'editpost' === $_POST['action']
        ) {
            $old_post_id = $_POST['post_ID'];
            $instance_id = $_POST['ai1ec_instance_id'];
            $post_id     = $this->_registry->get( 'model.event.creating' )
                ->create_duplicate_post();
            if ( false !== $post_id ) {
                $this->_handle_instances(
                    $this->_registry->get( 'model.event', $post_id ),
                    $this->_registry->get( 'model.event', $old_post_id ),
                    $instance_id
                );
                $this->_registry->get( 'model.event.instance' )->clean(
                    $old_post_id,
                    $instance_id
                );
                $location = add_query_arg(
                    'message',
                    1,
                    get_edit_post_link( $post_id, 'url' )
                );
                wp_redirect(
                    apply_filters(
                        'redirect_post_location',
                        $location,
                        $post_id
                    )
                );
                exit();
            }
        }
    }

    /**
     * Inject base event edit link for modified instances
     *
     * Modified instances are events, belonging to some parent having recurrence
     * rule, and having some of it's properties altered.
     *
     * @param array    $actions List of defined actions
     * @param stdClass $post Instance being rendered (WP_Post class instance in WP 3.5+)
     *
     * @return array Optionally modified $actions list
     */
    public function post_row_actions( $actions, $post ) {
        if ( $this->_registry->get( 'acl.aco' )->is_our_post_type( $post ) ) {
            $parent_post_id = $this->event_parent( $post->ID );
            if (
                $parent_post_id &&
                NULL !== ( $parent_post = get_post( $parent_post_id ) ) &&
                isset( $parent_post->post_status ) &&
                'trash' !== $parent_post->post_status
            ) {
                $parent_link = get_edit_post_link(
                    $parent_post_id,
                    'display'
                );
                $actions['ai1ec_parent'] = sprintf(
                    '<a href="%s" title="%s">%s</a>',
                    wp_nonce_url( $parent_link ),
                    sprintf(
                        __( 'Edit &#8220;%s&#8221;', AI1EC_PLUGIN_NAME ),
                        apply_filters(
                            'the_title',
                            $parent_post->post_title,
                            $parent_post->ID
                        )
                    ),
                    __( 'Base Event', AI1EC_PLUGIN_NAME )
                );
            }
        }
        return $actions;
    }

    /**
     * add_exception_date method
     *
     * Add exception (date) to event.
     *
     * @param int   $post_id Event edited post ID
     * @param mixed $date    Parseable date representation to exclude
     *
     * @return bool Success
     */
    public function add_exception_date( $post_id, Ai1ec_Date_Time $date ) {
        $event      = $this->_registry->get( 'model.event', $post_id );
        $dates_list = explode( ',', $event->get( 'exception_dates' ) );
        if ( empty( $dates_list[0] ) ) {
            unset( $dates_list[0] );
        }
        $date->set_time( 0, 0, 0 );
        $dates_list[] = $date->format(
            'Ymd\THis\Z'
        );
        $event->set( 'exception_dates', implode( ',', $dates_list ) );
        return $event->save( true );
    }

    /**
     * Handles instances saving and switching if needed. If original event
     * and created event have different start dates proceed in old style
     * otherwise find next instance, switch original start date to next
     * instance start date. If there are no next instances mark event as
     * non recurring. Filter also exception dates if are past.
     *
     * @param Ai1ec_Event $created_event  Created event object.
     * @param Ai1ec_Event $original_event Original event object.
     *
     * @return void Method does not return.
     */
    protected function _handle_instances(
        Ai1ec_Event $created_event,
        Ai1ec_Event $original_event,
        $instance_id
    ) {
        $ce_start = $created_event->get( 'start' );
        $oe_start = $original_event->get( 'start' );
        if (
            $ce_start->format() !== $oe_start->format()
        ) {
            $this->add_exception_date(
                $original_event->get( 'post_id' ),
                $ce_start
            );
            return;
        }
        $next_instance = $this->_find_next_instance(
            $original_event->get( 'post_id' ),
            $instance_id
        );
        if ( ! $next_instance ) {
            $original_event->set( 'recurrence_rules', null );
            $original_event->save( true );
            return;
        }
        $original_event->set(
            'start',
            $this->_registry->get( 'date.time', $next_instance->get( 'start' ) )
        );
        $original_event->set(
            'end',
            $this->_registry->get( 'date.time', $next_instance->get( 'end' ) )
        );
        $edates = $this->_filter_exception_dates( $original_event );
        $original_event->set( 'exception_dates', implode( ',', $edates ) );
        $recurrence_rules = $original_event->get( 'recurrence_rules' );
        $rules_info       = $this->_registry->get( 'recurrence.rule' )
            ->build_recurrence_rules_array( $recurrence_rules );
        if ( isset( $rules_info['COUNT'] ) ) {
            $next_instances_count = $this->_count_next_instances(
                $original_event->get( 'post_id' ),
                $instance_id
            );
            $rules_info['COUNT']  = (int)$next_instances_count + count( $edates );
            $rules                = '';
            if ( $rules_info['COUNT'] <= 1 ) {
                $rules_info = array();
            }
            foreach ( $rules_info as $key => $value ) {
                $rules .= $key . '=' . $value . ';';
            }
            $original_event->set(
                'recurrence_rules',
                $rules
            );
        }
        $original_event->save( true );
    }

    /**
     * Returns next instance.
     *
     * @param int $post_id     Post ID.
     * @param int $instance_id Instance ID.
     *
     * @return null|Ai1ec_Event Result.
     */
    protected function _find_next_instance( $post_id, $instance_id ) {
        $dbi              = $this->_registry->get( 'dbi.dbi' );
        $table_instances  = $dbi->get_table_name( 'ai1ec_event_instances' );
        $table_posts      = $dbi->get_table_name( 'posts' );
        $query            = $dbi->prepare(
            'SELECT i.id FROM ' . $table_instances . ' i JOIN ' .
            $table_posts . ' p ON (p.ID = i.post_id) ' .
            'WHERE i.post_id = %d AND i.id > %d ' .
            'AND p.post_status = \'publish\' ' .
            'ORDER BY id ASC LIMIT 1',
            $post_id,
            $instance_id
        );
        $next_instance_id = $dbi->get_var( $query );
        if ( ! $next_instance_id ) {
            return null;
        }
        return $this->_registry->get( 'model.search' )
            ->get_event( $post_id, $next_instance_id );
    }

    /**
     * Counts future instances.
     *
     * @param int $post_id     Post ID.
     * @param int $instance_id Instance ID.
     *
     * @return int Result.
     */
    protected function _count_next_instances( $post_id, $instance_id ) {
        $dbi             = $this->_registry->get( 'dbi.dbi' );
        $table_instances = $dbi->get_table_name( 'ai1ec_event_instances' );
        $table_posts     = $dbi->get_table_name( 'posts' );
        $query = $dbi->prepare(
            'SELECT COUNT(i.id) FROM ' . $table_instances . ' i JOIN ' .
            $table_posts . ' p ON (p.ID = i.post_id) ' .
            'WHERE i.post_id = %d AND i.id > %d ' .
            'AND p.post_status = \'publish\'',
            $post_id,
            $instance_id
        );
        return (int)$dbi->get_var( $query );
    }

    /**
     * Filters past or out of range exception dates.
     *
     * @param Ai1ec_Event $event Event.
     *
     * @return array Filtered exception dates.
     */
    protected function _filter_exception_dates( Ai1ec_Event $event ) {
        $start           = (int)$event->get( 'start' )->format();
        $exception_dates = explode( ',', $event->get( 'exception_dates' ) );
        $dates           = array();
        foreach ( $exception_dates as $date ) {
            $ex_date = (int)$this->_registry->get( 'date.time', $date )->format();
            if ( $ex_date > $start ) {
                $dates[] = $date;
            }
        }
        return $dates;
    }
}
