<?php
/**
 * Base class for 'view' post type, that means Views and WPAs.
 *
 * Contains code common for both, mostly related to "view query mode", a value determining what kind of object
 * it is.
 *
 * @since 1.8
 *
 * @property-read bool $has_loop_template
 * @property int $loop_template_id
 * @property mixed $loop_included_ct_ids
 * @property string $loop_meta_html
 * @property-read array $loop_settings
 * @property-read array $view_settings
 * @property-read string $query_mode
 * @property-read string $query_type
 */
abstract class WPV_View_Base extends WPV_Post_Object_Wrapper {


    /* ************************************************************************* *\
            Postmeta
    \* ************************************************************************* */


    /**
     * View post type slug.
     */
    const POST_TYPE = 'view';


    const POSTMETA_DESCRIPTION = '_wpv_description';


    const POSTMETA_LOOP_TEMPLATE_ID = '_view_loop_template';


    /**
     * Array with View settings (used also by WPA).
     *
     * For documentation of particular elements see comments at those constants:
     * - VIEW_SETTINGS_CSS
     * - VIEW_SETTINGS_JS
     *
     * Note that this list is not complete and there might be other settings specific
     * to Views or WPAs only.
     *
     * @since 1.10
     */
    const POSTMETA_VIEW_SETTINGS = '_wpv_settings';


    /**
     * Array with loop settings (former layout settings; used also by WPA).
     *
     * For documentation of particular elements see comments at those constants:
     * - LOOP_SETTINGS_META_HTML
     *
     * Note that this list is not complete and there might be other settings specific
     * to Views or WPAs only.
     *
     * @since 1.10
     */
    const POSTMETA_LOOP_SETTINGS = '_wpv_layout_settings';


    /**
     * Default postmeta values common for Views and WPAs.
     *
     * Note that this should contain all postmeta keys they can have, but it doesn't (yet).
     *
     * @todo Add missing default values.
     * @todo Add description to default values.
     * @var array
     */
    protected static $postmeta_defaults = array(
        WPV_View_Base::POSTMETA_DESCRIPTION => '',
        WPV_View_Base::POSTMETA_VIEW_SETTINGS => array(
            WPV_View_Base::VIEW_SETTINGS_CSS => '',
            WPV_View_Base::VIEW_SETTINGS_JS => '',
            WPV_View_Base::VIEW_SETTINGS_QUERY_MODE => 'normal'
        ),
        WPV_View_Base::POSTMETA_LOOP_SETTINGS => array( // todo incomplete
            WPV_View_Base::LOOP_SETTINGS_META_HTML => '', // todo this is not a valid default value
            WPV_View_Base::LOOP_SETTINGS_INCLUDED_CT_IDS => ''
        )
    );


    /**
     * Get default postmeta common for the View and WPA.
     * @return array
     */
    protected function get_postmeta_defaults() {
        return WPV_View_Base::$postmeta_defaults;
    }



    /* ************************************************************************* *\
            Constants and static methods
    \* ************************************************************************* */


    /**
     * Determine whether View/WPA with given ID exists.
     *
     * @param int $view_id ID of the View to check.
     *
     * @return bool True if post with given ID exists and if it's a View.
     */
    public static function is_valid( $view_id ) {
        /* Note: This should not cause a redundant database query. Post objects are cached by WP core, so this one was
         * either already loaded or it has to be loaded now and will be reused in the future. */
        return WPV_View_Base::is_wppost_view( WP_Post::get_instance( $view_id ) );
    }


    /**
     * For a given object, determine if it's a valid WP_Post object representing a View/WPA.
     *
     * @param mixed $post Value to check.
     *
     * @return bool True if $post is a valid WP_Post object representing a View/WPA, false otherwise.
     */
    public static function is_wppost_view( $post ) {
        return ( ( $post instanceof WP_Post ) && ( $post->ID > 0 ) && ( WPV_View_Base::POST_TYPE == $post->post_type ) );
    }


    /**
     * Determine if the object is used as a WordPress Archive.
     *
     * We cannot rely only on the value of "view query mode" stored in postmeta, because some filters need to be
     * applied along the way. Current implementation causes a get_post_meta() call.
     *
     * @todo can this be done better, without another query or filters?
     *
     * @param int $view_id ID of the object ('view' post type).
     *
     * @return bool True if it is a WPA, false otherwise.
     */
    public static function is_archive_view( $view_id ) {
        global $WP_Views;
        return $WP_Views->is_archive_view( $view_id );
    }


    /**
     * Create an appropriate wrapper for View or WPA post object.
     *
     * Decides by self::is_archive_view() if it's a WPA. Then it checks whether the full version of the wrapper exist,
     * and instantiates it or falls back to the embedded version.
     *
     * @param int|WP_Post $view Post ID or post object.
     *
     * @return null|WPV_View_Embedded|WPV_WordPress_Archive_Embedded|WPV_View|WPV_WordPress_Archive The appropriate wrapper or null on error.
     */
    public static function get_instance( $view ) {
        // http://stackoverflow.com/questions/2559923/shortest-way-to-check-if-a-variable-contains-positive-integer-using-php
        if( (int)$view == $view && (int)$view > 0 ) {
            $post = WP_Post::get_instance( $view );
        } else {
            $post = $view;
        }

        if( ! WPV_View_Base::is_wppost_view( $post ) ) {
            return null;
        }

        try {
            if ( WPV_View_Base::is_archive_view( $post->ID ) ) {
                if( class_exists( 'WPV_WordPress_Archive' ) ) {
                    return new WPV_WordPress_Archive( $post );
                } else {
                    return new WPV_WordPress_Archive_Embedded( $post );
                }
            } else {
                if( class_exists( 'WPV_View' ) ) {
                    return new WPV_View( $post );
                } else {
                    return new WPV_View_Embedded( $post );
                }
            }
        } catch( Exception $ex ) {
            return null;
        }
    }


    /**
     * Determine whether given View name is already used as a post slug or post title.
     *
     * @param string $name View name to check.
     *
     * @param int $except_id The View ID to exclude from checking.
     * @param array &$collision_data (since 1.10) If there is a name collision, this will be set to an array:
     *     - id: ID of the other post
     *     - colliding_field: Where has the collision with $name happened: 'post_title', 'post_name' or 'both'
     *     - post_title: Title of the other post
     *
     * @return bool True if name is already used, false otherwise.
     *
     * @since 1.9
     */
    public static function is_name_used( $name, $except_id = 0, &$collision_data = null ) {
        return WPV_Post_Object_Wrapper::is_name_used_base( $name, WPV_View_Base::POST_TYPE, $except_id, $collision_data );
    }


    /**
     * Generate an unique title for a View/WPA based on a candidate value.
     *
     * @param string $title_candidate Non-blank (e.g. not only whitespace) title candidate.
     * @param int $except_id View/WPA id that should be excluded from the uniqueness check.
     * @return null|string An unique title or null if the input was invalid.
     *
     * @since 1.9
     */
    public static function get_unique_title( $title_candidate, $except_id = 0 ) {
        return WPV_Post_Object_Wrapper::get_unique_title_base( WPV_View_Base::POST_TYPE, $title_candidate, $except_id );
    }


    /**
     * Create new post of the View type.
     *
     * Used for the create() methods for Views and WPAs.
     *
     * @param string $title New post title. Must be unique.
     * @return int ID of the new post.
     * @throws WPV_RuntimeExceptionWithMessage
     * @throws RuntimeException
     * @since 1.10
     */
    protected static function create_post( $title ) {

        // Ensure unique non-empty title
        if( empty( $title ) ) {
            $title = __( 'Unnamed View', 'wpv-views' );
        }

        WPV_View_Base::validate_title( $title );
		
		$proposed_name = sanitize_text_field( sanitize_title( $title ) );
		
		if ( empty( $proposed_name ) ) {
			$proposed_name = WPV_View_Base::POST_TYPE . '-rand-' . uniqid();
		}

        // Create the post
        $post_data = array(
            'post_type'	=> WPV_View_Base::POST_TYPE,
            'post_title' => $title,
			'post_name' => $proposed_name,
            'post_status' => 'publish',
            'post_content' => "[wpv-filter-meta-html]\n[wpv-layout-meta-html]"
        );
        $post_id = wp_insert_post( $post_data );
        if( 0 == $post_id ) {
            throw new RuntimeException( 'cannot wp_insert_post' );
        }

        return $post_id;
    }


    /**
     * Validate new View/WPA title.
     *
     * Throws an exception with an user-friendly message if the title value is not valid:
     * - contains invalid characters
     * - is not unique among existing View/WPA titles and slugs.
     *
     * @param string $value New title.
     * @param int $view_id ID of a View/WPA that should be skipped during checking uniqueness (use this when
     *     changing title of existing View/WPA).
     * @return string Sanitized value that can be used safely.
     * @throws WPV_RuntimeExceptionWithMessage
     * @since 1.10
     */
    protected static function validate_title( $value, $view_id = 0 ) {

        $sanitized_value = sanitize_text_field( $value );

        // Check if the original value contains something that shouldn't be there.
        // We tolerate whitespace at the beginning and end, ergo the trim (but we will
        // work with the trimmed value from now on).
        if( trim( $value ) != $sanitized_value ) {
            throw new WPV_RuntimeExceptionWithMessage(
                '_validate_title failed: invalid characters',
                __( 'The title can not contain any tabs, line breaks or HTML code.', 'wpv-views' )
            );
        }

        if( empty( $sanitized_value ) ) {
            throw new WPV_RuntimeExceptionWithMessage(
                '_validate_title failed: empty value',
                __( 'You can not leave the title empty.', 'wpv-views' )
            );
        }

        $collision_data = array();
        if( WPV_View_Base::is_name_used( $sanitized_value, $view_id, $collision_data ) ) {
	        $view_query_mode = WPV_View_Base::is_archive_view( $collision_data['id'] ) ? __( 'WordPress Archive', 'wpv-views' ) : __( 'View', 'wpv-views' );
            switch( $collision_data['colliding_field'] ) {
                case 'post_name':
                    $exception_message = sprintf(
                        __( 'Another %1$s (%2$s) already uses this slug. Please use another name.', 'wpv-views' ),
	                    $view_query_mode,
                        sanitize_text_field( $collision_data['post_title'] )
                    );
                    break;
                case 'post_title':
                    $exception_message = sprintf( __( 'Another %1$s already uses this name. Please use another name.', 'wpv-views' ), $view_query_mode );
                    break;
                case 'both':
                    $exception_message = sprintf( __( 'Another %1$s already uses this name and slug. Please use another name.', 'wpv-views' ), $view_query_mode );
                    break;
                default:
                    // Should never happen
                    $exception_message = sprintf( __( 'Another %1$s already uses this name and slug. Please use another name.', 'wpv-views' ), $view_query_mode );
                    break;
            }
            //$exception_message = print_r( $collision_data, true );
            throw new WPV_RuntimeExceptionWithMessage(
                '_validate_title failed: name is already being used for another CT',
                $exception_message,
                WPV_RuntimeExceptionWithMessage::EXCEPTION_VALUE_ALREADY_USED
            );
        }

        return $sanitized_value;
    }



    /* ************************************************************************* *\
            Methods
    \* ************************************************************************* */


    /**
     * Class constructor. Create an instance from View ID or WP_Post object representing a View.
     *
     * Please note that WP_Post object will be validated and an exception is thrown on error.
     * However, if only an ID is provided, no such validation takes place here (in order to avoid potentionally
     * unnecessary database query). So, the ID must be validated before (by WPV_View_Base::is_valid() or by other
     * means), otherwise the behaviour of this object is undefined. Also note that "view query mode" is not checked
     * here. If you are not certain about it's value, use self::create().
     *
     * @param int|WP_Post $view View ID or a WP_Post object.
     *
     * @throws InvalidArgumentException when provided argument is not a WP_Post instance representing a View or an
     * integer that *might* be a View ID.
     */
    public function __construct( $view ) {
        if( $view instanceof WP_Post ) {
            // Let's check that we indeed have a valid post and View post type
            if( WPV_View_Base::is_wppost_view( $view ) ) {
                // Store the data we got;
                $this->object_id = $view->ID;
                $this->post = clone $view;
            } else {
                throw new InvalidArgumentException( "Invalid WP_Post object provided (not a View): " . print_r( $view, true ) );
            }
        } elseif( is_numeric( $view ) && $view > 0 ) {
            // We assume (!) this is a valid View ID.
            $this->object_id = $view;
        } else {
            throw new InvalidArgumentException( "Invalid argument provided (not a View or ID): " . print_r( $view, true ) );
        }
    }


    /**
     * Get the post object representing this View.
     *
     * @return WP_Post Post object.
     *
     * @throws InvalidArgumentException if the post object cannot be retrieved or is invalid.
     */
    protected function &post() {

        if( null == $this->post ) {
            // Requesting WP_Post object, but we haven't got it yet.
            $post = WP_Post::get_instance( $this->object_id );
            if( WPV_View_Base::is_wppost_view( $post ) ) {
                $this->post = $post;
            } else {
                throw new InvalidArgumentException( 'Invalid View ID' );
            }
        }

        return $this->post;
    }


    /**
     * @var null|array Cache for View settings.
     */
    protected $views_settings_cache = null;


    /**
     * Obtain View settings. Optional caching.
     *
     * The proper way to obtain View settings is through $WP_Views->get_view_settings(), which applies some filters
     * on it. We may not need to apply them more than once.
     *
     * @param bool $use_cached If true, prefer cached version. Otherwise no caching.
     *
     * @todo review
     *
     * @return array View settings.
     */
    protected function get_view_settings( $use_cached = false ) {
        if( !$use_cached || ( null == $this->views_settings_cache ) ) {
            global $WP_Views;
            $this->views_settings_cache = $WP_Views->get_view_settings( $this->object_id );
        }

        return $this->views_settings_cache;
    }


	/**
	 * Determine if this is a View and not a WPA.
	 * @return bool
	 * @since 1.12
	 */
    public function is_a_view() {
        return false;
    }


	/**
	 * Determine if this is a WPA and not a View
	 * @return bool
	 * @since 1.12
	 */
    public function is_a_wordpress_archive() {
        return false;
    }


    /* ************************************************************************* *\
            Custom getters and setters and validators
    \* ************************************************************************* */


    /**
     * Set View description.
     *
     * @param string $value New description. It will be sanitized before saving.
     *
     * @since 1.10
     */
    protected function _set_description( $value ) {
        $sanitized_value = sanitize_text_field( $value );
        $this->update_postmeta( WPV_View_Base::POSTMETA_DESCRIPTION, $sanitized_value );
    }


    /**
     * View description.
     *
     * @return string
     */
    protected function _get_description() {
        return esc_html( $this->get_postmeta( WPV_View_Base::POSTMETA_DESCRIPTION ) );
    }


    /**
     * Get cached(!) version of View settings array.
     *
     * Please use this only when you are sure you will not break anything by caching.
     *
     * @deprecated Deprecated in favor of _get_view_settings().
     *
     * @return array View settings.
     */
    protected function _get_settings() {
        return $this->get_view_settings( true );
    }



    /**
     * @return string Label for the object depending on "view query mode". Empty string when it's invalid.
     */
    protected function _get_query_mode_display_name() {
        switch( $this->query_mode ) {
            case 'normal':
                return __( 'View', 'wpv-views' );
            case 'archive':
            case 'layouts-loop':
                return __( 'WordPress Archive', 'wpv-views' );
            default:
                // should never happen
                return '';
        }
    }


    protected function _validate_title( $value ) {

        return WPV_View_Base::validate_title( $value, $this->id );

    }


    /**
     * Post title setter.
     *
     * See _validate_title().
     *
     * @param string $value New post title.
     * @throws Exception, WPV_RuntimeExceptionWithMessage
     * @since 1.9
     */
    protected function _set_title( $value ) {

        $value = $this->_validate_title( $value );

        $result = $this->update_post( array( 'post_title' => $value ) );

        if( $result instanceof WP_Error ) {
            throw new Exception( '_set_title failed: WP_Error' );
        }
    }


    /**
     * Post slug validation.
     *
     * Accepts a non-empty value containing only lowercase letters, numbers or dashes.
     *
     * @param string $value New post slug.
     * @return string Sanitized value safe to be used.
     * @throws WPV_RuntimeExceptionWithMessage
     * @since 1.9
     */
    protected function _validate_slug( $value ) {

        $sanitized_value = sanitize_title( $value );
        if( $value != $sanitized_value ) {
            throw new WPV_RuntimeExceptionWithMessage(
                '_validate_slug failed: invalid characters',
                __( 'The slug can only contain lowercase latin letters, numbers or dashes.', 'wpv-views' )
            );
        }

        if( empty( $sanitized_value ) ) {
            throw new WPV_RuntimeExceptionWithMessage(
                '_validate_slug failed: empty value',
                __( 'You can not leave the slug empty.', 'wpv-views' )
            );
        }

        $collision_data = array();
        if( WPV_View_Base::is_name_used( $sanitized_value, $this->id, $collision_data ) ) {
            switch( $collision_data['colliding_field'] ) {
                case 'post_name':
                    $exception_message = sprintf(
                        __( 'Another item (%s) with that slug already exists. Please use another slug.', 'wpv-views' ),
                        sanitize_text_field( $collision_data['post_title'] )
                    );
                    break;
                case 'post_title':
                    $exception_message = __( 'Another item already uses this slug value as its title. Please use another slug.', 'wpv-views' );
                    break;
                case 'both':
                    $exception_message = __( 'Another item already uses this slug value as its slug and title. Please use another slug.', 'wpv-views' );
                    break;
                default:
                    $exception_message = __( 'Another item with that slug or title already exists. Please use another slug.', 'wpv-views' );
                    break;
            }
            throw new WPV_RuntimeExceptionWithMessage(
                '_validate_slug failed: name is already being used for another View/WPA',
                $exception_message,
                WPV_RuntimeExceptionWithMessage::EXCEPTION_VALUE_ALREADY_USED
            );
        }

        return $sanitized_value;
    }


    /**
     * Post slug (a.k.a. post_name) setter.
     *
     * See _validate_slug().
     *
     * @param string $value New post slug.
     * @throws Exception, WPV_RuntimeExceptionWithMessage
     * @since 1.9
     */
    protected function _set_slug( $value ) {

        $sanitized_value = $this->_validate_slug( $value );

        $result = $this->update_post( array( 'post_name' => $sanitized_value ) );

        if( $result instanceof WP_Error ) {
            throw new Exception( '_set_title failed: WP_Error' );
        }
    }


    /* ************************************************************************* *\
        Loop templates
    \* ************************************************************************* */


    /**
     * @return bool True if this View/WPA uses a CT as a Loop Template.
     */
    protected function _get_has_loop_template() {
        return ( $this->loop_template_id > 0 );
    }


    /**
     * @return int ID of the CT used as a Loop Template or zero if no such CT exists.
     */
    protected function _get_loop_template_id() {
        return (int) $this->get_postmeta( WPV_View_Base::POSTMETA_LOOP_TEMPLATE_ID );
    }


    protected function _set_loop_template_id( $value ) {
        $this->update_postmeta( WPV_View_Base::POSTMETA_LOOP_TEMPLATE_ID, (int) $value );
    }


    /**
     * Delete a CT used as a loop template.
     *
     * Deletes the Content Template and removes references to it from
     * loop_template_id and loop_included_ct_ids properties.
     *
     * @param int $ct_id Content Template ID.
     * @return bool True if the operation was successful, false otherwise.
     * @since 1.10
     */
    public function delete_unused_loop_template( $ct_id ) {
        $ct_id = (int) $ct_id;
        if( $ct_id < 1 ) {
            return false;
        }

        wp_delete_post( $ct_id, true );
        $this->loop_template_id = 0;

        $included_ct_ids = $this->loop_included_ct_ids;
        $reg_templates = explode( ',', $included_ct_ids );
        if ( in_array( $ct_id, $reg_templates ) ) {
            $delete_key = array_search( $ct_id, $reg_templates );
            unset( $reg_templates[$delete_key] );
            $this->loop_included_ct_ids = implode( ',', $reg_templates );
        }

        return true;
    }


    /**
     * Create new Content Template and set it as this View's Loop template.
     *
     * Note that this method doesn't care about existing Loop template. That should be handled separately before it
     * is called.
     *
     * @param string $title Valid title for the Content Template (will be adjusted if not unique).
     * @param string $content Content of the CT
     * @throws RuntimeException
     * @return WPV_Content_Template Newly created CT.
     * @since 1.10
     */
    public function create_loop_template( $title, $content = '[wpv-post-link]' ) {

        $ct = WPV_Content_Template::create( $title, true );
        if( ! $ct instanceof WPV_Content_Template ) {
            throw new RuntimeException( 'couldn\'t create the loop template' );
        }

        $ct->defer_after_update_actions();

        // Update Loop and content of the Loop template
        $ct->content = $content;

        $this->loop_meta_html = str_replace(
            '<wpv-loop>',
            sprintf( "<wpv-loop>\n\t\t\t[wpv-post-body view_template=\"%s\"]", $ct->title ),
            $this->loop_meta_html
        );

        // Create bindings between View and Loop template
        $this->loop_included_ct_ids = $ct->id;

        $this->loop_template_id = $ct->id;
        $ct->loop_output_id = $this->id;

        $ct->resume_after_update_actions();

        return $ct;
    }



    /* ************************************************************************* *\
            View settings
    \* ************************************************************************* */

    /* For the sake of brevity we're referring to POSTMETA_VIEW_SETTINGS as "View settings"
     * although they are used by both Views and WPAs.
     *
     * Individual settings may differ for both types of objects. Here are defined only the
     * common ones.
     *
     * Also note there's a mechanism for avoiding redundant database updates if more settings
     * are changed in a row. The intended usage is following:
     * - Call $this->begin_modifying_view_settings().
     * - Then make all desired changes of View settings.
     * - Call $this->finish_modifying_view_settings().
     *
     * For this to work, setters and getters for the settings should use get_view_setting()
     * and set_view_setting().
     */


    /**
     * View settings key for additional CSS code for the loop.
     *
     * @since 1.10
     */
    const VIEW_SETTINGS_CSS = 'layout_meta_html_css';


    /**
     * View settings key for additional JS code for the loop.
     *
     * @since 1.10
     */
    const VIEW_SETTINGS_JS = 'layout_meta_html_js';


    /**
     * Obsolete setting that will be removed if present.
     *
     * @since 1.10
     */
    const VIEW_SETTINGS_FILTER_STATE = 'filter_meta_html_state';


    /**
     * "View query mode" setting indicating whether this is a View or a WPA.
     *
     * Allowed values are:
     * - normal
     * - archive
     * - layouts-loop
     */
    const VIEW_SETTINGS_QUERY_MODE = 'view-query-mode';


	/**
	 * Type of content that is being queried.
	 *
	 * Allowed values are:
	 * - posts
	 * - taxonomy
	 * - users
	 *
	 * Values are stored as a first element of an array (for historical reasons, I assume).
	 * For WPAs, only 'posts' is valid.
	 */
    const VIEW_SETTINGS_QUERY_TYPE = 'query_type';



    /**
     * Defines whether View settings are in the process of being modified.
     *
     * If true, no setting will be saved into database until finish_modifying_view_settings() is called.
     *
     * @var bool
     * @since 1.10
     */
    protected $are_view_settings_being_modified = false;


    /**
     * Indicates that when finish_modifying_view_settings() is called, View settings should be updated.
     *
     * @var bool
     * @since 1.10
     */
    protected $is_view_settings_update_needed = false;


    /**
     * Cache for View settings fetched from the database and eventually modified. Null if not initialized.
     *
     * @var null|array
     * @since 1.10
     */
    protected $view_settings_cache = null;


    /**
     * Get View settings.
     *
     * Uses cached settings if available. For the description of View settings refer to VIEW_SETTING_* constants
     * that define individual settings.
     *
     * @return array
     * @since 1.10
     */
    protected function _get_view_settings() {
        if( null == $this->view_settings_cache ) {
            $this->view_settings_cache = $this->get_postmeta( WPV_View_Base::POSTMETA_VIEW_SETTINGS );
        }
        return $this->view_settings_cache;
    }


    /**
     * Indicate that multiple View settings are going to be changed in order to prevent redundant database queries.
     *
     * After calling this method, no setting will be saved into database until finish_modifying_view_settings() is called.
     *
     * @since 1.10
     */
    public function begin_modifying_view_settings() {
        $this->are_view_settings_being_modified = true;
    }


    /**
     * Indicate that the changes of View settings has ended.
     *
     * If database update is needed, it will be performed now.
     *
     * @since 1.10
     */
    public function finish_modifying_view_settings() {
        if( $this->are_view_settings_being_modified ) {
            $this->are_view_settings_being_modified = false;
            if( $this->is_view_settings_update_needed ) {
                $this->update_view_settings();
            }
        }
    }
	
	
	/**
	 * Add or update several View settings at once, like when saving a query filter.
	 *
	 * @param array $settings Settings as key->value pairs.
	 *
	 * @since m2m
	 */
	public function set_view_settings( $settings ) {
		$this->view_settings;
		foreach ( $settings as $setting_key => $setting_value ) {
			if ( $setting_value === toolset_getarr( $this->view_settings_cache, $setting_key ) ) {
				continue;
			}
			$this->set_view_setting( $setting_key, $setting_value );
		}
	}
	
	
	/**
	 * Remove one or several View settings at once, like when deleting a query filter.
	 *
	 * @param string|array $setting_keys Setting keys.
	 *
	 * @since m2m
	 */
	public function delete_view_settings( $setting_keys ) {
		if ( ! is_array( $setting_keys ) ) {
			$setting_keys = array( $setting_keys );
		}
		foreach ( $setting_keys as $setting_key ) {
			$this->delete_view_setting( $setting_key );
		}
	}


    /**
     * Indicate that one or more settings have been changed and need updating.
     *
     * If begin_modifying_view_settings() was called before, this will do nothing but indicate View settings
     * need to be updated. Otherwise the update will be executed immediately.
     *
     * @since 1.10
     */
    protected function view_settings_update_needed() {
        if( $this->are_view_settings_being_modified ) {
            $this->is_view_settings_update_needed = true;
        } else {
            $this->update_view_settings();
        }
    }


    /**
     * Get current value for an individual View setting.
     *
     * Read the value from View settings cache, use value from postmeta defaults or an empty string
     * if neither are defined.
     *
     * @param string $setting_key Setting key.
     * @return mixed Setting value.
     * @since 1.10
     */
    protected function get_view_setting( $setting_key ) {
        $view_settings = $this->view_settings;
        $default = $this->get_postmeta_defaults();
        $default = wpv_getarr( $default[ WPV_View_Base::POSTMETA_VIEW_SETTINGS ], $setting_key, '' );
        return wpv_getarr( $view_settings, $setting_key, $default );
    }


    /**
     * Update an individual View setting.
     *
     * Store it's value in cache and indicate that update is needed.
     *
     * @param string $setting_key Setting key.
     * @param string $value Setting value.
     * @since 1.10
     */
    protected function set_view_setting( $setting_key, $value ) {
        $this->view_settings;
        $this->view_settings_cache[ $setting_key ] = $value;
        $this->view_settings_update_needed();
    }
	
	/**
     * Delete an individual View setting, and indicate that update is needed.
     *
     * @param string $setting_key Setting key.
	 *
     * @since m2m
     */
    protected function delete_view_setting( $setting_key ) {
        $this->view_settings;
		if ( isset( $this->view_settings_cache[ $setting_key ] ) ) {
			unset( $this->view_settings_cache[ $setting_key ] );
		}
        $this->view_settings_update_needed();
    }


    /**
     * Update View settings in database.
     *
     * Resets the $is_view_settings_update_needed flag.
     *
     * @since 1.10
     */
    protected function update_view_settings() {
        $this->is_view_settings_update_needed = false;
        if( null != $this->view_settings_cache ) {

            // Remove deprecated setting
            if( isset( $this->view_settings_cache[ WPV_View_Base::VIEW_SETTINGS_FILTER_STATE ] ) ) {
                unset( $this->view_settings_cache[ WPV_View_Base::VIEW_SETTINGS_FILTER_STATE ] );
            }

            $this->update_postmeta( WPV_View_Base::POSTMETA_VIEW_SETTINGS, $this->view_settings_cache );
        }
    }


    /**
     * Get extra CSS code for the Loop section.
     *
     * This is a View setting.
     *
     * @return string
     * @since 1.10
     */
    protected function _get_css() {
        return $this->get_view_setting( WPV_View_Base::VIEW_SETTINGS_CSS );
    }


    protected function _set_css( $value ) {
        $this->set_view_setting( WPV_View_Base::VIEW_SETTINGS_CSS, $value );
    }


    /**
     * Get extra JS code for the Loop section.
     *
     * This is a View setting.
     *
     * @return string
     * @since 1.10
     */
    protected function _get_js() {
        return $this->get_view_setting( WPV_View_Base::VIEW_SETTINGS_JS );
    }


    protected function _set_js( $value ) {
        $this->set_view_setting( WPV_View_Base::VIEW_SETTINGS_JS, $value );
    }


    /**
     * Get "query mode", a value determining what kind of object this is.
     *
     * Allowed values are 'normal', 'archive' and 'layouts-loop'. Any other value will default to 'normal'.
     *
     * @return string
     * @since 1.8
     */
    protected function _get_query_mode() {
        $query_mode = $this->get_view_setting( WPV_View_Base::VIEW_SETTINGS_QUERY_MODE );
        if( !in_array( $query_mode, array( 'normal', 'archive', 'layouts-loop' ) ) ) {
            $query_mode = 'normal';
        }
        return $query_mode;
    }


	/**
	 * Type of content that is being queried.
	 *
	 * @return string Defaults to 'posts', see WPV_View_Base::VIEW_SETTINGS_QUERY_TYPE.
	 * @since 1.12
	 */
	protected function _get_query_type() {
		return 'posts';
	}


    /* ************************************************************************* *\
        Loop
    \* ************************************************************************* */

    /* Individual settings may differ for Views and WPAs. Here are defined only the
     * common ones.
     *
     * Also note there's a mechanism for avoiding redundant database updates if more settings
     * are changed in a row. The intended usage is following:
     * - Call $this->begin_modifying_loop_settings().
     * - Then make all desired changes of loop settings.
     * - Call $this->finish_modifying_loop_settings().
     *
     * For this to work, setters and getters for the settings should use get_loop_setting() and set_loop_setting().
     */


    /**
     * Loop settings key for the actual Loop.
     *
     * This setting contains the "meta html" code.
     *
     * @since 1.10
     */
    const LOOP_SETTINGS_META_HTML = 'layout_meta_html';

    /*
     * Loop settings (that need to be documented).
     */
    const LOOP_SETTINGS_STYLE = 'style';
    const LOOP_SETTINGS_TABLE_COLUMN_COUNT = 'table_cols';
    const LOOP_SETTINGS_BS_COLUMN_COUNT = 'bootstrap_grid_cols';
    const LOOP_SETTINGS_BS_GRID_CONTAINER = 'bootstrap_grid_container';
    const LOOP_SETTINGS_BS_ROW_CLASS = 'bootstrap_grid_row_class';
    const LOOP_SETTINGS_BS_INDIVIDUAL = 'bootstrap_grid_individual';
    const LOOP_SETTINGS_INCLUDE_FIELD_NAMES = 'include_field_names';
    const LOOP_SETTINGS_FIELDS = 'fields';
    const LOOP_SETTINGS_REAL_FIELDS = 'real_fields';
    const LOOP_SETTINGS_LIST_SEPARATOR = 'list_separator';


    /**
     * This loop setting contains IDs of Content Templates included in the Loop
     * as a comma-separated string (without spaces).
     *
     * @since 1.10
     */
    const LOOP_SETTINGS_INCLUDED_CT_IDS = 'included_ct_ids';


    /**
     * Defines whether loop settings are in the process of being modified.
     *
     * If true, no setting will be saved into database until finish_modifying_loop_settings() is called.
     *
     * @var bool
     * @since 1.10
     */
    protected $are_loop_settings_being_modified = false;


    /**
     * Indicates that when finish_modifying_loop_settings() is called, loop settings should be updated.
     *
     * @var bool
     * @since 1.10
     */
    protected $is_loop_settings_update_needed = false;


    /**
     * Cache for loop settings fetched from the database and eventually modified. Null if not initialized.
     *
     * @var null|array
     * @since 1.10
     */
    protected $loop_settings_cache = null;


    /**
     * Indicate that multiple loop settings are going to be changed in order to prevent redundant database queries.
     *
     * After calling this method, no setting will be saved into database until finish_modifying_loop_settings() is called.
     *
     * @since 1.10
     */
    public function begin_modifying_loop_settings() {
        $this->are_loop_settings_being_modified = true;
    }


    /**
     * Indicate that the changes of loop settings has ended.
     *
     * If database update is needed, it will be performed now.
     *
     * @since 1.10
     */
    public function finish_modifying_loop_settings() {
        if( $this->are_loop_settings_being_modified ) {
            $this->are_loop_settings_being_modified = false;
            if( $this->is_loop_settings_update_needed ) {
                $this->update_loop_settings();
            }
        }
    }


    /**
     * Indicate that one or more loop settings have been changed and need updating.
     *
     * If begin_modifying_loop_settings() was called before, this will do nothing but indicate loop settings
     * need to be updated. Otherwise the update will be executed immediately.
     *
     * @since 1.10
     */
    protected function loop_settings_update_needed() {
        if( $this->are_loop_settings_being_modified ) {
            $this->is_loop_settings_update_needed = true;
        } else {
            $this->update_loop_settings();
        }
    }


    /**
     * Update loop settings in database.
     *
     * Resets the $is_loop_settings_update_needed flag.
     *
     * @since 1.10
     */
    protected function update_loop_settings() {
        $this->is_loop_settings_update_needed = false;
        if( null != $this->loop_settings_cache ) {
            $this->update_postmeta( WPV_View_Embedded::POSTMETA_LOOP_SETTINGS, $this->loop_settings_cache );
        }
    }


    /**
     * Get current value for an individual loop setting.
     *
     * Read the value from loop settings cache, use value from postmeta defaults or an empty string
     * if neither are defined.
     *
     * @param string $setting_key Setting key.
     * @return mixed Setting value.
     * @since 1.10
     */
    protected function get_loop_setting( $setting_key ) {
        $loop_settings = $this->loop_settings;
        $default = $this->get_postmeta_defaults();
        $default = wpv_getarr( $default[ WPV_View_Base::POSTMETA_LOOP_SETTINGS ], $setting_key, '' );
        return wpv_getarr( $loop_settings, $setting_key, $default );
    }


    /**
     * Update an individual loop setting.
     *
     * Store it's value in cache and indicate that update is needed.
     *
     * @param string $setting_key Setting key.
     * @param string $value Setting value.
     * @since 1.10
     */
    protected function set_loop_setting( $setting_key, $value ) {
        $this->loop_settings;
        $this->loop_settings_cache[ $setting_key ] = $value;
        $this->loop_settings_update_needed();
    }


    /**
     * Get array of loop settings.
     *
     * Uses cached settings if available. For the description of loop settings refer to LOOP_SETTING_* constants
     * that define individual settings.
     *
     * @return array
     * @since 1.10
     */
    protected function _get_loop_settings() {
        if( null == $this->loop_settings_cache ) {
            $this->loop_settings_cache = $this->get_postmeta( WPV_View_Embedded::POSTMETA_LOOP_SETTINGS );
        }
        return $this->loop_settings_cache;
    }


    /**
     * Get the Loop itself.
     *
     * A.k.a. "loop meta HTML".
     *
     * This is a loop setting.
     *
     * @return string
     * @since 1.10
     */
    protected function _get_loop_meta_html() {
        return $this->get_loop_setting( WPV_View_Base::LOOP_SETTINGS_META_HTML );
    }


    /**
     * Validate "loop meta html" (content of the Loop editor) before saving it to database.
     *
     * Perform syntax check to ensure mandatory elements are all present exactly once and in the right order.
     * If that's not the case, throw an exception containing a message - this time very user-friendly one,
     * with thorough description of what's wrong and with minimal demo content.
     *
     * @param string $value The value to be sanitized. It *must* have added slashes (especially before quotes), otherwise
     *     the validation has undefined result.
     * @return string The same value if validation has passed.
     * @throws WPV_RuntimeExceptionWithMessage if validation fails.
     * @since 1.10
     */
    protected function _validate_loop_meta_html( $value ) {

        // List of separate elements to match, each with a match pattern and label and indent level for display purposes.
        $elements = array(
            array( 'label' => '[wpv-layout-start]', 'pattern' => "\\[wpv-layout-start\\]", 'indent' => 0 ),
            array( 'label' => '[wpv-items-found]', 'pattern' => "\\[wpv-items-found\\]", 'indent' => 1 ),
            array( 'label' => esc_html( '<!-- wpv-loop-start -->' ), 'pattern' => "<!--\\ wpv-loop-start\\ -->", 'indent' => 1 ),
            array( 'label' => esc_html( '<wpv-loop>' ), 'pattern' => "\\<wpv-loop(\\s+[a-z]+\\=\\\\\\\"[a-z0-9]*\\\\\\\")*\\s*\\>", 'indent' => 1 ),
            array( 'label' => esc_html( '</wpv-loop>' ), 'pattern' => "<\\/wpv-loop>", 'indent' => 1 ),
            array( 'label' => esc_html( '<!-- wpv-loop-end -->' ), 'pattern' => "<!--\\ wpv-loop-end\\ -->", 'indent' => 1 ),
            array( 'label' => '[/wpv-items-found]', 'pattern' => "\\[\\/wpv-items-found\\]", 'indent' => 1 ),
            array( 'label' => '[wpv-layout-end]', 'pattern' => "\\[wpv-layout-end\\]", 'indent' => 0 )
        );

        $this->validate_meta_html_content( $value, __( 'Loop', 'wpv-views' ), $elements );

        return $value;
    }


    /**
     * Update loop meta html.
     *
     * This is a loop setting, so the actual update may be deferred.
     * See WPV_View_Embedded::begin_modifying_loop_settings() for details.
     *
     * @param string $value The value to be sanitized. It *must* have added slashes (especially before quotes), otherwise
     *     the validation has undefined result.
     * @throws WPV_RuntimeExceptionWithMessage if validation fails.
     * @since 1.10
     */
    protected function _set_loop_meta_html( $value ) {

        // Validate or throw.
        $value = $this->_validate_loop_meta_html( $value );

        $this->set_loop_setting( WPV_View_Base::LOOP_SETTINGS_META_HTML, $value );

		/**
		 * Fires once the value for the Loop editor for a View has been updated,
		 * but before it has been saved.
		 *
		 * @since 2.3.0
		 *
		 * @param string 	$value		Value of the Filter editor.
		 * @param int		$this->id	View ID.
		 *
		 * @note This replaces one call used here before: 'wpv_register_wpml_strings'
		 */
		do_action( 'wpv_action_wpv_after_set_loop_meta_html', $value, $this->id );
    }


    /**
     * Validate generic meta HTML content.
     *
     * Based on given match patterns, perform syntax check to ensure mandatory elements are all present exactly once
     * and in the right order. If that's not the case, throw an exception containing a message - this time very
     * user-friendly one, with thorough description of what's wrong and with minimal demo content.
     *
     * @param string $content The value to be sanitized. It *must* have added slashes (especially before quotes),
     *     otherwise the validation has undefined result.
     * @param string $field_name Display name of the field (e.g. "Loop") that will be used
     *     in generated error messages.
     * @param array $elements (
     *         Definition of syntax elements that must be present exactly once in the content. Order of those elements
     *         in this array defines the required order of elements and also how error messages will be generated.
     *
     *         @type string $label The element as it should be displayed in an error message.
     *         @type string $pattern Regex match pattern (without //) to match this particular element.
     *         @type int $indent Indentation level for the "demo code" that will be rendered in the error message.
     *     )
     *
     * @throws WPV_RuntimeExceptionWithMessage if validation fails.
     *
     * @since 1.10
     */
    protected function validate_meta_html_content( $content, $field_name, $elements ) {

        // Check which elements are missing or present too many times.
        $elements_too_many = array();
        $elements_missing = array();
        $is_correct_order = true;
        $previous_element_end_offset = -1;
        foreach( $elements as $element ) {
            $matches = null;
            $match_count = preg_match_all( "/{$element['pattern']}/", $content, $matches, PREG_OFFSET_CAPTURE );

            if( $match_count > 1 ) {

                $elements_too_many[] = $element;

            } else if( 0 == $match_count ) {

                $elements_missing[] = $element;

            } else {

                // Check order of the elements only if none are missing (it would fail)
                $element_offset = $matches[0][0][1];
                $element_length = strlen( $matches[0][0][0] );

                if( $previous_element_end_offset > $element_offset ) {
                    $is_correct_order = false;
                    break;
                }
                $previous_element_end_offset = $element_offset + $element_length;
            }
        }

        $elements_with_problems = array_merge( $elements_too_many, $elements_missing );
        $some_elements_have_problems = ( !empty( $elements_with_problems ) );

        // Throw an exception with a user-readable message if validation didn't pass.
        if( ! $is_correct_order ) {
            $error_message =
                sprintf(
                    '<p>%s</p>%s<p>%s</p>',
                    sprintf(
                        __( 'The %s cannot be saved because required elements are not in correct order.', 'wpv-views' ),
                        $field_name
                    ),
                    $this->generate_demo_meta_html_content( $field_name, $elements ),
                    __( 'Please fix the problem and click on Update again.', 'wpv-views' )
                );
            throw new WPV_RuntimeExceptionWithMessage(
                'validate_meta_html_content: incorrect element order',
                $error_message
            );
        } else if( $some_elements_have_problems ) {

            // List which elements have what problems
            $element_errors = array();
            foreach( $elements_missing as $element ) {
                $element_errors[] = sprintf( __( '%s is missing or malformed.', 'wpv-views' ), "<code>{$element['label']}</code>" );
            }
            foreach( $elements_too_many as $element ) {
                $element_errors[] = sprintf( __( '%s is used more than once.', 'wpv-views' ), "<code>{$element['label']}</code>" );
            }

            $error_message = sprintf(
                '<p>%s</p><ul><li>%s</li></ul>%s<p>%s</p>',
                sprintf(
                    __( 'The %s cannot be saved because some required elements missing or are present more than once. Please make sure that your code contains those elements in correct order:', 'wpv-views' ),
                    $field_name
                ),
                implode( '</li><li>', $element_errors ),
                $this->generate_demo_meta_html_content( $field_name, $elements, $elements_with_problems ),
                __( 'Please fix the problem and click on Update again.', 'wpv-views' )
            );
            throw new WPV_RuntimeExceptionWithMessage(
                'validate_meta_html_content: too much or few elements',
                $error_message
            );
        }
    }


    /**
     * Helper method to generate demo content for error messages in validate_meta_html_content().
     *
     * @param string $field_name Display name of the field (e.g. "Loop") that will be used
     *     in generated error messages.
     * @param array $all_elements Definition of mandatory syntax elements. See validate_meta_html_content().
     * @param array $highlight_elements Subset of $all_elements. Those elements will be rendered in "strong" tags.
     *
     * @return string Rendered HTML.
     *
     * @since 1.10
     */
    private function generate_demo_meta_html_content( $field_name, $all_elements, $highlight_elements = array() ) {
        $list_items = array();
        foreach( $all_elements as $element ) {
            if( in_array( $element, $highlight_elements ) ) {
                $element_label = sprintf( '<strong>%s</strong>', $element['label'] );
            } else {
                $element_label = $element['label'];
            }
            $list_items[] = sprintf( '%s%s', str_repeat( "&nbsp;", $element['indent'] * 4 ), $element_label );
        }
        $demo_content = sprintf(
            '<p>%s</p><p><code>%s</code></p>',
            sprintf(
                __( 'This is a minimal example of %s with mandatory elements that you can use as a reference:', 'wpv-views' ),
                $field_name
            ),
            implode( '<br />', $list_items )
        );
        return $demo_content;
    }


    /* Loop options. These need
     * - documentation
     * - sanitization
     * - some of them perhaps also renaming
     *
     * Everything @since 1.10
     */

    protected function _get_loop_style() {
        return $this->get_loop_setting( WPV_View_Base::LOOP_SETTINGS_STYLE );
    }


    protected function _get_loop_table_column_count() {
        return $this->get_loop_setting( WPV_View_Base::LOOP_SETTINGS_TABLE_COLUMN_COUNT );
    }


    protected function _get_loop_bs_column_count() {
        return $this->get_loop_setting( WPV_View_Base::LOOP_SETTINGS_BS_COLUMN_COUNT );
    }


    protected function _get_loop_bs_grid_container() {
        return $this->get_loop_setting( WPV_View_Base::LOOP_SETTINGS_BS_GRID_CONTAINER );
    }


    protected function _get_loop_row_class() {
        return $this->get_loop_setting( WPV_View_Base::LOOP_SETTINGS_BS_ROW_CLASS );
    }


    protected function _get_loop_bs_individual() {
        return $this->get_loop_setting( WPV_View_Base::LOOP_SETTINGS_BS_INDIVIDUAL );
    }


    protected function _get_loop_include_field_names() {
        return $this->get_loop_setting( WPV_View_Base::LOOP_SETTINGS_INCLUDE_FIELD_NAMES );
    }


    protected function _get_loop_fields() {
        return $this->get_loop_setting( WPV_View_Base::LOOP_SETTINGS_FIELDS );
    }


    protected function _get_loop_real_fields() {
        return $this->get_loop_setting( WPV_View_Base::LOOP_SETTINGS_REAL_FIELDS );
    }


    protected function _get_loop_included_ct_ids() {
        return $this->get_loop_setting( WPV_View_Base::LOOP_SETTINGS_INCLUDED_CT_IDS );
    }


    protected function _set_loop_style( $value ) {
        $this->set_loop_setting( WPV_View_Base::LOOP_SETTINGS_STYLE, sanitize_text_field( $value ) );
    }

	protected function _get_list_separator() {
		// We can skip sanitization for this specific field for two reasons:
		//    * Even if we sanitize it, the user will be able to change it back to the unsanitized version in the Loop
		//      so there will be inconsistencies between the saved meta value and the value that appears on the editor.
		//    * WordPress will sanitize the contents of the Loop editor, which will include the skipped fields below,
		//      as the field is saved in the database as part of a serialized array.
		return $this->get_loop_setting( WPV_View_Base::LOOP_SETTINGS_LIST_SEPARATOR );
	}

    protected function _set_loop_table_column_count( $value ) {
        $this->set_loop_setting( WPV_View_Base::LOOP_SETTINGS_TABLE_COLUMN_COUNT, sanitize_text_field( $value ) );
    }


    protected function _set_loop_bs_column_count( $value ) {
        $this->set_loop_setting( WPV_View_Base::LOOP_SETTINGS_BS_COLUMN_COUNT, sanitize_text_field( $value ) );
    }


    protected function _set_loop_bs_grid_container( $value ) {
        $this->set_loop_setting( WPV_View_Base::LOOP_SETTINGS_BS_GRID_CONTAINER, sanitize_text_field( $value ) );
    }


    protected function _set_loop_row_class( $value ) {
        $this->set_loop_setting( WPV_View_Base::LOOP_SETTINGS_BS_ROW_CLASS, sanitize_text_field( $value ) );
    }


    protected function _set_loop_bs_individual( $value ) {
        $this->set_loop_setting( WPV_View_Base::LOOP_SETTINGS_BS_INDIVIDUAL, sanitize_text_field( $value ) );
    }


    protected function _set_loop_include_field_names( $value ) {
        $this->set_loop_setting( WPV_View_Base::LOOP_SETTINGS_INCLUDE_FIELD_NAMES, sanitize_text_field( $value ) );
    }


    protected function _set_loop_fields( $value ) {
        $this->set_loop_setting( WPV_View_Base::LOOP_SETTINGS_FIELDS, $value );
    }


    protected function _set_loop_real_fields( $value ) {
        $this->set_loop_setting( WPV_View_Base::LOOP_SETTINGS_REAL_FIELDS, $value );
    }


    protected function _set_loop_included_ct_ids( $value ) {
        $this->set_loop_setting( WPV_View_Base::LOOP_SETTINGS_INCLUDED_CT_IDS, $value );
    }

    protected function _set_list_separator( $value ) {
	    // We can skip sanitization for this specific field for two reasons:
	    //    * Even if we sanitize it, the user will be able to change it back to the unsanitized version in the Loop
	    //      so there will be inconsistencies between the saved meta value and the value that appears on the editor.
	    //    * WordPress will sanitize the contents of the Loop editor, which will include the skipped fields below,
	    //      as the field is saved in the database as part of a serialized array.
	    $this->set_loop_setting( WPV_View_Base::LOOP_SETTINGS_LIST_SEPARATOR, $value );
    }


    /* ************************************************************************* *\
        Loop rendering (static)
    \* ************************************************************************* */


    /**
     * Generate default loop settings (former layout settings) for a View, based on chosen loop style
     *
     * @param string $style Loop style name, which must be one of the following values:
     *     - table
     *     - bootstrap-grid
     *     - table_of_fields
     *     - ordered_list
     *     - un_ordered_list
     *     - unformatted
     *     - empty (since 1.10): Ignores fields and renders just an empty <wpv-loop></wpv-loop>
     *
     * @param array $fields (
     *         Array of definitions of fields that will be present in the loop. If an element is not present, empty
     *         string is used instead.
     *
     *         @type string $prefix Prefix, text before shortcode.
     *         @type string $shortcode The shortcode ('[shortcode]').
     *         @type string $suffix Text after shortcode.
     *         @type string $field_name Field name.
     *         @type string $header_name Header name.
     *         @type string $row_title Row title <TH>.
     *     )
     *
     * @param array $args(
     *         Additional arguments.
     *
     *         @type bool $include_field_names If the loop style is table_of_fields, determines whether the rendered
     *             loop will contain table header with field names. Optional. Default is true.
     *
     *         @type int $tab_column_count Number of columns for the bootstrap-grid style. Optional. Default is 1.
     *         @type int $bootstrap_column_count Number of columns for the table style. Optional. Default is 1.
     *         @type int $bootstrap_version Version of Bootstrap. Mandatory for bootstrap-grid style, irrelephant
     *             otherwise. Must be 2 or 3.
     *         @type bool $add_container Argument for bootstrap-grid style. If true, enclose rendered html in a
     *             container div. Optional. Default is false.
     *         @type bool $add_row_class Argument for bootstrap-grid style. If true, a "row" class will be added to
     *             elements representing rows. For Bootstrap 3 it is added anyway. Optional. Default is false.
     *         @type bool $render_individual_columns Argument for bootstrap-grid style. If true, a wpv-item shortcode
     *             will be rendered for each singular column. Optional. Default is false.
     *
     *         @type bool $render_only_wpv_loop If true, only the code that should be within "<!-- wpv-loop start -->" and
     *             "<!-- wpv-loop end -->" tags is rendered. Optional. Default is false.
     *
     *         @type bool $use_loop_template Determines whether a Content Template will be used for field shortcodes.
     *             If true, the content of the CT will be returned in the 'ct_content' element and the loop will
     *             contain shortcodes referencing it. In such case the argument loop_template_title is mandatory. Optional.
     *             Default is false.
     *
     *         @type string $loop_template_title Title of the Content Template that should contain field shortcodes. Only
     *             relevant if use_loop_template is true, and in such case it is mandatory.
     *     )
     *
     * @return  null|array Null on error. Otherwise an array containing following elements:
     *     array(
     *         @type array loop_output_settings Loop settings for a View, as they should be stored in the database:
     *             array(
     *                 @type string $style
     *                 @type string $layout_meta_html
     *                 @type int $table_cols
     *                 @type int $bootstrap_grid_cols
     *                 @type string $bootstrap_grid_container '1' or ''
     *                 @type string $bootstrap_grid_row_class '1' or ''
     *                 @type string $bootstrap_grid_individual '1' or ''
     *                 @type string $include_field_names '1' or ''
     *                 @type array $fields
     *                 @type array $real_fields
     *             )
     *         @type string ct_content Content of the Content Template (see use_loop_template argument for more info) or
     *             an empty string.
     *     )
     *
     * @since 1.10
     * @since 2.6.4 This method was deprecated and it was replaced by this method inside a non static class.
     *              It became a wrapper that creates an instance of \OTGS\Toolset\Views\ViewLoopOutputGenerator and
     *              calls the "generate" method.
     *
     * @deprecated Use the "generate" method of the "\OTGS\Toolset\Views\View\LoopOutputGenerator()" class instead.
     */
    static function generate_loop_output( $style = 'empty', $fields = array(), $args = array() ) {
    	$loop_output_generator = new \OTGS\Toolset\Views\View\LoopOutputGenerator();
    	return $loop_output_generator->generate( $style, $fields, $args );
    }



    /* ************************************************************************* *\
            Generic View/WPA duplication support
    \* ************************************************************************* */


	/**
	 * Create a duplicate of this View.
	 *
	 * Clone the View and it's postmeta. If there is a Loop Template assigned, duplicate that as well and update
	 * references (in the appropriate postmeta, in shortcodes in loop, etc.) in the duplicated View.
	 *
	 * @param string $new_post_title Title of the new View. Must not be used in any existing View or WPA.
	 * @param bool $adjust_duplicate_title If true, the title might get changed in order to ensure it's uniqueness.
     *     Otherwise, if $title is not unique, the duplication will fail.
	 * @return bool|int ID of the new View or false on error.
	 * @since 1.11
	 */
	public function duplicate( $new_post_title, $adjust_duplicate_title = false ) {

		// Sanitize and validate
		$new_post_slug = sanitize_text_field( sanitize_title( $new_post_title ) );
		$sanitized_title = sanitize_text_field( $new_post_title );

		if( empty( $sanitized_title ) ) {
			return false;
		}

		// Ensure title uniqueness (or fail)
        $is_title_unique = ! WPV_View_Base::is_name_used( $sanitized_title );

		if( !$is_title_unique ) {
			if( $adjust_duplicate_title ) {
				$sanitized_title = WPV_View_Base::get_unique_title( $sanitized_title );
			} else {
				// Non-unique title & we're not allowed to re-use it -> fail
				return null;
			}
		}

		// Clone existing View post object
		$new_post = (array) clone $this->post();
		$new_post['post_title'] = $sanitized_title;

		$keys_to_unset = array( 'ID', 'post_name', 'post_date', 'post_date_gmt' );
		foreach( $keys_to_unset as $key ) {
			unset( $new_post[ $key ] );
		}
		
		if ( empty( $new_post_slug ) ) {
			$new_post_slug = WPV_View_Base::POST_TYPE . '-rand-' . uniqid();
		}

		$new_post['post_name'] = $new_post_slug;

		$new_post_id = wp_insert_post( $new_post );

		// Clone existing View/WPA postmeta
		$postmeta_keys_to_copy = array_keys( $this->get_postmeta_defaults() );
		$new_postmeta_values = array();
		foreach ( $postmeta_keys_to_copy as $key ) {
			$new_postmeta_values[ $key ] = $this->get_postmeta( $key );
		}

		// If this View has a loop Template, we need to clone it and adjust the layout settings.
		if ( $this->has_loop_template ) {
			$new_postmeta_values = $this->duplicate_loop_template( $new_postmeta_values, $new_post_id, $sanitized_title );
		}

		// Update postmeta of the new View.
		foreach ( $new_postmeta_values as $meta_key => $meta_value ) {
			update_post_meta( $new_post_id, $meta_key, $meta_value );
		}

		return $new_post_id;
	}


    /**
     * Duplicate a loop template of a View and update references to it.
     *
     * @param array $new_postmeta_values Array of postmeta of the View.
     * @param int $new_post_id ID of the View.
     * @param string $new_post_title Post title of the View.
     *
     * @return array Updated array of postmeta values of the View.
     */
    protected function duplicate_loop_template( $new_postmeta_values, $new_post_id, $new_post_title ) {

        // This will throw an exception if the original CT can't be accessed
        $original_ct = new WPV_Content_Template( $this->loop_template_id );

        // Clone the Content Template acting as a Loop template
        $cloned_ct = $original_ct->duplicate( sprintf( __( 'Loop item in %s', 'wpv-views' ), $new_post_title ), true );

        if( null == $cloned_ct ) {
            throw new RuntimeException( 'unable to clone loop template' );
        }

        // Cloning was successful.

        // Create reference from new View to new Loop template.
        $new_postmeta_values[ WPV_View_Base::POSTMETA_LOOP_TEMPLATE_ID ] = $cloned_ct->id;

        // Create reference from new Loop template to new View.
        $cloned_ct->loop_output_id = $new_post_id;

        // Process inline Content templates if there are any.
        $inline_templates = wpv_getarr( $new_postmeta_values[ WPV_View_Base::POSTMETA_LOOP_SETTINGS ], WPV_View_Base::LOOP_SETTINGS_INCLUDED_CT_IDS, '' );
        if ( !empty( $inline_templates ) ) {
            $inline_templates = explode( ',', $inline_templates );

            // Go through all inline templates (referenced in original View) and if we find a reference
            // to original Loop template, we will replace it with new one.
            foreach ( $inline_templates as $inline_template_key => $inline_template_id ) {

                if ( $inline_template_id == $this->loop_template_id ) {
                    // Replace with new Loop template.
                    $inline_templates[ $inline_template_key ] = $cloned_ct->id;
                }
            }

            // Update the array of inline Content templates.
            $new_postmeta_values[ WPV_View_Base::POSTMETA_LOOP_SETTINGS ][ WPV_View_Base::LOOP_SETTINGS_INCLUDED_CT_IDS ] = implode( ',', $inline_templates );
        }


        // Replace name of the old Loop template with new name in Loop.
        $loop_output = wpv_getarr( $new_postmeta_values[ WPV_View_Base::POSTMETA_LOOP_SETTINGS ], WPV_View_Base::LOOP_SETTINGS_META_HTML, '' );
        if ( !empty( $loop_output ) ) {

            // Search and replace Loop template references. Replace all occurences of title and slug by new slugs.
			$new_slug_replacement = sprintf( 'view_template="%s"', sanitize_text_field( $cloned_ct->slug ) );

            $new_loop_output = str_replace( sprintf( 'view_template="%s"', $original_ct->title ), $new_slug_replacement, $loop_output );
            $new_loop_output = str_replace( sprintf( 'view_template="%s"', $original_ct->slug ), $new_slug_replacement, $new_loop_output );

            // Save new value
            $new_postmeta_values[ WPV_View_Base::POSTMETA_LOOP_SETTINGS ][ WPV_View_Base::LOOP_SETTINGS_META_HTML ] = $new_loop_output;
        }

        return $new_postmeta_values;
    }

}
