<?php
defined( 'ABSPATH' ) or exit;   // show a blank page if try to access this file directly

require_once( EWZ_PLUGIN_DIR . "classes/ewz-settings.php" );
require_once( EWZ_PLUGIN_DIR . "classes/ewz-exception.php" );
require_once( EWZ_PLUGIN_DIR . "classes/ewz-base.php" );
require_once( EWZ_PLUGIN_DIR . "classes/ewz-field.php" );
require_once( EWZ_PLUGIN_DIR . "classes/ewz-permission.php" );
require_once( EWZ_CUSTOM_DIR . "ewz-custom-data.php" );

/********************************************************************************************
 * Interaction with the EWZ_LAYOUT table.
 *
 * Determines the appearance of a webform. Contains the title, the number of items, any
 * restrictions ( which are stored as serialized arrays ) and any member information  columns
 * ( which are for display only, and are also stored  as serialized arrays ).
 *
 * Each Ewz_Layout also "contains" several Ewz_fields, which correspond to the EWZ_FIELD table.
 * Each field contains a layout_id which specifies which layout it belongs to.
 *
 ********************************************************************************************/

class Ewz_Layout extends Ewz_Base
{

    const INCLUDE_FOLLOWUP = 1;
    const EXCLUDE_FOLLOWUP = 0;

    const DELETE_FORMS = 1;
    const FAIL_IF_FORMS = 0;


   /**
    * @var int  $layout_id   key
    */
    public $layout_id;

    // data stored on db
   /**
    * @var string  $layout_name
    */
    public $layout_name;

   /**
    * @var int  $max_num_items
    */
    public $max_num_items;

   /**
    * @var bool  $override
    */
    public $override;
   /**
    * @var array<mixed>  $restrictions
    */
    public $restrictions;

   /**
    * @var array<string,int>  $extra_cols
    */
    public $extra_cols;      // columns for the spreadsheet generated from WP member data and other tables

    /**
    * @var int  $layout_order
    */
    public $layout_order;
   /**
    * @var string  $notes
    */
    public $notes;

    // other data generated
   /**
    * @var array<int, Ewz_Field>  $fields
    */
    public $fields;

   /**
    * @var int  $n_webforms
    */
    public $n_webforms;      // number of webforms using the layout - for warning on webform edit page

    /**
    * @var int  $n_items
    */
    public $n_items;         // number of items uploaded to webforms using the layout

    /**
    * @var array<int>  $forder
    */
    public $forder;          // array of field_ids in order of display

    // non-key data stored on db
    /**
     * @var array<string,string>   $varlist
     */
    public static $varlist = array(
        'layout_name'   => 'string',
        'max_num_items' => 'integer',
        'override'      => 'boolean',
        'restrictions'  => 'array',
        'extra_cols'    => 'array',
        'layout_order'  => 'integer',
        'notes'         => 'string',
    );

    /**
    * @var string  $walias
    */
    protected static $walias;

    // items selectable for display in spreadsheet
    /**
    * @var array<string, array<string,string>>  $display_data_item
    */
    protected static $display_data_item =
        // a value containing '>' is interpreted so that origin='object', value='property>key' becomes 'object->property[key]'
        // a value containing '|' is interpreted so that origin='object', value='property|key' where property is an array
        //          becomes the concatenation of all object->property[key] values in the array
        //          e.g. item_data is an array of data whose keys are the field_ids
        //             item_data|pexcerpt becomes the concatenation of all uploaded excerpts for all image fields in the item
        // other values are interpreted so that origin='object', value='property' becomes 'object->property'
        array(
              'att' => array(  'header' => 'Attached To',   'dobject' => 'item', 'origin' => 'EWZ Item',    'value' => 'item_data>attachedto' ),
              'aat' => array(  'header' => 'Added Title',   'dobject' => 'item', 'origin' => 'EWZ Item',    'value' => 'item_data|ptitle' ),
              'aae' => array(  'header' => 'Added Caption', 'dobject' => 'item', 'origin' => 'EWZ Item',    'value' => 'item_data|pexcerpt' ),
              'aac' => array(  'header' => 'Added Description', 'dobject' => 'item', 'origin' => 'EWZ Item', 'value' => 'item_data|pcontent' ),
              'atg' => array(  'header' => 'Added Tag',      'dobject' => 'item', 'origin' => 'EWZ Item', 'value' => 'item_data|ptag' ),
              'add' => array(  'header' => 'Added Item Data','dobject'=> 'item', 'origin' => 'EWZ Item',    'value' => 'item_data>admin_data' ),
              'dlc' => array(  'header' => 'Last Changed',  'dobject' => 'item', 'origin' => 'EWZ Item',    'value' => 'last_change' ),
              'dtu' => array(  'header' => 'Upload Date',   'dobject' => 'item', 'origin' => 'EWZ Item',    'value' => 'upload_date' ),
              'iid' => array(  'header' => 'WP Item ID',    'dobject' => 'item', 'origin' => 'EWZ Item',    'value' => 'item_id' ),
              'wft' => array(  'header' => 'Webform Title', 'dobject' => 'wform','origin' => 'EWZ Webform', 'value' => 'webform_title' ),
              'wid' => array(  'header' => 'WP Webform ID', 'dobject' => 'wform','origin' => 'EWZ Webform', 'value' => 'webform_id' ),
              'wfm' => array(  'header' => 'Webform Ident', 'dobject' => 'wform','origin' => 'EWZ Webform', 'value' => 'webform_ident' ),
              );

   /**
    * @var array<string, array<string, string|array<string>>>   $display_data_user
    */
    protected static $display_data_user =
        array(
              'nam' => array(  'header' => 'Full Name',    'dobject' => 'user', 'origin' => 'WP User',   'value' => array('first_name',' ','last_name') ),
              'fnm' => array(  'header' => 'First Name',   'dobject' => 'user', 'origin' => 'WP User',   'value' => 'first_name' ),
              'lnm' => array(  'header' => 'Last Name',    'dobject' => 'user', 'origin' => 'WP User',   'value' => 'last_name' ),
              'mnm' => array(  'header' => 'Display Name', 'dobject' => 'user', 'origin' => 'WP User',   'value' => 'display_name' ),
              'mem' => array(  'header' => 'Email',        'dobject' => 'user', 'origin' => 'WP User',   'value' => 'user_email' ),
              'mid' => array(  'header' => 'WP User ID',   'dobject' => 'user', 'origin' => 'WP User',   'value' => 'ID' ),
              'mli' => array(  'header' => 'User Login',   'dobject' => 'user', 'origin' => 'WP User',   'value' => 'user_login' ),
              );


    /**
     * Check that a layout with the input id exists
     *
     * @param int $layout_id
     *
     * @return int  count of number from table with input layout_it
     */
    public static function get_count($layout_id){
        global $wpdb;
        return $wpdb->get_var( $wpdb->prepare( "SELECT count(*) FROM " . EWZ_LAYOUT_TABLE .
                                               " WHERE layout_id = %d", $layout_id ) );
    }

    /**
     * Return an array of all the headers for the spreadsheet or item list
     *
     * @return  array<int,array<string,string>>
     */
    public static function get_all_display_headers(){
        $display = array();
        foreach(  self::$display_data_item as $key => $disp ){
            $display[$key] = $disp;
            if( strpos(  $disp['header'], 'Webform') !== false ){
                $display[$key]['header'] = str_replace( 'Webform', self::$walias, $display[$key]['header'] );
            }
        }

        $data = array_merge( $display, self::$display_data_user );
        foreach( Ewz_Custom_Data::$data as $customN => $header ){
            $data[$customN] = array( 'header'  => $header,
                                     'dobject' => 'custom',
                                     'origin'  => 'Custom',
                                    );
        }
        return $data;
    }

    /**
     * Return all the data for the spreadsheet or item list
     *
     * @return  array<int, array<string,string|int>>
     */
    public static function get_all_display_data(){
        $data = array_merge( self::$display_data_item, self::$display_data_user );

        foreach( Ewz_Custom_Data::$data as $customN => $header ){
            $data[$customN]  = array( 'header'  => $header,
                                      'dobject' => 'custom',
                                      'origin'  => 'Custom',
                                      'value'   => $customN,
                                    );
        }
        return $data;
    }

    /**
     * Get an extra data item ( user, item webform or custom info ) for display
     *
     * @param  object  $dobj    data source object: Ewz_Webform, Ewz_Item, Ewz_Custom or WP_User
     * @param  mixed   $keyholder  array with keys 'dtu',  'nam', ... -- extra data required for display
     * @param  boolean $is_auto
     *
     * @return string
     */
    public static function get_extra_data_item( $dobj, $keyholder, $is_auto=false ){

        // if this is a user object, return blank if current user doesnt have permission to list
        if( isset( $dobj->user_login ) && !Ewz_Permission::can_see_users() ){
            if( !$is_auto ){
                return '';
            }
        }
       $value = '';
       if( is_array( $keyholder ) ){
            // may be used for custom data arrays
            foreach( $keyholder as $key ){
                if( isset( $dobj->$key ) ){
                    $value .= $dobj->$key;
                } else {
                    $value .= $key;
                }
            }
        } else {
            if( strpos(  $keyholder, '>' ) !== false ){
                $xx = explode( '>', $keyholder );
                if( isset( $dobj->{$xx[0]}[$xx[1]] ) ){
                    $value = $dobj->{$xx[0]}[$xx[1]];
                }
            } elseif(  strpos(  $keyholder, '|' ) !== false ){
                $xx = explode( '|', $keyholder );
                if( is_array( $dobj->{$xx[0]} ) ){
                    $value = '';
                    foreach(  $dobj->{$xx[0]} as $field_id=>$fielddata ){
                        if( isset( $fielddata[$xx[1]] ) ){
                            $value .= $fielddata[$xx[1]];
                        }
                    }
                }
            } else {
                $value = $dobj->$keyholder;
            }
        }
        if( isset($value )){
            return $value;
        }
        return '';
      }


    /**
     * Return an array of all defined layouts
     *
     * @param   string  $filter  name of Ewz_Permission function used to filter  the layouts
     *
     * @return  array<Ewz_Layout>  of all defined layout objects visible to current user
     */
    public static function get_all_layouts( $filter = 'truefunc' )
    {
        global $wpdb;

        $list = $wpdb->get_col( "SELECT layout_id  FROM " . EWZ_LAYOUT_TABLE . " ORDER BY layout_order" );    // no tainted data
        $layouts = array();
        foreach ( $list as $layout_id ) {
            if ( call_user_func( array( 'Ewz_Permission',  $filter ), $layout_id ) ) {
                $layout = new Ewz_Layout( $layout_id );
                array_push( $layouts, $layout );
            }
        }
        return $layouts;
    }

    /**
     * Return an array whose keys are all the layout_ids, and values are the layout order,
     * with gaps and duplications fixed.
     *
     * @return array<int,int>
     */
    public static function get_layout_order( ) {
        global $wpdb;
        $list = $wpdb->get_results(  "SELECT layout_id FROM " . EWZ_LAYOUT_TABLE . "  ORDER BY layout_order"  );       // no tainted data
        $order = array();
        $n=0;
        // doing it this way instead of using the saved layout_order fixes any duplication or gaps in positions
        foreach( $list as $layout ){
            $order[$layout->layout_id] = $n++;
        }
        return $order;
    }



    /**
     * Return the extra column info required for a spreadsheet download
     *
     * @param   int   $layout_id
     * @return  array<string,int> of extra_cols
     */
    public static function get_extra_cols( $layout_id )
    {
        global $wpdb;
        $xcols = $wpdb->get_var( $wpdb->prepare( "SELECT extra_cols  FROM " .  EWZ_LAYOUT_TABLE .
                                                 " WHERE layout_id = %d", $layout_id ) );
        $col_arr = self::check_unserial_arr( $xcols );
        return $col_arr;
    }


    /**
     * Return a string consisting of the html options for selecting a layout
     * NB: must return in layout_id order to match get_all_layouts
     * @param   string  $filter  name of Ewz_Permission function used to filter  the layouts
     * @param   int       $selected_id   Selected layout id, default 0
     * @return  array<int, array<string,mixed>> consisting of the html options for selecting a layout
     */
    public static function get_layout_opt_array( $filter = 'truefunc',
                                                 $selected_id = 0 )
    {

        $options = array();
        foreach ( self::get_layout_list() as $l ) {
            if ( call_user_func( array( 'Ewz_Permission',  $filter ), $l->layout_id ) ) {
                if ( $l->layout_id == $selected_id ) {
                    $is_sel = true;
                } else {
                    $is_sel = false;
                }
                array_push( $options, array( 'value' => $l->layout_id ,
                                             'display' => esc_html( $l->layout_name ),
                                             'selected' => $is_sel ) );
            }
        }
        return $options;
    }

    /**
     * Return the list of all layouts
     *
     * @return object{'layout_id':int,'layout_name':string}  with items (layout_id, layout_name)
     */
    public static function get_layout_list(){
        global $wpdb;

        $dblayouts = wp_cache_get( 'layout_list' );
        if( !$dblayouts ){
            $dblayouts = $wpdb->get_results( "SELECT layout_id, layout_name  FROM " . EWZ_LAYOUT_TABLE .        // no tainted data
                                           " ORDER BY layout_order", OBJECT );
            wp_cache_set( 'layout_list', $dblayouts);
        }
        return $dblayouts;
    }


    /**
     * Make sure the input layout_id is a valid one
     *
     * @param    int      $layout_id
     * @return   boolean  true if $layout_id is the key for a EWZ_LAYOUT_TABLE row, otherwise false
     */
    public static function is_valid_layout( $layout_id )
    {
        global $wpdb;
        $count = $wpdb->get_var( $wpdb->prepare( "SELECT count(*)  FROM " . EWZ_LAYOUT_TABLE .
                                                 " WHERE  layout_id = %d", $layout_id ) );
        return ( 1 == $count );
    }

     /**
      * Get a count of the layouts
      *
      * @return   int count
      */
     public static function count_layouts( ) {
         global $wpdb;
         $count = $wpdb->get_var(  "SELECT count(*) FROM " . EWZ_LAYOUT_TABLE  );       // no tainted data
         return $count;
     }


     /**
      * Deal with upgrade -- set layout_order field in layouts
      * @return void
      */
     public static function upgrade_set_layout_order(){
         global $wpdb;
         $list = $wpdb->get_col( "SELECT layout_id  FROM " . EWZ_LAYOUT_TABLE . " ORDER BY layout_id" );   // no tainted data
         $n = 0;
         wp_cache_delete('layout_list');
         foreach ( $list as $layout_id ) {
             wp_cache_delete( $layout_id, 'ewz_layout' );
             $wpdb->query( $wpdb->prepare( "UPDATE " . EWZ_LAYOUT_TABLE .                                  // upgrade
                                           "   SET layout_order = %d WHERE layout_id = %d ", $n, $layout_id ));
             if( $wpdb->last_error ){
                 throw new EWZ_Exception( 'Upgrade of layout order failed', $wpdb->last_error );
             }
             ++$n;
         }
     }

    /*     * ****************** Object Functions ************************* */

    /*     * ******************** Construction *************************** */
    /**
     * Assign the object variables from an array
     *
     * Calls parent::base_set_data with the list of variables and the data structure
     *
     * @param  array{'layout_id':int,array<string,string>}   $data input data array.
     * @return void
     */
    protected function set_data( $data )
    {
        parent::base_set_data( array_merge( array('layout_id' => 'integer'),
                                            self::$varlist ),
                               $data );
    }

    /**
     * Constructor
     *
     * @param  int|array<mixed>  $init  layout_id or array of data
     * @param  int    $inc_followup = self::INCLUDE_FOLLOWUP or self::EXCLUDE_FOLLOWUP
     * @return void
     */
    public function __construct( $init, $inc_followup = self::INCLUDE_FOLLOWUP )
    {
        self::$walias = esc_html( Ewz_Settings::get_ewz_option('webform_alias') );
        if ( is_numeric( $init ) ) {
            $this->create_from_id( $init, $inc_followup );
        } elseif ( is_array( $init ) ) {
            if ( array_key_exists( 'layout_id', $init ) && $init['layout_id'] ) {
                $this->update_from_data( $init );
            } else {
                $this->create_from_data( $init );
            }
        }
    }

    /**
     * Create a new layout from the layout_id by getting the data from the database
     *
     * Creates the fields array from the database
     *
     * @param  int  $id  the layout id
     * @param  int  $inc_followup = self::INCLUDE_FOLLOWUP or self::EXCLUDE_FOLLOWUP
     * @return void
     */
    protected function create_from_id( $id, $inc_followup = self::INCLUDE_FOLLOWUP )
    {
        global $wpdb;

        $dblayout = wp_cache_get( $id, 'ewz_layout' );
        if( !$dblayout ){
            $dblayout = $wpdb->get_row( $wpdb->prepare(                                         // select
                    "SELECT layout_id, " . implode( ',', array_keys( self::$varlist ) ) .
                    " FROM " . EWZ_LAYOUT_TABLE .
                    " WHERE layout_id=%d", $id ), ARRAY_A );
            if ( !$dblayout ) {
                throw new EWZ_Exception("Unable to find matching layout for id {$id}." );
            } else {
                wp_cache_set( $id, $dblayout, 'ewz_layout' );
            }
        }

        $this->set_data( $dblayout );
        $this->fields = Ewz_Field::get_fields_for_layout( $this->layout_id, 'pg_column', $inc_followup );

        // fields are in pg_column order, so this works
        $this->forder = array_keys( $this->fields );

        $this->update_usage_counts();
    }

    /**
     * Create a layout object from $data, which contains a "layout_id" key
     *
     * Error if  $data['layout_id'] does not exist on database
     *
     * @param  array<mixed>  $data
     * @return void
     */
    protected function update_from_data( $data )
    {
        global $wpdb;
        $ok = $wpdb->get_var( $wpdb->prepare( "SELECT count(*)  FROM " . EWZ_LAYOUT_TABLE .
                        " WHERE layout_id = %d", $data['layout_id'] ) );
        if ( $ok != 1 ) {
            throw new EWZ_Exception( 'Unable to find layout', $data['layout_id'] );
        }

        $this->set_data( $data );
        $this->set_field_data( $data );
        $this->update_usage_counts();
    }

    /**
     * Create a new  layout object from $data, which has no "layout_id" key
     *
     * Set the new object's layout_id to 0 and it's usage counts to 0
     *
     * @param array<mixed> $data
     * @return void
     */
    protected function create_from_data( $data )
    {
        $this->layout_id = 0;
        $this->n_webforms = 0;
        $this->n_items = 0;

        $this->set_data( $data );
        $this->set_field_data( $data );

        $this->check_errors();
    }


    /**
     * Copy the restrictions from layout with input id
     *
     *  @param  int $copyfrom    layout_id of layout whose restrictions are to be copied
     *  @return void
     */
    public function copy_restrictions_from( $copyfrom )
    {

        $field_map = array();
        $copyfrom_layout = new Ewz_Layout( $copyfrom );
        foreach( $this->forder as $n => $field_id ){
            $field_map[$copyfrom_layout->forder[$n]] = $this->fields[$this->forder[$n]]->field_id;
        }
        $this->restrictions = array();

        foreach( $copyfrom_layout->restrictions as $restrnum => $old_restr ){
            foreach( $old_restr as $old_field_id => $rval ){
                if( is_numeric( $old_field_id ) ){
                    $this->restrictions[$restrnum][$field_map[$old_field_id]] = $rval;
                } else {
                    // 'msg'
                    $this->restrictions[$restrnum][$old_field_id] = $copyfrom_layout->restrictions[$restrnum][$old_field_id];
                }
            }
        }
    }

    /**
     * Set the "fields" array from $data
     *
     * Creates a new Ewz_Field object from each element of $data['fields']
     *
     * @param  array<mixed>  $data
     * @return void
     */
    protected function set_field_data( $data )
    {
        $this->fields = array();
        if ( !array_key_exists( 'fields', $data ) ) {
            throw new EWZ_Exception( 'At least one field is required for a layout' );
        }
        foreach ( $data['fields'] as $num => $field_data ) {
            $field_data['layout_id'] = $this->layout_id;
            $this->fields[$num] = new Ewz_Field( $field_data );
        }
       $this->forder = array_keys( $this->fields );
    }

    /**
     * Set "n_webforms" and "n_items" to the counts of matching webforms/items from the database
     * @return  void
     */
    protected function update_usage_counts()
    {
        $this->n_webforms = Ewz_Webform::get_count_for_layout( $this->layout_id );

        $this->n_items = Ewz_Item::get_itemcount_for_layout( $this->layout_id );

    }


    /*     * ******************  Object Functions  *************** */
    /**
     * Does the layout contain a followup field?
     *
     * @return  int   field_id
     */

    public function contains_followup(){
        foreach( $this->fields as $field ){
            if( $field->field_ident == 'followupQ' ){
                return $field->field_id;
            }
        }
        return 0;
    }

    /**
     * Return the max dimension settings for the FIRST image field
     * Or default to 1024, 768
     * @return array<int> of 2 integers
     */
    public function get_image_dimensions(){
        foreach( $this->fields as $field ){
            if( $field->field_type == 'img' ){
                return array( $field->fdata['max_img_w'], $field->fdata['max_img_h'] );
            }
        }
        return array(1024, 768);   // fallback
    }

    /*     * ******************  Validation  *************** */

    /**
     * Check for various error conditions
     *
     * @return boolean true on success
     */
    protected function check_errors()
    {
        foreach ( self::$varlist as $key => $type ) {
            settype( $this->$key, $type );
        }

        global $wpdb;

        // check for duplicate keys
        $used = $wpdb->get_var( $wpdb->prepare( "SELECT count(*)  FROM " . EWZ_LAYOUT_TABLE .
                        " WHERE layout_name = %s AND layout_id != %d", $this->layout_name,
                                         $this->layout_id ) );
        if ( $used > 0 ) {

            throw new EWZ_Exception( "Name '$this->layout_name' already in use for this layout"  );
        }

        // make sure restrictions apply to fields belonging to the layout
        foreach ( $this->restrictions as $restr ) {
            foreach ( array_keys( $restr ) as $key ) {
                if ( is_numeric( $key ) && !in_array( $key, array_keys( $this->fields ) ) ) {
                    throw new EWZ_Exception( "Invalid Restriction Field $key in restriction " .
                            $restr['msg'] );
                }
            }
        }

        // make sure pg_column is 1:1
        $sfields = $this->fields;
        uasort( $sfields, array('self', 'pgcol_sort' ) );
        $n = 0;
        foreach ( $sfields as $key => $field ) {
            if ( !( isset( $field->pg_column ) && ( $field->pg_column == $n ) ) ) {
                $this->rearrange_columns();
                break;
            }
            $n++;
        }

        // make sure ss_column is not the same in two different fields (-1 means do not show in spreadsheet)
        $seen2 = array();
        foreach ( $this->fields as $key => $field ) {
            if ( isset( $field->ss_column ) && ( $field->ss_column >= 0 ) ) {
                    if ( array_key_exists( $field->ss_column, $seen2 ) ) {
                        $field->ss_column = -1;
                    } else {
                        $seen2[$field->ss_column] = true;
                    }
            }
        }
        // if override is off, change the webforms
        if( $this->layout_id && !$this->override ) {
            $webforms = Ewz_Webform::get_webforms_for_layout( $this->layout_id );
            foreach( $webforms as $webform ){
                $webform->update_num_items( $this->max_num_items );
            }
        }
        if( strlen( $this->layout_name ) > 60 ){
            throw new EWZ_Exception( 'Layout name too long');
        }

        return true;
    }

    /**
     * If some error has produced pg_column values that do not exist or are not consecutive integers,
     * at least set something so the user can see the data, even if the order is not what is expected.
     * @return void
     **/
    protected function rearrange_columns()
    {
        $col = 0;
        foreach( $this->fields as $field ){
            $field->pg_column = $col;
            ++$col;
        }
    }

    /*     * ******************  Database Updates ********************* */

    /**
      * Save the order of the layouts
      * Clear all layout caches (layout_list will change order)
      *
      * @param  array<int,int>  $l_orders with key=layout_id, value=order
      * @return int    number of rows updated
      */
     public static function save_layout_order( $l_orders ) {
         global $wpdb;
         $n = 0;
         wp_cache_delete('layout_list');

         $is_admin = Ewz_Permission::can_edit_all_layouts();
         if( !$is_admin ){
             foreach( array_keys( $l_orders ) as $layout_id  ){
                 if( !Ewz_Permission::can_edit_layout( $layout_id ) ){
                      throw new EWZ_Exception( 'Insufficient permissions to edit layout', "$layout_id" );
                 }
             }
         }

         $stored_orders = self::get_layout_order();
         if( $is_admin ){
             $allowed_orders =  array_values( $l_orders );
             sort( $allowed_orders);
         } else {
             $orig_order = array_intersect_key( $stored_orders, $l_orders );
             $allowed_orders = array_values( $orig_order);
             sort( $allowed_orders);
         }

         foreach( $l_orders as $layout_id => $order ){
             wp_cache_delete( $layout_id, 'ewz_layout' );
             $n = $n + $wpdb->query( $wpdb->prepare("UPDATE " . EWZ_LAYOUT_TABLE . " wf " .
                                                    "   SET layout_order = %d WHERE layout_id = %d ",
                                                      $allowed_orders[$order], $layout_id ));
             if( $wpdb->last_error ){
                 throw new EWZ_Exception( 'Saving of layout order failed', $wpdb->last_error );
             }
         }
         return $n;
     }


     /**
      * Renumber the subsequent layouts when one is deleted
      * Clear all layout caches (layout_list will change order)
      *
      * @param  int   $order  order of the deleted layout
      * @return void
      */
     private static function renumber_layouts( $order ) {
         global $wpdb;
         // no permissions check -- called only from $this->delete

         wp_cache_flush();

         $wpdb->query($wpdb->prepare("UPDATE " . EWZ_LAYOUT_TABLE . " wf " .
                                     "   SET layout_order = layout_order - 1 WHERE  layout_order > %d " , $order ));
         if( $wpdb->last_error ){
             throw new EWZ_Exception( 'Renumbering of layouts failed', $wpdb->last_error );
         }
     }


    /**
     * Save the layout to the database
     *
     * Check for permissions, then update or insert the layout data
     *                             update or insert the fields
     *
     * @return int   layout_id of saved layout
     */
    public function save()
    {
        global $wpdb;
        if( $this->layout_id ){
            if ( !Ewz_Permission::can_edit_layout( $this->layout_id ) ) {
                    throw new EWZ_Exception( 'Insufficient permissions to edit layout',
                            "$this->layout_id" );
            }
        } else {
            if ( !Ewz_Permission::can_edit_all_layouts() ) {
                throw new EWZ_Exception( 'Insufficient permissions to create a new layout' );
            }
        }

        $this->check_errors();

        wp_cache_delete( $this->layout_id, 'ewz_layout' );
        wp_cache_delete( 'layout_list' );

        // NB: layout_order is not saved here
        $data = stripslashes_deep( array(
                                         'layout_name' => $this->layout_name,          // %s
                                         'max_num_items' => $this->max_num_items,      // %d
                                         'override' => $this->override ? 1 : 0,        // %d
                                         'restrictions' => serialize( stripslashes_deep( $this->restrictions ) ),   // %s
                                         'extra_cols' => serialize( stripslashes_deep( $this->extra_cols ) ),       // %s
                                         'notes' => $this->notes,                      // %s
                                         ) );
        $datatypes = array( '%s', // = layout_name
                            '%d', // = max_num_items
                            '%d', // = override
                            '%s', // = restrictions
                            '%s', // = extra_cols
                            '%s', // = notes
                            );
        // update or insert the layout itself
        if ( $this->layout_id ) {
            $rows = $wpdb->update( EWZ_LAYOUT_TABLE,                                           // no esc
                                   $data,        array('layout_id' => $this->layout_id),
                                   $datatypes,   array('%d')
                                 );
            if( false === $rows || $wpdb->last_error ){
                throw new EWZ_Exception( 'Failed to update layout', $wpdb->last_error );
            }
            if ( $rows > 1 ) {
                throw new EWZ_Exception( 'Problem with update of layout ' . $this->layout_name );
            }
        } else {
            $n_layouts = self::count_layouts();
            $rows = $wpdb->insert( EWZ_LAYOUT_TABLE, $data, $datatypes );                  // no esc
            if( false === $rows  || $wpdb->last_error ){
                throw new EWZ_Exception( 'Failed to create layout', $wpdb->last_error );
            }
            $this->layout_id = $wpdb->insert_id;
            if ( !$this->layout_id ) {
                throw new EWZ_Exception( 'Problem with creation of layout ' . $this->layout_name );
            }

            $rows = $wpdb->query($wpdb->prepare( "UPDATE " . EWZ_LAYOUT_TABLE .
                                         "   SET layout_order = %d WHERE  layout_id = %d ", $n_layouts, $this->layout_id ) );
            if( false === $rows || $wpdb->last_error){  /** @phpstan-ignore-line */
                throw new EWZ_Exception( 'Failed to update layout order', $wpdb->last_error );
            }
            $this->layout_order = $n_layouts;
        }

        // save the field data and fix up any restrictions ( need field id to do that )
        $havenewfield = false;
        foreach ( $this->fields as $fid=>$field ) {
            $field->layout_id = $this->layout_id;
            $field_id_or_0 = $field->save();
            if( $field_id_or_0 ){
                unset( $this->fields[$fid] );
                $this->fields[$field_id_or_0] = $field;
                $havenewfield = true;
                // new field, set its value to '~*~' in each restriction
                foreach( $this->restrictions as $n => $restr ){
                    $this->restrictions[$n][$field_id_or_0] = '~*~';
                }
            }
        }
       $this->forder = array_keys( $this->fields );
       $restrs = serialize( stripslashes_deep( $this->restrictions ) );
       // save the restrictions again
       if(  $havenewfield && $this->restrictions ){
            $rows = $wpdb->update( EWZ_LAYOUT_TABLE,                                       // no esc
                                   array( 'restrictions' => $restrs ),  array( 'layout_id' => $this->layout_id ),
                                   array( '%s' ),                       array( '%d' ) );
            if( false === $rows || $wpdb->last_error ){  /** @phpstan-ignore-line */
                throw new EWZ_Exception( 'Failed to update restrictions for new fields', $wpdb->last_error );
            }
            if ( $rows != 1 ) {
                throw new EWZ_Exception( "$rows: Problem with update of restrictions for new fields  " . $this->layout_name );
            }
       }
       return $this->layout_id;
    }

    /**
     * Delete a specified field from the database
     *
     * @param  int   $field_id
     * @return void
     */
    public function delete_field( $field_id )
    {
        if ( !Ewz_Permission::can_edit_layout( $this->layout_id ) ) {
            throw new EWZ_Exception( 'Insufficient permissions to edit the layout', $this->layout_id );
        }

        // make sure this is a valid field for this layout
        $field = null;
        foreach ( $this->fields as $test_field ) {
            if ( $test_field->field_id == $field_id ) {
                $field = $test_field;
            }
        }
        if ( $field !== null ) {
            foreach( $this->restrictions as $n => $restr ){
                if( isset( $this->restrictions[$n][$field_id] ) ){
                    if ( $this->restrictions[$n][$field_id] != '~*~' ){
                        // must remove the restriction, since an essential part of it has been removed
                        // ( only way we get here is if the restriction was removed but the layout not saved )
                        unset( $this->restrictions[$n] );
                    } else {
                        unset( $this->restrictions[$n][$field_id] );
                    }
                }
            }
            $this->save();
            $field->delete();
        } else {
            throw new EWZ_Exception( 'Failed to find field to delete', $field_id );
        }
    }

    /**
     * Delete the layout and all its fields from the database
     * First calls action ewz_before_delete_layout
     *
     * @param  int   $delete_forms = self::FAIL_IF_FORMS ( fail if any weforms use this layout )
     *                               or self::DELETE_FORMS (delete all webforms using this layout so long as they have no items)
     * @return void
     */
    public function delete( $delete_forms = self::FAIL_IF_FORMS )
    {
        global $wpdb;

        if ( !Ewz_Permission::can_edit_all_layouts() ) {
            throw new EWZ_Exception( 'Insufficient permissions to delete the layout',
                    $this->layout_id );
        }

        $all = self::get_all_layouts();
        if( count( $all ) < 2 ){
            throw new EWZ_Exception( "Attempt to delete the only layout", "layout_id " . $this->layout_id );
        }

        try {
            do_action( 'ewz_before_delete_layout', $this->layout_id );

            $webforms = Ewz_Webform::get_webforms_for_layout( $this->layout_id );
            if( $delete_forms ==  self::DELETE_FORMS ){
                foreach( $webforms as $wform ){
                    // never delete forms containing items using this function
                    // - force each form to be deleted separately
                    $wform->delete( Ewz_Webform::FAIL_IF_ITEMS );
                }
                $webforms = Ewz_Webform::get_webforms_for_layout( $this->layout_id );
                $n = count( $webforms );
                if( ( $n > 0 ) ){
                    $forms = join(', ', array_map(function($v){ return $v->webform_title; }, $webforms ) );
                    throw new EWZ_Exception( "Attempt to delete layout with $n associated webforms: " . $forms );
                }
            }

        } catch( Exception $e ){
            // bail now, before the deletion gets done
            throw new EWZ_Exception( 'Error deleting layout: ' . $e->getMessage() );
        }

        foreach ( $this->fields as $field ) {
            $field->delete();
        }

        wp_cache_delete( $this->layout_id, 'ewz_layout' );
        wp_cache_delete( 'layout_list' );

        // now delete the layout and renumber the layout_order for the remaining ones
        $rowsaffected = $wpdb->query( $wpdb->prepare( "DELETE FROM " . EWZ_LAYOUT_TABLE .
                " WHERE layout_id = %d", $this->layout_id ) );
        if( false === $rowsaffected  || $wpdb->last_error ){
            throw new EWZ_Exception( 'Failed to delete layout', $wpdb->last_error );
        }
        if ( 1 == $rowsaffected ) {
            self::renumber_layouts($this->layout_order);
            // do this after the deletion, so any failures in it don't stop the deletion
            Ewz_Permission::delete_layout_permissions( $this->layout_id );
        } else {
            throw new EWZ_Exception( "Problem deleting layout '$this->layout_name '" );
        }
    }

    /**
     * Sort function for use in sorting the fields for the admin layouts page
     *
     * Sorts by the pg_column
     *
     * @param Ewz_Field  $a  an Ewz_Field object
     * @param Ewz_Field  $b  an Ewz_Field object
     * @return  int  -1 if $a sorts before $b, 0 if they are equal, 1 otherwise
     **/
    protected static function pgcol_sort( $a, $b ){
        if( empty($a->pg_column) ){
            return -1;
        }
        if( empty($b->pg_column) ){
            return 1;
        }
        if($a->pg_column == $b->pg_column){
            return 0;
        }
        return ($a->pg_column < $b->pg_column) ? -1 : 1;
    }

}
