<?php

namespace MenuManagerUltra\API\Endpoints;

use MenuManagerUltra\API\ReturnValue;
use MenuManagerUltra\API\MenuItemsReturnValue;
use MenuManagerUltra\util\MenuManagerUltra_Logger;
use MenuManagerUltra\lib\MenuItem\MenuItemFormatter;
use MenuManagerUltra\lib\Constants;
use MenuManagerUltra\lib\MenuItems\ArrayOperations;

class MenuItems extends \WP_REST_Controller {

  public function register_routes() {
    register_rest_route( Constants::ROUTE_BASE, '/menu/(?P<menu_id>\d+)/items(/(?P<last_id>\d+)?)?', array(
      'methods' => 'GET',
      'callback' => [$this, 'getMenuItems'],
      'args' => array(
        'menu_id' => array(
          'validate_callback' => function($param, $request, $key) {
            return is_numeric( $param );
          }
        ),
        'last_id' => array(
          'validate_callback' => function($param, $request, $key) {
            return is_numeric( $param );
          }
        ),   
      ),
      'permission_callback' => function($request) {
        return current_user_can('edit_theme_options');
      },
    ) );

    register_rest_route( Constants::ROUTE_BASE , '/menu/(?P<menu_id>\d+)/items/refresh', array(
      'methods' => 'POST',
      'callback' => [$this, 'refreshMenuItems'],
      'args' => array(
        'menu_id' => array(
          'validate_callback' => function($param, $request, $key) {
            return is_numeric( $param );
          }
        ),
      ),
      'permission_callback' => function($request) {
        return current_user_can('edit_theme_options');
      },
    ) );

    register_rest_route( Constants::ROUTE_BASE , '/menu/(?P<menu_id>\d+)/item/(?P<item_id>\d+)/children(/(?P<last_id>\d+)?)?', array(
      'methods' => 'GET',
      'callback' => [$this, 'getMenuItems'],
      'args' => array(
        'menu_id' => array(
          'validate_callback' => function($param, $request, $key) {
            return is_numeric( $param );
          }
        ),
        'item_id' => array(
          'validate_callback' => function($param, $request, $key) {
            return is_numeric( $param );
          }
        ),
        'last_id' => array(
          'validate_callback' => function($param, $request, $key) {
            return preg_match('/^[A-Za-z0-9_\-]+$', $param);
          }
        ),        
      ),
      'permission_callback' => function($request) {
        return current_user_can('edit_theme_options');
      }
    ) );
    
    register_rest_route( Constants::ROUTE_BASE , '/menu/update', array(
      'methods' => 'POST',
      'callback' => [$this, 'endpointSaveMenuItems'],
      'permission_callback' => function($request) {
        return current_user_can('edit_theme_options');
      }
    ));

    register_rest_route( Constants::ROUTE_BASE , '/menu/prepare-revision', array(
      'methods' => 'POST',
      'callback' => [$this, 'endpointPrepareRevisionForSave'],
      'permission_callback' => function($request) {
        return current_user_can('edit_theme_options');
      }
    ));    

    register_rest_route( Constants::ROUTE_BASE , '/menu/process-deletions', array(
      'methods' => 'POST',
      'callback' => [$this, 'endpointProcessDeletions'],
      'permission_callback' => function($request) {
        return current_user_can('edit_theme_options');
      }
    ));

    register_rest_route( Constants::ROUTE_BASE , '/menu/(?P<menu_id>\d+)/post-save-actions', array(
      'methods' => 'GET',
      'callback' => [$this, 'endpointRunPostSaveActions'],
      'args' => array(
        'menu_id' => array(
          'validate_callback' => function($param, $request, $key) {
            return is_numeric( $param );
          }
        )
      ),
      'permission_callback' => function($request) {
        return current_user_can('edit_theme_options');
      }
    ));   
  }

  public function getMenuItems( $data ) {

    $ret_status = ReturnValue::STATUS_SUCCESS;
    $ret_messages = [];

    $parent_id = (isset($data['item_id'])) ? $data['item_id']: '0';
    $items = [];
    $last_item_id = null;

    try {

      $query = new \WP_Query([
        'post_type' => 'nav_menu_item',
        'posts_per_page' => -1,
        'meta_query' => [
          [
          'key' => '_menu_item_menu_item_parent',
          'value' => $parent_id
          ]
        ],
        'orderby' => 'menu_order',
        'order' => 'ASC',
        'tax_query'   => array(
          array(
            'taxonomy' => 'nav_menu',
            'field'    => 'term_taxonomy_id',
            'terms'    => $data['menu_id']
          ),
        ),
      ]);

      $posts = $query->get_posts();

      //
      // Last ID allows us to process this operation in batches
      // because on large menus, the load can take longer than php max execution time.
      // The code below removes anything from the array before "last_id", which really should be thought of 
      // as "last_seen_id". @TODO maybe rename last_id to last_seen_id. 
      // 
      if (!empty($data['last_id'])) {

        $last_seen_id = $data['last_id'];
        $found_last_id = false;

        $posts = array_filter(
          $posts, function ($post) use ($last_seen_id, &$found_last_id) {
            if ($found_last_id) {
              return true;
            }
            if (isset($post->ID) && $post->ID == $last_seen_id) {
                $found_last_id = true;
            }
            return false;
          }
        );
      }

      $items = $this->applyMenuItemDataToPosts($data['menu_id'], $posts);
    }
    catch(TimeoutException $timeout) {

      $last_item_id = !empty($timeout->last_item_processed->ID) ? $timeout->last_item_processed->ID : null ;
      $items = $timeout->items;

      $ret_status = ($last_item_id) ? ReturnValue::STATUS_SUCCESS : ReturnValue::STATUS_ERROR;
      $ret_messages[] = ($ret_status == ReturnValue::STATUS_SUCCESS) ? 'Continue batch' : 'Error getting ID for next batch';

    }
    catch(\Exception $e) {
      $ret_messages[] = $e->getMessage();
    }
    finally {
      return [
        'status' => $ret_status,
        'messages' => $ret_messages,
        'items' => $items,
        'last_item_id' => $last_item_id
      ];
    }          

  }

  public function applyMenuItemDataToPosts($menu_id, $posts) {

    try {
     
      $batch_timeout = $this->getBatchTimeout();
      $time_start = microtime(true); 
      $enriched_items = [];

      foreach($posts as $post) {
        $menu_item = $this->enrichMenuItem($post);
        $menu_item = MenuItemFormatter::applyChildDataToItem($menu_item, $menu_id);
        $enriched_items[] = $menu_item;

        $time_end = microtime(true);
        $execution_time = ($time_end - $time_start);

        if ($execution_time >= $batch_timeout) {
          $timeout_exception = new TimeoutException();
          $timeout_exception->items = $enriched_items;
          $timeout_exception->last_item_processed = $menu_item;
          throw $timeout_exception;
        }

      }

      return $enriched_items;
    }
    catch(\Exception $e) {
      throw $e;
    }

  }

  public function fetchMenuItem($item_id) {

    return get_post($item_id);

  }

  public function confirmValidSaveRequest($request) {

    if (empty($request['items']) || empty($request['menu_id'])) {
      return false;
    }

    return true;
  }

  public function errorReturnData($message) {

    return  [
      'status' => ReturnValue::STATUS_ERROR,
      'messages' => [$message],
      'items' => array()
    ];

  }

  public function invalidSaveRequestReturnData() {
    
    return $this->errorReturnData('Missing required request parameters');
    
  }

  /**
   * Direct endpoint
   */ 
  public function endpointPrepareRevisionForSave($request) {

    if (!$this->confirmValidSaveRequest($request)) {
      return $this->invalidSaveRequestReturnData();
    }

    try {
      $revision_id = $this->prepareRevisionForSave($request['menu_id'], $request['items']);
    }
    catch(\Exception $e) {
      return $this->errorReturnData($e->getMessage());
    }

    return  [
      'status' => ReturnValue::STATUS_SUCCESS,
      'messages' => ['Revision created'],
      'revision_id' => $revision_id
    ];

  }

  public function itemsApplyPositionByOrder($items_arr) {

    return array_map(
      function($item, $index) {
        $item['menu-item-position'] = $index + 1;
        $item['mmu_applied_position'] = $item['menu-item-position'];
        return $item;
      }
      , $items_arr
      , array_keys($items_arr)
    );
  }

  public function applyUniqueIDsToItems($items_arr) {

    return array_map(
      function($item) {
        $item['mmu_unique_id'] = uniqid();
        return $item;
      },
      $items_arr
    );

  }

  public function prepareRevisionForSave($menu_id, $items_arr) {

    //Probably dont need this: $items_arr = mm_enrich_frontend_items_from_db($items_arr);
    $items_arr = $this->markItemsAsSeenByFrontend($items_arr);
    $items_arr = $this->loadItemsNotSeenByFrontend($menu_id, $items_arr);
    $items_arr = $this->applyUniqueIDsToItems($items_arr);
    $items_arr = $this->itemsAssignDeleteActions($items_arr);
    $items_arr = ArrayOperations::order_parents_and_children($items_arr);
    $items_arr = $this->itemsApplyPositionByOrder($items_arr);

    return $this->saveMenuItemsRevision($items_arr);

  }

  public function endpointProcessDeletions( $request ) {
    
    global $wpdb;

    $ret_status = ReturnValue::STATUS_ERROR;
    $ret_revision_id = null;
    $ret_messages = [];
    
    if (empty($request['menu_id']) || !is_numeric($request['menu_id'])) {
      return $this->InvalidSaveRequestReturnData();
    }

    if (empty($request['revision_id']) || !is_numeric($request['revision_id'])) {
      return $this->InvalidSaveRequestReturnData();
    }

    $revision = $this->loadMenuItemsRevision($request['revision_id']);

    if (!$revision || is_wp_error($revision)) {
      $message = is_wp_error($revision) ? $revision->get_error_messages() : 'Error loading revision';
      $ret_messages = [$message];
    }
    else {

      try {
        $items_arr = unserialize($revision->post_content);

        if (!$items_arr) {
          $ret_messages = ['Error retrieving menu data from revision'];
        }
        else {

          if ($this->anyItemMarkedForDeletion($items_arr)) {
          
            $wpdb->query('START TRANSACTION');
          
            $items_arr = $this->processDeletions($items_arr, $request['menu_id'], false);

            $ret_revision_id = $this->saveMenuItemsRevision($items_arr);
            $ret_messages[] = 'Deletions Processed';
            $ret_status = ReturnValue::STATUS_SUCCESS;

            $wpdb->query('COMMIT');

          }
          else {
            $ret_revision_id = $request['revision_id'];
            $ret_messages[] = 'No Deletions Found';
            $ret_status = ReturnValue::STATUS_SUCCESS;
          }
        }
      }
      catch(\Exception $e) {
        $wpdb->query('ROLLBACK');
        return $this->errorReturnData($e->getMessage());
      }             
    }
 
    return  [
      'status' => $ret_status,
      'messages' => $ret_messages,
      'revision_id' => $ret_revision_id
    ];
  }



  /**
   * Direct endpoint for just the main save operation; 
   * expects a revision id
   */
  public function endpointSaveMenuItems( $request ) {

    $ret_status = ReturnValue::STATUS_ERROR;
    $given_last_item_id = null;
    $updated_last_item_id = null;
    $ret_messages = [];
    $frontend_items = [];
    $items_arr = [];

    try {

      if (empty($request['revision_id']) || !is_numeric($request['revision_id'])) {
        return $this->InvalidSaveRequestReturnData();
      }

      if (empty($request['menu_id']) || !is_numeric($request['menu_id'])) {
        return $this->InvalidSaveRequestReturnData();
      }

      if (!empty($request['last_item_id'])) {
        $given_last_item_id = $request['last_item_id'];
      }

      $revision = $this->loadMenuItemsRevision($request['revision_id']);

      if (!$revision || is_wp_error($revision)) {
        $message = is_wp_error($revision) ? $revision->get_error_messages() : 'Error loading revision';
        $ret_messages = [$message];
      }
      else {
        $items_arr = unserialize($revision->post_content);

        if (!$items_arr) {
          $ret_messages = ['Error retrieving menu data from revision'];
        }
        else {

          if ($given_last_item_id) {
            $last_item_index = \MenuManagerUltra\util\flat_tree\find_item_index_by_id_field($items_arr, $given_last_item_id, Constants::FIELD_KEY_MMU_UNIQUE_ID );

            if ($last_item_index < 0) {
              throw new \Exception("Could not find index for item id {$given_last_item_id}");
            }

            $items_arr = array_slice($items_arr, $last_item_index + 1);
          }

          $items_arr = $this->itemsPerformDbSave($request['menu_id'], $items_arr);
        }
        
        $ret_status = ReturnValue::STATUS_SUCCESS;
      }
      

    }
    catch(TimeoutException $timeout) {

      $updated_last_item_id = !empty($timeout->last_item_processed[Constants::FIELD_KEY_MMU_UNIQUE_ID]) ? $timeout->last_item_processed[Constants::FIELD_KEY_MMU_UNIQUE_ID] : null ;
      $items_arr = $timeout->items;

      $ret_status = ($updated_last_item_id) ? ReturnValue::STATUS_SUCCESS : ReturnValue::STATUS_ERROR;
      $ret_messages[] = ($ret_status == ReturnValue::STATUS_SUCCESS) ? 'Continue batch' : 'Error getting ID for next batch';

    }
    catch(\Exception $e) {
      $ret_messages[] = $e->getMessage();
    }
    finally {
      $items_arr = ArrayOperations::items_recalculate_depths($items_arr);
      $frontend_items = ArrayOperations::extract_frontend_items($items_arr);

      return [
        'status' => $ret_status,
        'messages' => $ret_messages,
        'items' => array_values($frontend_items),
        'last_item_id' => $updated_last_item_id
      ];
    }

  }

  /**
   * Direct endpoint for all operations at once.
   * This has since been split into multiple operations to save on memory.
   */
  public function endpointSaveMenuItemsSingleOperation( $request ) {

    $ret_status = ReturnValue::STATUS_ERROR;
    $ret_messages = [];
    
    if (!$this->confirmValidSaveRequest($request)) {
      return $this->InvalidSaveRequestReturnData();
    }

    $frontend_items = [];
    $revision_id = $this->prepareRevisionForSave($request['menu_id'], $request['items']);
    $revision = $this->loadMenuItemsRevision($revision_id);

    if (!$revision || is_wp_error($revision)) {
      $message = is_wp_error($revision) ? $revision->get_error_messages() : 'Error loading revision';
      $ret_messages = [$message];
    }
    else {

      $items_arr = unserialize($revision->post_content);
      $items_arr = $this->applyUniqueIDsToItems($items_arr);

      if (!$items_arr) {
        $ret_messages = ['Error retrieving menu data from revision'];
      }
      else {
        $items_arr = $this->itemsPerformDbSave($request['menu_id'], $items_arr);
      }

      $items_arr = ArrayOperations::items_recalculate_depths($items_arr);
      $frontend_items = ArrayOperations::extract_frontend_items($items_arr);
      
      do_action( 'wp_update_nav_menu', $request['menu_id'], $items_arr );

      $ret_status = ReturnValue::STATUS_SUCCESS;
    }
    
    return [
      'status' => $ret_status,
      'messages' => $ret_messages,
      'items' => array_values($frontend_items)
    ];

  }

  public function endpointRunPostSaveActions($request) {

    if (empty($request['menu_id'])) {
      return $this->InvalidSaveRequestReturnData();
    }

    $items_arr = wp_get_nav_menu_items($request['menu_id']);

    do_action( 'wp_update_nav_menu', $request['menu_id'], $items_arr ); 

    return [
      'status' => ReturnValue::STATUS_SUCCESS,
      'messages' => 'Post save actions completed'
    ];

  }

  public function getBatchTimeout() {

    $timeout = Constants::BATCH_TIMEOUT_DEFAULT;
    $buffer_time = 5; //dont run right up against the execution time, give it some leeway

    if (defined('MMU_BATCH_TIMEOUT')) {
      $timeout = constant('MMU_BATCH_TIMEOUT');
    }
    else {
      $max_execution_time = ini_get('max_execution_time'); 

      if ($max_execution_time) {
        if (!empty($_ENV['PANTHEON_ENVIRONMENT'])) {
          //
          // Pantheon GCDN has a maximum 59 second request timeout
          //
          $max_execution_time = min($max_execution_time, 59);
        }

        //
        // Don't go over max execution time
        //
        $timeout = min($max_execution_time, $timeout);

        if ($timeout >= $buffer_time) {
          $timeout -= $buffer_time;
        }
      }
    }

    return $timeout;
  }

  public function itemsPerformDbSave($menu_id, $items_arr) {
    
    global $wpdb;

    $batch_timeout = $this->getBatchTimeout();
    $time_start = microtime(true); 

    // begin transaction
    $wpdb->query('START TRANSACTION');
    
    $updated_items_arr = [];
    $updated_item = [];
    $index = 0;

    foreach ($items_arr as $item) {

      //$position = $index + 1;

      // if ($item['ID'] == 1952) {
      //   print_r($items_arr);
      //   print_r($item);
      // }

      if (!empty($item['isNewAddition'])) {
        $insertable_item = $this->prepareMenuItemForInsert($item);
      }
      else {
        $insertable_item = $this->prepareMenuItemForUpdate($item);
      }

      //$insertable_item['menu-item-position'] = $position;
    
      // if ($item['ID'] == 1952) {
      // print_r($insertable_item);
      // }

      $temp_id = null;

      if (!empty($item['isNewAddition'])) {
        $insert_id = 0;
        $temp_id = $item['ID'];
      }
      else {
        $insert_id = $item['ID'];
      }

      $insert_result = wp_update_nav_menu_item($menu_id, $insert_id, $insertable_item);

      if (is_wp_error($insert_result)) {
        $wpdb->query('ROLLBACK');
        throw new \Exception($insert_result->get_error_messages());
        break;
      }
      else {
        $item_unique_id = $item[Constants::FIELD_KEY_MMU_UNIQUE_ID];
        $item_index = \MenuManagerUltra\util\flat_tree\find_item_index_by_id_field($items_arr, $item_unique_id, Constants::FIELD_KEY_MMU_UNIQUE_ID);

        if ($item_index < 0) {
          throw new \Exception("Could not find item {$item_unique_id} in items during save operation");
        }

        MenuManagerUltra_Logger::Debug("Item index for new or updated item with unique id {$item_unique_id}: {$item_index}\n");

        $updated_item = $items_arr[$item_index];
        
        if ($temp_id) {
          /* Update the array with the new DB ID, rather than the temporary one */
          $updated_item = (array) $this->loadAndEnrichMenuItem($insert_result);
          $updated_item['isNewAddition'] = false;
          $updated_item[Constants::FIELD_KEY_MMU_UNIQUE_ID] = $item_unique_id;
          $updated_item[Constants::KEY_ITEM_SEEN_BY_FRONTEND] = true;

          MenuManagerUltra_Logger::Debug("Found temp ID {$temp_id}, new item is " . print_r($updated_item, 1) . "\n");
        }

        $updated_item['menu_order'] = $updated_item['menu-item-position'];

        $updated_items_arr[$item_index] = $updated_item;

      }

      $index++;

      $time_end = microtime(true);
      $execution_time = ($time_end - $time_start);

      if ($execution_time >= $batch_timeout) {
        $wpdb->query('COMMIT');
        $timeout_exception = new TimeoutException();
        $timeout_exception->items = $updated_items_arr;
        $timeout_exception->last_item_processed = $updated_item;
        throw $timeout_exception;
      }

    }

    $wpdb->query('COMMIT');
    
    return $updated_items_arr;
  }

  public function saveMenuItemsRevision($items_arr) {

    $post_id = wp_insert_post(
      [
        'post_type' => 'mmu_revision',
        'post_title' => "Menu Revision from " . date('Y-m-d h:i:s A'),
        'post_content' => serialize($items_arr)
      ]
    );

    if (is_wp_error($post_id)) {
      throw new \Exception($post_id->get_error_message());
    }

    return $post_id;
  }

  public function loadMenuItemsRevision($id) {
    return get_post($id);
  }

  /**
   * Direct Endpoint
   *
   * Refresh items on the frontend without changing their values,
   * except the fields specified by override fields.
   * Used when settings have changed and we want to reflect the changes on the
   * frontend without reverting any changes the user has made
   */
  public function refreshMenuItems($data) {

    $return = new MenuItemsReturnValue();

    if ( empty($data['override_fields']) ) {
      /* Nothing to do here */
      $return->status = ReturnValue::STATUS_SUCCESS;
      $return->items = $data['items'];

    }
    else {
      $items = $data['items'];
      $override_fields = $data['override_fields'];

      if (!is_array($override_fields)) {
        $override_fields = [$override_fields];
      }

      $updated_items = array_map(
        function($item) use ($override_fields) {
          $db_item = (array) $this->loadAndEnrichMenuItem($item['ID']);

          foreach ($override_fields as $field) {

            if (!empty($db_item[$field])) {
              $item[$field] = $db_item[$field];
            }
          }

          return $item;
        },
        $items
      );

      $return->status = ReturnValue::STATUS_SUCCESS;
      $return->items = $updated_items;

    }

    return $return->formatResponse();
  }

  /*
  * Take the items array from the frontend and merge each item with
  * any additional data from the database, in case there's anything from the
  * database that's necessary to create a complete menu item
  * @TODO: this can be done in a single query instead of many
  * @param $items_arr
  * @return Array
  */
  public function enrichFrontendItemsFromDb($items_arr) {

    return array_map(
      function($item) {
        $item[Constants::KEY_ITEM_SEEN_BY_FRONTEND] = true;

        if (empty($item['isNewAddition'])) {
          $saved_menu_item = (array) $this->loadAndEnrichMenuItem($item['ID']);
          return array_merge($saved_menu_item, $item);
        }
        else {
          return $item;
        }
      },
      $items_arr
    );

  }

  public function markItemsAsSeenByFrontend($items_arr) {

    return array_map(
      function($item) {
        $item[Constants::KEY_ITEM_SEEN_BY_FRONTEND] = true;
        return $item;
      },
      $items_arr
    );

  }

  public function loadAndEnrichMenuItem($id) {

    $menu_item = [];
    
    if (!is_numeric($id)) {
      throw new \InvalidArgumentException('id');
    }

    $menu_item = $this->fetchMenuItem($id);
    $menu_item = $this->enrichMenuItem($menu_item);
    
    return $menu_item;

  }

  public function enrichMenuItem($menu_item) {

    if (!$this->menuItemIsSetUp($menu_item)) {
      $menu_item = wp_setup_nav_menu_item($menu_item);
    }

    $menu_item = MenuItemFormatter::applyCustomFields($menu_item);

    MenuManagerUltra_Logger::Debug("Applying links to " . print_r($menu_item, 1). "\n");
    $menu_item = MenuItemFormatter::applyLinks($menu_item);
    $menu_item = MenuItemFormatter::applyDefaultFields($menu_item);
    
    MenuManagerUltra_Logger::Debug("Applied links to " . print_r($menu_item, 1). "\n");

    return $menu_item;

  }

  public function menuItemIsSetUp($menu_item) {

    return (isset($menu_item->db_id)) ? true : false;
  }

  /*
  * Load any items from the database that are not already present
  * in the passed array
  * @param $items_arr
  */
  public function loadItemsNotSeenByFrontend($menu_id, $items_arr) {

    $query = new \WP_Query([
      'post_type' => 'nav_menu_item',
      'post__not_in' => array_column($items_arr, 'ID'),
      'posts_per_page' => -1,
      'orderby' => 'menu_order',
      'order' => 'ASC',
      'tax_query'   => array(
        array(
          'taxonomy' => 'nav_menu',
          'field'    => 'term_taxonomy_id',
          'terms'    => $menu_id
        ),
      ),
    ]);

    $other_menu_items = $query->get_posts();

    $other_menu_items = array_map(
      function($item) {
        $item = (array) wp_setup_nav_menu_item($item);
        $item[Constants::KEY_ITEM_SEEN_BY_FRONTEND] = false;
        return $item;
      },
      $other_menu_items
    );

    return array_merge($items_arr, $other_menu_items);
  }

  public function anyItemMarkedForDeletion($items_arr) {
    return !empty(
            array_filter($items_arr,
              function($item) {
                return isset($item['markedForDelete']) && $item['markedForDelete'] == true;
              }
            )
          );
  }

  public function processDeletions($items_arr, $menu_id, $assign_actions = true) {

    if ($assign_actions) {
      $items_arr = $this->itemsAssignDeleteActions($items_arr);
    }

    $delete_toggle_column_entries = array_column($items_arr, 'markedForDelete', 'ID');

    /* Find items that are marked for deletion */
    while (($id_to_delete = array_search(true, $delete_toggle_column_entries)) !== false) {

      $item_index = \MenuManagerUltra\util\flat_tree\find_item_index_by_id($items_arr, $id_to_delete);

      if ($item_index > -1) {
        $items_arr = $this->itemPerformDeleteAction($items_arr, $items_arr[$item_index]);

        /* Rescan the array in case indexes have changed due to the above operation */
        $delete_toggle_column_entries = array_column($items_arr, 'markedForDelete', 'ID');
      }

    }

    $items_arr = $this->afterProcessDeletions($items_arr, $menu_id);

    return $items_arr;

  }

  public function afterProcessDeletions($items_arr, $menu_id) {

    $items_arr = ArrayOperations::order_parents_and_children($items_arr);
    $items_arr = $this->itemsApplyPositionByOrder($items_arr);
    $items_arr = MenuItemFormatter::applyChildDataToItems($items_arr, $menu_id, true);

    return $items_arr;
  }

  public function itemsAssignDeleteActions($items_arr) {

    foreach ($items_arr as $item) {
      $items_arr = $this->itemAssignDeleteAction($items_arr, $item);
    }

    return $items_arr;

  }

  public function itemAssignDeleteAction($items_arr, $item) {

    if (!empty($item['markedForDelete'])) {
      if (!empty($item['deleteTree'])) {
        $child_item_ids = \MenuManagerUltra\util\flat_tree\find_child_ids($items_arr, $item);

        if (count($child_item_ids) > 0) {
          foreach ($child_item_ids as $child_item_id) {
            $child_item = \MenuManagerUltra\util\flat_tree\find_item_by_id($items_arr, $child_item_id);

            if ($child_item) {
              /*
                An item in the deleted tree can be manually undeleted by the user on the frontend.
                That's why we check to determine if it was ever visible on the frontend (since the user
                may not have expanded the parent tree). If it was seen by the frontend, double check markedForDelete
                which will be false if the user manually undeleted this particular item within a deleted tree.
              */
              if (!isset($child_item['deleteTree'])) {
                $child_item['deleteTree'] = true;
              }

              if (empty($child_item[Constants::KEY_ITEM_SEEN_BY_FRONTEND])) {
                $child_item['markedForDelete'] = true;
                $child_item['deleteTree'] = true;
              }

              $child_item_index = \MenuManagerUltra\util\flat_tree\find_item_index_by_id($items_arr, $child_item_id);
              $items_arr[$child_item_index] = $child_item;

              $items_arr = $this->itemAssignDeleteAction($items_arr, $child_item);
                        
            }
          }
        }
      }
    }

    return $items_arr;

  }

  public function itemPerformDeleteAction($items_arr, $item) {

    if (isset($item['deleteTree']) && !$item['deleteTree']) {
      /* Move children to parent */

      MenuManagerUltra_Logger::Debug("Should move children to parent");

      $parent_item_id = !empty($item['menu_item_parent']) ? $item['menu_item_parent'] : null;
      $parent_item    = null;
      
      if ($parent_item_id) {
        $parent_item = \MenuManagerUltra\util\flat_tree\find_item_by_id($items_arr, $parent_item_id);
      }

      $items_arr = ArrayOperations::item_reassign_children_to_new_parent($items_arr, $item, $parent_item);
      
    }

    $items_arr = \MenuManagerUltra\util\flat_tree\remove_item($items_arr, $item);
    $this->itemDeleteFromDb($item);

    return $items_arr;

  }

  public function itemDeleteFromDb($item) {

    MenuItemFormatter::confirmValidMenuItem($item);

    $result = wp_delete_post($item['ID']);

    MenuManagerUltra_Logger::Debug("Deleted Item " . $item['ID']);

    if (is_wp_error($result)) {
      throw new \RuntimeException($result->get_error_messages());
    }

    return $result;
  }

  public function prepareMenuItemForInsert($menu_item_data = []) {

    $ret = $this->prepareMenuItemForUpdate($menu_item_data);
    $ret['menu-item-status'] = 'publish';

    return $ret;
  }

  /**
  * wp_update_nav_menu_item() expects different array keys from
  * what is provided by the REST API. So this function simply converts the keys.
  * @param array $menu_item_data The data object in its "RESTful" format
  * @return array
  */
  public function prepareMenuItemForUpdate($menu_item_data = []) {

    $classes = is_array($menu_item_data['classes']) ? join(' ', $menu_item_data['classes']) : $menu_item_data['classes'];

    $ret = array(
      'menu-item-db-id'         => $menu_item_data['ID'],
      'menu-item-object-id'     => $menu_item_data['object_id'],
      'menu-item-object'        => $menu_item_data['object'],
      'menu-item-parent-id'     => $menu_item_data['menu_item_parent'],
      'menu-item-type'          => $menu_item_data['type'],
      'menu-item-title'         => $menu_item_data['title'],
      'menu-item-url'           => $menu_item_data['url'],
      'menu-item-description'   => $menu_item_data['description'],
      'menu-item-attr-title'    => $menu_item_data['attr_title'],
      'menu-item-target'        => $menu_item_data['target'],
      'menu-item-classes'       => $classes,
      'menu-item-xfn'           => $menu_item_data['xfn'],
      'menu-item-status'        => $menu_item_data['post_status'],
      'menu-item-post-date'     => $menu_item_data['post_date'],
      'menu-item-post-date-gmt' => $menu_item_data['post_date_gmt'],
    );

    //
    // We may have set this explicitly; don't override.
    //
    if (empty($menu_item_data['menu-item-position'])) {
      $ret['menu-item-position'] = $menu_item_data['menu_order'];
    }
    else {
      $ret['menu-item-position'] = $menu_item_data['menu-item-position']; 
      $ret['menu_order'] = intval($ret['menu-item-position']);
    }

    if ($ret['menu-item-description'] == null) {
      $ret['menu-item-description'] = " "; //Yes, this is how WordPress does this. ¯\_(ツ)_/¯
    }

    if ($ret['menu-item-attr-title'] === null) {
      $ret['menu-item-attr-title'] = "";
    }

    return $ret;

  }
}

class TimeoutException extends \Exception {

  public $last_item_processed;
  public $items;

}