<?php
#BEGIN_LICENSE
#-------------------------------------------------------------------------
# Module: CMSMSExt (c) 2020-2021 by CMS Made Simple Foundation
#  An add-on module for CMS Made Simple to provide useful functions
#  and commonly used gui capabilities to other modules.
#-------------------------------------------------------------------------
# A fork of:
#
# Module: CGExtensions (c) 2008-2014 by Robert Campbell
#         (calguy1000@cmsmadesimple.org)
#
#-------------------------------------------------------------------------
#
# CMSMS - CMS Made Simple is (c) 2006 - 2021 by CMS Made Simple Foundation
# CMSMS - CMS Made Simple is (c) 2005 by Ted Kulp (wishy@cmsmadesimple.org)
# Visit the CMSMS Homepage at: http://www.cmsmadesimple.org
#
#-------------------------------------------------------------------------
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# However, as a special exception to the GPL, this software is distributed
# as an addon module to CMS Made Simple.  You may not use this software
# in any Non GPL version of CMS Made simple, or in any version of CMS
# Made simple that does not indicate clearly and obviously in its admin
# section that the site was built with CMS Made simple.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
# Or read it online: http://www.gnu.org/licenses/licenses.html#GPL
#
#-------------------------------------------------------------------------
#END_LICENSE

namespace CMSMSExt;


use CmsModule;

class MigratorException extends \Exception {}
class MigratorRuntimeException extends MigratorException {}
class MigratorDbException extends MigratorException {}
class MigratorDbDropException extends MigratorException {}
class MigratorDbRollBackException extends MigratorException {}

/**
 * Class module_migrator
 *
 * This class has the implementation of the basic methods
 * needed to migrate settings and data from the source module
 * to its fork, the target module
 *
 * The class attempts to create one or more temporary DB tables
 * if a database migration is required and tries to recreate
 * the source tables as a copy of the original ones
 *
 * On a second pass it tries to recreate the indexes
 * as per the array structure described bellow
 * note on indexes: PRIMARY indexes are always automatically copied,
 * there is no need to add them to the index list;
 * trying to do so will result in a DB error anyway...
 *
 * If all goes well it copies the preferences from the source module
 *
 * The migrator is also able to copy files from a list of source files or folders
 * to a target list of folders
 *
 * @note: the class is expanding frequently, there will surely be
 * undocumented features at one point or another
 *
 * @author Fernando Morgado aka Jo Morg
 * @package CMSMSExt
 */
class module_migrator
{
  
  /**
   * @var bool
   *          should be true after initialization
   */
  protected bool                  $_initialized      = FALSE;
  protected string                $_source_module_name;
  protected string                $_target_module_name;
  protected bool                  $_source_installed = FALSE;
  protected CmsModule             $_target_module;
  protected CmsModule             $_source_module;
  protected                       $_db;
  protected int                   $_flags = self::DEFAULT;
  protected string                $_stma = 'module';
  protected string                $_ttma = 'module';
  
  /**
   * @var array
   *
   *           format of this array should be
   *           [
   *             '', // <-- empty if the table name is === to the module name
   *             'table_name_a',
   *             'table_name_b',
   *             'table_name_c',
   *                 ......
   *             'table_name_n'
   *           ]
   */
  protected array $_tables         = [];
  
  /**
   * @var array
   *
   * note:
   *        the format for this array should be
   *        [
   *          ['table_name_a', 'index_name_a'],
   *          ['table_name_b', 'index_name_b'],
   *                    .......
   *          ['table_name_n', 'index_name_n']
   *        ]
   *        empty table name if it is the module name
   * @see $_tables
   */
  protected array $_indexes_tables = [];
  
  /**
   * @var array
   *
   *           format for this array should be
   *           [
   *              'index_name_a' => ['affected_column_a'],
   *              'index_name_b' => ['affected_column_a', 'affected_column_b', 'affected_column_c']
   *           ]
   *
   *           in which index names have to exist in the $_indexes_tables array,
   *            and the column names must be valid existent ones
   *
   *           Alternatively the format can be
   *
   *           [
   *              'table_name_a' => [ 'index_name_a' => ['affected_column_a'] ],
   *              'table_name_b' =>[ 'index_name_b' => ['affected_column_a', 'affected_column_b', 'affected_column_c'] ]
   *           ]
   *
   *           to accommodate cases where the index of different tables have the same name
   */
  protected array $_indexes        = [];
  
  /**
   * @var array
   *
   *           [
   *             'preference_name_a',
   *             'preference_name_b',
   *             'preference_name_c',
   *                 ......
   *             'preference_name_n'
   *           ]
   */
  protected array $_prefs          = [];
  
  /**
   * @var array
   *
   *           a mixed array with either
   *           a folder path assuming * (all files)
   *           or an array of ['folder_path', 'pattern']
   *
   *           [
   *             ['folder_path_a', 'pattern_a'],
   *             ['folder_path_b',
   *             ['folder_path_c', 'pattern_c']
   *                 ......
   *             ['folder_path_n'
   *           ]
   */
  protected array $_folders        = [];
  
  /**
   * an array of fully qualified filenames including the path
   * or a relative path to the module folder
   *
   * @var array
   */
  protected array $_files          = [];
  
  # Flags
  
  public const DEFAULT               = 0; # h0000
  public const AUTO_PREFERENCES_COPY = 1; # h0001
  
  
  /**
   * initialization method for the migrator
   *
   * @throws \CMSMSExt\MigratorRuntimeException
   */
  protected function _initialize() : void
  {
    if(!$this->_initialized)
    {
      # determine if we have the source module in the system at all
      $mods                    = \ModuleOperations::get_instance()->GetInstalledModules();
      $this->_source_installed = \in_array($this->_source_module_name, $mods, TRUE);
      
      # classes extending this one or creating an extending class should catch the exceptions
      if(!$this->_source_installed)
      {
        throw new MigratorRuntimeException('Could not find ' . $this->_source_module_name);
      }
  
      # we do have the source, so let's try to load it along with the target module
      # note that we shouldn't have to check the target module at all
      # as this method should be called from the target
      # so we would assume it is loaded
      try
      {
        $this->_source_module = \cms_utils::get_module($this->_source_module_name);
      }
      catch(\TypeError $exception)
      {
        throw new MigratorRuntimeException('Could not load ' . $this->_source_module_name);
      }
      
      try
      {
        $this->_target_module = \cms_utils::get_module($this->_target_module_name);
      }
      catch(\TypeError $exception)
      {
        throw new MigratorRuntimeException('Could not load ' . $this->_target_module_name);
      }
      
      # this should not be needed any longer but, just in case...
      if( !$this->_have_source() )
      {
        throw new MigratorRuntimeException('Could not load ' . $this->_source_module_name);
      }
      
      if( !\is_object($this->_target_module))
      {
        throw new MigratorRuntimeException('Could not load ' . $this->_target_module_name);
      }
      
      # all good proceed
      $this->_db            = \cms_utils::get_db();
      $this->_initialized   = TRUE;
    }
  }
  
  protected function _is_source_active() : bool
  {
    return \ModuleOperations::get_instance()->IsModuleActive($this->_source_module_name);
  }
  
  protected function _is_source_installed() : bool
  {
    return $this->_source_installed;
  }
  
  protected function _have_source() : bool
  {
    return ($this->_is_source_installed() && $this->_is_source_active() && \is_object($this->_source_module) );
  }
  
  protected function _get_source_db_prefix()
  {
    return \cms_db_prefix() . $this->_stma . '_' . \strtolower($this->_source_module_name);
  }
  
  protected function _get_target_db_prefix()
  {
    return \cms_db_prefix() . $this->_ttma . '_' . \strtolower($this->_target_module_name);
  }
  
  /**
   * backs up Source tables
   * @throws \CMSMSExt\MigratorDbException
   */
  protected function _CopySourceTables2bkup() : void
  {
    $prefix_s = \cms_db_prefix() . $this->_stma . '_' . \strtolower($this->_source_module_name);
    $prefix_t = \cms_db_prefix() . $this->_ttma . '_' . \strtolower($this->_target_module_name) . '__tmp';
  
    foreach($this->_tables as $one)
    {
      if( empty($one) )
      {
        $s = $prefix_s;
        $t = $prefix_t;
      }
      else
      {
        $s = $prefix_s . '_' . $one;
        $t = $prefix_t . '_' . $one;
      }
      
      $context = $this->_source_module_name . '2' . $this->_target_module_name;
      
      \audit(80085, $context, 'processing table ' . $t);
      
      $q = 'DROP TABLE IF EXISTS ' . $t;
      $r = $this->_db->Execute($q);
    
      if(!$r)
      {
        throw new MigratorDbException(
          $this->_db->ErrorNo() . ' - ' . $this->_db->ErrorMsg(),
          $this->_db->ErrorNo()
        );
      }
  
      \audit(80085, $context, 'creating table ' . $t);
  
      $q = 'CREATE TABLE IF NOT EXISTS ' . $t . ' LIKE ' . $s;
      $r = $this->_db->Execute($q);
  
      if(!$r)
      {
        throw new MigratorDbException(
          $this->_db->ErrorNo() . ' - ' . $this->_db->ErrorMsg(),
          $this->_db->ErrorNo()
        );
      }
      
      \audit(80085, $context, 'created , copying...');
      
      $q = 'INSERT ' . $t . ' SELECT * FROM ' . $s;
      $r = $this->_db->Execute($q);
  
      if(!$r)
      {
        throw new MigratorDbException(
          $this->_db->ErrorNo() . ' - ' . $this->_db->ErrorMsg(),
          $this->_db->ErrorNo()
        );
      }
  
      \audit(80085, $context, 'copyed!');
    }
  }
  
  /**
   * copies tables from source to target
   *
   * @throws \CMSMSExt\MigratorDbException
   */
  protected function _CopySourceTables2Target()
  {
    $prefix_s = \cms_db_prefix() . $this->_stma . '_' . \strtolower($this->_source_module_name);
    $prefix_t = \cms_db_prefix() . $this->_ttma . '_' . \strtolower($this->_target_module_name);
  
    $q = 'SET foreign_key_checks = 0';
    $r = $this->_db->Execute($q);
  
    if(!$r)
    {
      throw new MigratorDbException(
        $this->_db->ErrorNo() . ' - ' . $this->_db->ErrorMsg(),
        $this->_db->ErrorNo()
      );
    }
    
    foreach($this->_tables as $one)
    {
      if( empty($one) )
      {
        $s = $prefix_s;
        $t = $prefix_t;
      }
      else
      {
        $s = $prefix_s . '_' . $one;
        $t = $prefix_t . '_' . $one;
      }
      
      $q = 'DROP TABLE IF EXISTS ' . $t;
      $r = $this->_db->Execute($q);
      
      if(!$r)
      {
        throw new MigratorDbException(
          $this->_db->ErrorNo() . ' - ' . $this->_db->ErrorMsg(),
          $this->_db->ErrorNo()
        );
      }
  
      $q = 'CREATE TABLE IF NOT EXISTS ' . $t . ' LIKE ' . $s;
      $r = $this->_db->Execute($q);
  
      if(!$r)
      {
        throw new MigratorDbException(
          $this->_db->ErrorNo() . ' - ' . $this->_db->ErrorMsg(),
          $this->_db->ErrorNo()
        );
      }
  
      $q = 'INSERT ' . $t . ' SELECT * FROM ' . $s;
      $r = $this->_db->Execute($q);
  
      if(!$r)
      {
        throw new MigratorDbException(
          $this->_db->ErrorNo() . ' - ' . $this->_db->ErrorMsg(),
          $this->_db->ErrorNo()
        );
      }
    }
    
    $q = 'SET foreign_key_checks = 1';
    $r = $this->_db->Execute($q);
  
    if(!$r)
    {
      throw new MigratorDbException(
        $this->_db->ErrorNo() . ' - ' . $this->_db->ErrorMsg(),
        $this->_db->ErrorNo()
      );
    }
  }
  
  /**
   * Rolls back i.e. gets original tables from backup
   * @throws \CMSMSExt\MigratorDbRollBackException
   */
  protected function _RollBack() : void
  {
    $prefix_s = \cms_db_prefix() . $this->_stma . '_' . \strtolower($this->_target_module_name) . '__tmp';
    $prefix_t = \cms_db_prefix() . $this->_ttma . '_' . \strtolower($this->_source_module_name);
    
    $q = 'SET foreign_key_checks = 0';
    $r = $this->_db->Execute($q);
  
    if(!$r)
    {
      throw new MigratorDbRollBackException(
        $this->_db->ErrorNo() . ' - ' . $this->_db->ErrorMsg(),
        $this->_db->ErrorNo()
      );
    }
    
    foreach($this->_tables as $one)
    {
      if( empty($one) )
      {
        $s = $prefix_s;
        $t = $prefix_t;
      }
      else
      {
        $s = $prefix_s . '_' . $one;
        $t = $prefix_t . '_' . $one;
      }
      
      $q = 'DROP TABLE IF EXISTS ' . $t;
      $r = $this->_db->Execute($q);
      
      if(!$r)
      {
        throw new MigratorDbRollBackException(
          $this->_db->ErrorNo() . ' - ' . $this->_db->ErrorMsg(),
          $this->_db->ErrorNo()
        );
      }
  
      $q = 'CREATE TABLE IF NOT EXISTS ' . $t . ' LIKE ' . $s;
      $r = $this->_db->Execute($q);
  
      if(!$r)
      {
        throw new MigratorDbRollBackException(
          $this->_db->ErrorNo() . ' - ' . $this->_db->ErrorMsg(),
          $this->_db->ErrorNo()
        );
      }
  
      $q = 'INSERT ' . $t . ' SELECT * FROM ' . $s;
      $r = $this->_db->Execute($q);
  
      if(!$r)
      {
        throw new MigratorDbRollBackException(
          $this->_db->ErrorNo() . ' - ' . $this->_db->ErrorMsg(),
          $this->_db->ErrorNo()
        );
      }
    }
  
    $q = 'SET foreign_key_checks = 1';
    $r = $this->_db->Execute($q);
  
    if(!$r)
    {
      throw new MigratorDbRollBackException(
        $this->_db->ErrorNo() . ' - ' . $this->_db->ErrorMsg(),
        $this->_db->ErrorNo()
      );
    }
  }
  
  /**
   * drops temporary backup tables
   * @throws \CMSMSExt\MigratorDbDropException
   */
  protected function _DropTemps() : void
  {
    $prefix = \cms_db_prefix() . $this->_ttma . '_' . \strtolower($this->_target_module_name) . '__tmp';
    
    # try reversing the order to prevent foreign key constraints fails
    # $tables = array_reverse($this->_tables);
    
    $q = 'SET foreign_key_checks = 0';
    $r = $this->_db->Execute($q);
  
    if(!$r)
    {
      throw new MigratorDbDropException(
        $this->_db->ErrorNo() . ' - ' . $this->_db->ErrorMsg(),
        $this->_db->ErrorNo()
      );
    }
    
    foreach($this->_tables as $one)
    {
  
      if( empty($one) )
      {
        $t = $prefix;
      }
      else
      {
        $t = $prefix . '_' . $one;
      }
      
      $context = $this->_source_module_name . '2' . $this->_target_module_name;
      \audit(80085, $context, 'dropping table ' . $t);
      
      $q = 'DROP TABLE IF EXISTS ' . $t;
      $r = $this->_db->Execute($q);
      
      if(!$r)
      {
        throw new MigratorDbDropException(
          $this->_db->ErrorNo() . ' - ' . $this->_db->ErrorMsg(),
          $this->_db->ErrorNo()
        );
      }
  
      \audit(80085, $context, 'dropped!');
    }
  
    $q = 'SET foreign_key_checks = 1';
    $r = $this->_db->Execute($q);
  
    if(!$r)
    {
      throw new MigratorDbDropException(
        $this->_db->ErrorNo() . ' - ' . $this->_db->ErrorMsg(),
        $this->_db->ErrorNo()
      );
    }
  }
  
  /**
   * @throws \CMSMSExt\MigratorDbException
   */
  protected function _RenameIndexes()
  {
    $prefix = \cms_db_prefix() . $this->_ttma . '_' . \strtolower($this->_target_module_name) . '_';
    $f = 0;
    
    foreach($this->_indexes_tables as $one)
    {
      ++$f;
      [$table, $index_name] = $one;
      
      /*
       * now here's the thing:
       * - if the table is not in our list we skip it;
       * - if it is, we need to make sure the indexes list is in the correct format
       */
      
      if( \in_array($table, $this->_tables) )
      {
        # format a?
        if( isset($this->_indexes[$index_name]) )
        {
          $table_name = $prefix . $table;
          $drop      = $index_name;
          $add       = $index_name;
          $fields    = \implode(',', $this->_indexes[$index_name]);
    
          $q = 'ALTER TABLE ' . $table_name
               . ' DROP INDEX ' . $drop
               . ', ADD INDEX ' . $add . ' (' . $fields . ') USING BTREE';
    
          $r = $this->_db->Execute($q);
    
          if(!$r)
          {
            throw new MigratorDbException(
              $this->_db->ErrorNo() . ' - ' . $this->_db->ErrorMsg(),
              $this->_db->ErrorNo()
            );
          }
        }
        # format b?
        elseif( isset($this->_indexes[$table]) )
        {
          foreach($this->_indexes[$table] as $index_name => $columns)
          {
            $table_name = $prefix . $table;
            $drop      = $index_name;
            $add       = $index_name;
            $fields    = \implode(',', $columns);
      
            $q = 'ALTER TABLE ' . $table_name
                 . ' DROP INDEX ' . $drop
                 . ', ADD INDEX ' . $add . ' (' . $fields . ') USING BTREE';
      
            $r = $this->_db->Execute($q);
            
            if(!$r)
            {
              throw new MigratorDbException(
                $this->_db->ErrorNo() . ' - ' . $this->_db->ErrorMsg(),
                $this->_db->ErrorNo()
              );
            }
          }
        }
      }
    }
    
    /*
     * previous loop didn't even iterate once?
     * then we may have an alternative format: b
     * even with an empty _indexes_tables list
     */
  
    if(!$f)
    {
      foreach($this->_indexes as $table => $data)
      {
        if( \in_array($table, $this->_tables) )
        {
          foreach($data as $index_name => $columns)
          {
            $table_name = $prefix . $table;
            $drop      = $index_name;
            $add       = $index_name;
            $fields    = \implode(',', $columns);
  
            $q = 'ALTER TABLE ' . $table_name
                 . ' DROP INDEX ' . $drop
                 . ', ADD INDEX ' . $add . ' (' . $fields . ') USING BTREE';
  
            $r = $this->_db->Execute($q);
  
            if(!$r)
            {
              throw new MigratorDbException(
                $this->_db->ErrorNo() . ' - ' . $this->_db->ErrorMsg(),
                $this->_db->ErrorNo()
              );
            }
          }
        }
      }
    }
  }
  
  /**
   * @throws \CMSMSExt\MigratorDbException
   */
  protected function _auto_preferences_copy()
  {
    try
    {
      $q = 'SELECT * FROM '
           . \cms_db_prefix()
           . 'siteprefs'
           . ' WHERE sitepref_name LIKE \'' . $this->_source_module_name.'%\'';
      
      $r = $this->_db->GetAll($q);
      
      if(FALSE === $r)
      {
        throw new MigratorDbException($this->_db->ErrorMsg(), $this->_db->ErrorNo() );
      }
      
      foreach($r as $one)
      {
        $pref = \str_replace($this->_source_module_name . '_mapi_pref_', '', $one['sitepref_name']);
        $this->_target_module->SetPreference($pref, $one['sitepref_value']);
      }
    }
    catch(\Exception $e)
    {
      throw new MigratorDbException($e->getMessage(), $e->getCode() );
    }
  
  }
  

  protected function _CopyPreferences() : void
  {
    if(\count($this->_prefs) )
    {
      foreach($this->_prefs as $one)
      {
        $this->_target_module->SetPreference( $one, $this->_source_module->GetPreference($one) );
      }
    }
    elseif($this->_flags === self::AUTO_PREFERENCES_COPY)
    {
      $this->_auto_preferences_copy();
    }

  }
  
  protected function _find_files_recursive($path , $pattern)
  {
    // todo: finish this!!!!
  }
  
  /**
   * public methods
   *
   * @param string|null $from name of the module to migrate from
   * @param string|null $to   name of the module to migrate to
   *
   * @throws \CMSMSExt\MigratorRuntimeException
   */
  
  public function __construct(string $from = NULL, string $to = NULL)
  {
    if( empty($from) || empty($to ) )
    {
      throw new MigratorRuntimeException('Both $from and $to parameters needed');
    }
    
    $this->_source_module_name = $from;
    $this->_target_module_name = $to;
    
    $this->_initialize();
  }
  
  /**
   * @param int $flags
   *
   * @return module_migrator
   */
  public function set_flags(int $flags) : module_migrator
  {
    $this->_flags = $flags;
    
    return $this;
  }
  
  
  public function set_tables($tables = NULL) : module_migrator
  {
    if(\is_array($tables) && !empty($tables) )
    {
      $this->_tables = $tables;
    }
    
    return $this;
  }
  
  public function set_indexes_tables($indexes_tables = NULL) : module_migrator
  {
    if(\is_array($indexes_tables) && !empty($indexes_tables) )
    {
      $this->_indexes_tables = $indexes_tables;
    }
    
    return $this;
  }
  
  public function set_indexes($indexes = NULL) : module_migrator
  {
    if(\is_array($indexes) && !empty($indexes) )
    {
      $this->_indexes = $indexes;
    }
    
    return $this;
  }
  
  public function set_prefs($prefs = NULL) : module_migrator
  {
    if(\is_array($prefs) && !empty($prefs) )
    {
      $this->_prefs = $prefs;
    }
    
    return $this;
  }
  
  public function set_folders($folders = NULL) : module_migrator
  {
    if(\is_array($folders) && !empty($folders) )
    {
      $this->_folders = $folders;
    }
  
    return $this;
  }
  
  public function set_files($files = NULL) : module_migrator
  {
    if(\is_array($files) && !empty($files) )
    {
      $this->_files = $files;
    }
  
    return $this;
  }
  
  public function set_source_db_table_module_alias($alias = NULL) : module_migrator
  {
    $this->_stma = $alias ?? 'module';
    return $this;
  }
  
  public function set_target_db_table_module_alias($alias = NULL) : module_migrator
  {
    $this->_ttma = $alias ?? 'module';
    return $this;
  }
  
  public function run() : array
  {
    $code = 0;
    $msg  = 'OK';
    $err  = [$code, $msg];
    $fail = FALSE;
    
    if( !$this->_is_source_installed() )
    {
      $code = 1;
      $msg = 'no_source_install';
      $err = [$code, $msg];
      return [FALSE, $err];
    }
    
    $context = $this->_source_module_name . '2' . $this->_target_module_name;
    
    # backup target tables
    try
    {
      \audit(880085, $context, 'Start making backups');
      $this->_CopySourceTables2bkup();
    }
    catch( MigratorDbException $e)
    {
      $code = $e->getCode();
      $msg = $e->getMessage();
      $full_msg = 'Something went wrong creating backups of '
                  . $this->_target_module_name
                  . ' tables with the message: code'
                  . $code
                  .' - "'
                  . $msg
                  . '" tables... going to drop temp tables!'
      ;
      
      \audit(880085, $context, $full_msg);
      
      try
      {
        # something went wrong remove the backups anyway
        $this->_DropTemps();
        \audit(880085, $context, 'Backup tables dropped successfully!');
      }
      catch( MigratorDbDropException $e)
      {
        $code = $e->getCode();
        $msg = $e->getMessage();
        \audit(880085, $context, 'Error dropping ' . $this->_target_module_name . ' temp tables: (' . $code . ') - ' . $msg);
      }
      finally
      {
        \audit(880085, $context, 'Copying tables from ' . $this->_source_module_name . ' failed! Import aborted and some cleaning up was attempted.');
        # whether backup failed or both backup and drop temps
        # we return the error code -- there is only so much we can do
        $err = [$code, $msg];
        return [FALSE, $err];
      }
    }
    
    # copy source to target
    try
    {
      \audit(880085, $context, 'Start copying tables from ' . $this->_source_module_name);
      $this->_CopySourceTables2Target();
    }
    catch( MigratorDbException $e)
    {
      $code = $e->getCode();
      $msg = $e->getMessage();
      
      # something went wrong
      \audit(880085, $context, 'Something went wrong copying tables from ' . $this->_source_module_name . ': (' . $code . ') - ' . $msg . '... going to rollback!');
      
      try
      {
        # roll back
        $this->_RollBack();
        # remove the backups anyway
        \audit(880085, $context, 'Rolled back... removing temp backup tables');
        $this->_DropTemps();
        \audit(880085, $context, 'Backup tables dropped successfully!');
      }
      catch( MigratorDbRollBackException $e)
      {
        $code = $e->getCode();
        $msg = $e->getMessage();
        \audit(880085, $context, 'Error rolling back: (' . $code . ') - ' . $msg);
      }
      catch( MigratorDbDropException $e)
      {
        $code = $e->getCode();
        $msg = $e->getMessage();
        \audit(880085, $context, 'Error dropping ' . $this->_target_module_name . ' temp tables: (' . $code . ') - ' . $msg);
      }
      finally
      {
        # whether backup failed or both backup and drop temps
        # we return the error code -- there is only so much we can do
        \audit(880085, $context, 'Copying tables from ' . $this->_source_module_name . ' failed! Import aborted and some cleaning up was attempted.');
        $err = [$code, $msg];
        
        return [FALSE, $err];
      }
    }
    
    try
    {
      $this->_DropTemps();
      \audit(880085, $context, 'Backup tables dropped successfully!');
    }
    catch( MigratorDbDropException $e)
    {
      $code = $e->getCode();
      $msg  = $e->getMessage();
      $fail = TRUE;
      \audit(880085, $context, 'Error dropping ' . $this->_target_module_name . ' temp tables: (' . $code . ') - ' . $msg);
    }
    finally
    {
      if($fail)
      {
        # whether backup failed or both backup and drop temps
        # we return the error code -- there is only so much we can do
        \audit(880085, $context, 'Copying tables from ' . $this->_source_module_name . ' seems to have worked!But apparently there are some leftover temp tables.');
        $err = [$code, $msg];
        return [FALSE, $err];
      }
    }
    
    try
    {
      $this->_RenameIndexes();
    }
    catch(MigratorDbException $e)
    {
      $code = $e->getCode();
      $msg  = $e->getMessage();
      $err  = [$code, $msg];
      return [FALSE, $err];
    }
    
    try
    {
      $this->_CopyPreferences();
    }
    catch(\Exception $e)
    {
      $code = $e->getCode();
      $msg  = $e->getMessage();
      $err  = [$code, $msg];
      return [FALSE, $err];
    }
    
    return [TRUE, $err];
  }
  
  /**
   * queries if the source module
   * is installed
   * whether it is active or not;
   *
   * can be overridden by child classes
   *
   * @return bool
   */
  public function have_source()
  {
    return $this->_have_source();
  }
  
}

?>