<?php

/**
 * @file
 * Drush hooks and include commands for Basic theme.
 */

/**
 * Implements hook_drush_command().
 */
function basic_drush_command() {

  $items['basic-install'] = array(
    'description' => dt('Run through the installation process of the Basic theme.'),
    'options' => array(
      'destination' => array(
        'description' => 'Path to which the theme will be placed. If you\'re providing a relative path, note it is relative to the drupal root.',
        'example-value' => 'themes/',
      ),
    ),
    'aliases' => array('binstall'),
  );

  return $items;
}

/**
 * Implements hook_drush_help().
 */
function basic_drush_help($section) {
  switch ($section) {
    case 'drush:basic-install':
      return dt('Runs through an automated process for installing and renaming your theme to use as your own.');
  }
}

/**
 * Implements drush_hook_COMMAND().
 */
function drush_basic_install() {
  drush_print('Checking for npm...');
  $npm_check_output = drush_shell_exec('npm version 2>&1');
  if (strpos($npm_check_output, 'command not found')) {
    drush_print('NPM could not be found. Please install npm (https://nodejs.org/en/) and try again.');
    exit();
  }
  else {
    drush_print('npm was found.');
  }

  // Prompt the user what we're doing.
  drush_print('-----------------------------');
  drush_print(dt('Welcome to the Basic Install!'));
  drush_print('-----------------------------');
  drush_print(dt('We will perform the automated tasks for you to use your new theme. We will create a copy of the Basic theme and store it in the standard theme directory or in spescified destination.'));
  drush_confirm(dt('Do you wish to continue?'));

  // Rebuild the theme data so that we can safely check for the existence of
  // themes by using the information provided by list_themes().
  $theme_handler = \Drupal::service('theme_handler');
  $theme_handler->rebuildThemeData();

  // Prompt for a theme name.
  $name = drush_prompt(dt('Please enter the name of that you want for your theme'), 'My Theme');

  // Try to generate a machine-readable name. If that fails, prompt for one.
  if (!$machine_name = drush_basic_generate_theme_name($name)) {
    drush_print(dt("Sorry, I couldn't generate a machine-readable name for @name", array(
      '@name' => $name,
    )));
  }
  // Prompt for a theme name using the automatically generated default if any.
  drush_set_option('machine-name', drush_basic_require_valid_theme_name(dt('Please enter a machine-readable name for your new theme'), $machine_name));

  // Try to generate a machine-readable name. If that fails, prompt for one.
  if (!$machine_name = drush_get_option('machine-name', drush_basic_generate_theme_name($name))) {
    drush_print(dt("Sorry, I couldn't generate a machine-readable name for @name. Please use the '--machine-name' option to specify it manually.", array(
      '@name' => $name,
    )));
  }

  $origin_path = $theme_handler->getTheme('basic')->getPath();
  $temporary = \Drupal::config('system.file')->get('path.temporary');
  $temp_path = $temporary . '/' . $machine_name;
  $destination_path = drush_basic_get_destination($machine_name);

  // Check whether the destination path exist and bail out if it does,
  // so we don't delete any important data by accident.
  if (file_exists($destination_path)) {
    return drush_set_error('BASIC_COPY_PATH', dt('The path !path already exists.', array(
      '!path' => $destination_path,
    )));
  }

  if (is_dir($temp_path)) {
    basic_rm_dir($temp_path);
  }

  // Copy into temp directory.
  if (!basic_copy_dir($origin_path, $temp_path)) {
    return drush_set_error('DRUSH_COPY_DIR_FAILURE', dt('Failed to copy !origin_path to !temp_path.', array(
      '!origin_path' => $origin_path,
      '!temp_path' => $temp_path,
    )));
  }

  // Recursively rewrite the file names and contents of all the files that are
  // now in the subtheme's directory to represent the human- and
  // machine-readable names of the subtheme.
  $search = array(
    'basic' => $machine_name,
    'Basic' => $name,
  );

  if (!drush_basic_rewrite_recursive($temp_path, array_keys($search), array_values($search))) {
    return drush_set_error('BASIC_COPY_GENERATE_THEME', dt('Failed to rewrite files and contents while generating theme.'));
  }

  // Copy from temp folder into new theme's folder.
  if (!basic_copy_dir($temp_path, $destination_path)) {
    return drush_set_error('DRUSH_COPY_DIR_FAILURE', dt('Failed to copy !temp_path to !destination_path.', array(
      '!temp_path' => $temp_path,
      '!destination_path' => $destination_path,
    )));
  }

  // Run npm install on the new theme folder.
  drush_print(dt("Building node dependencies for the theme @theme. This could take a couple minutes.", array(
    '@theme' => $machine_name,
  )));
  drush_op('chdir', DRUPAL_ROOT . '/' . $destination_path);
  drush_shell_exec('npm install');
  drush_print(dt("Node dependancies have been successfully built."));

  // Notify user of successful installation.
  drush_print(dt("------------------------------------------------------------------"));
  drush_print(dt("The new theme, !theme, has been successfully derived from Basic and placed in !destination.", array(
    '!theme' => $machine_name,
    '!destination' => $destination_path,
  )));
  drush_print(dt("------------------------------------------------------------------"));
}

/**
 * Generates a valid machine-readable name for a theme from any string.
 *
 * @param string $string
 *   The string to generate the machine-readable name from.
 *
 * @return string
 *   The generated machine-readable name.
 */
function drush_basic_generate_theme_name($string) {
  // Lowercase to start.
  $string = strtolower($string);
  // Machine-readable names have to start with a lowercase letter.
  // $string = preg_replace('/^[^a-z]+/', '', strtolower($string));
  // Machine-readable names may only contain alphanumeric characters and
  // underscores.
  $string = preg_replace('/[^a-z0-9_]+/', '_', $string);
  // Trim all trailing and leading underscores.
  $string = trim($string, '_');

  // Get list of current themes.
  $theme_handler = \Drupal::service('theme_handler');
  $themes = $theme_handler->listInfo();

  if (isset($themes[$string])) {
    $plain = $string;
    $counter = 0;

    while (isset($themes[$string])) {
      // Make sure that the machine-readable name of the theme is unique.
      $string = $plain . '_' . $counter++;
    }
  }

  return $string;
}

/**
 * Helper function that continuously prompts for a valid machine-readable name.
 *
 * @param string $message
 *   The message that should be displayed.
 * @param string $default
 *   (Optional) The default theme name. Defaults to NULL.
 *
 * @return string
 *   A valid, unique machine-readable name.
 */
function drush_basic_require_valid_theme_name($message, $default = NULL) {
  while (TRUE) {
    // Keep prompting for a machine-name until we get an acceptable value.
    $prompt = drush_prompt($message, $default);

    if (!preg_match('/^[a-z][a-z0-9_]*$/', $prompt)) {
      drush_print('The machine-readable name is invalid. It may only contain lowercase numbers, letters and underscores and must start with a letter.');
    }
    else {

      // Get list of current themes.
      $theme_handler = \Drupal::service('theme_handler');
      $themes = $theme_handler->listInfo();

      // Validate that the machine-readable name of the theme is unique.
      if (isset($themes[$prompt])) {
        drush_print(dt('A theme with the name @name already exists. The machine-readable name must be unique.', array(
          '@name' => $prompt,
        )));
      }
      else {
        // The given machine-readable name is valid. Let's proceed.
        return $prompt;
      }
    }
  }
}

/**
 * Helper function to get proper destination for the new theme.
 *
 * @param string $machine_name
 *   A valid, unique machine-readable name for the new theme.
 *
 * @return string
 *   The path to the destination where the new theme should be placed.
 */
function drush_basic_get_destination($machine_name) {
  // Find proper destination if no destination option is provided.
  if ($destination = drush_get_option('destination')) {
    $destination_path = drush_trim_path($destination) . '/' . $machine_name;
  }
  else {
    $site_root = drush_get_context('DRUSH_DRUPAL_SITE_ROOT');
    if ($site_root == 'sites/default') {
      $destination_path = 'themes/' . $machine_name;
    }
    else {
      $destination_path = $site_root . '/themes/' . $machine_name;
    }
  }
  $destination_path = drush_normalize_path($destination_path);
  return $destination_path;
}

/**
 * Recursively rewrites (and renames) all files in a given path.
 *
 * @param string $path
 *   The path to rewrite all files in.
 * @param string|array $search
 *   The string(s) to look for when replacing the file names and contents. Can
 *   be an array or a string.
 * @param string|array $replace
 *   The string(s) to replace $search with. Can be an array or a string.
 *
 * @return bool
 *   TRUE if the operation succeeded, FALSE otherwise.
 *
 * @see omega_drush_replace_contents()
 * @see str_replace()
 */
function drush_basic_rewrite_recursive($path, $search, $replace) {
  if (!is_dir($path)) {
    return drush_set_error('INVALID_PATH', dt('The given path !path is not a directory.', array(
      '!path' => $path,
    )));
  }

  // If the file actually is a directory, proceed with the recursion.
  $directory = new DirectoryIterator($path);

  foreach ($directory as $item) {
    if ($item->isDot()) {
      // Do not process '..' and '.'.
      continue;
    }

    // Retrieve the path of the current item.
    $pathname = $item->getPathname();
    if ($item->isDir() && !drush_basic_rewrite_recursive($pathname, $search, $replace)) {
      return FALSE;
    }
    elseif ($item->isFile()) {
      // If it is a file, try to replace its contents.
      $content = file_get_contents($pathname);

      // Nothing to replace in empty files.
      if (empty($content)) {
        unlink($pathname);
        continue;
      }

      if (($changed_content = str_replace($search, $replace, $content)) === NULL) {
        return drush_set_error('REWRITE_FAILURE', dt('There was an error while trying to rewrite !path (!search to !replace)', array(
          '!path' => $pathname,
          '!search' => $search,
          '!replace' => $replace,
        )));
      }

      if ($content !== $changed_content) {
        file_put_contents($pathname, $changed_content);
      }
    }
  }

  return TRUE;
}

/**
 * Copy $src to $dest.
 *
 * @param string $src
 *   The directory to copy.
 * @param string $dest
 *   The destination to copy the source to, including the new name of
 *   the directory.  To copy directory "a" from "/b" to "/c", then
 *   $src = "/b/a" and $dest = "/c/a".  To copy "a" to "/c" and rename
 *   it to "d", then $dest = "/c/d".
 * @param int $overwrite
 *   Action to take if destination already exists.
 *     - FILE_EXISTS_OVERWRITE - completely removes existing directory.
 *     - FILE_EXISTS_ABORT - aborts the operation.
 *     - FILE_EXISTS_MERGE - Leaves existing files and directories in place.
 *
 * @return bool
 *   TRUE on success, FALSE on failure.
 */
function basic_copy_dir($src, $dest, $overwrite = FILE_EXISTS_ABORT) {
  // Preflight based on $overwrite if $dest exists.
  if (file_exists($dest)) {
    if ($overwrite === FILE_EXISTS_OVERWRITE) {
      drush_op('drush_delete_dir', $dest, TRUE);
    }
    elseif ($overwrite === FILE_EXISTS_ABORT) {
      return drush_set_error('DRUSH_DESTINATION_EXISTS', dt('Destination directory !dest already exists.', array('!dest' => $dest)));
    }
    elseif ($overwrite === FILE_EXISTS_MERGE) {
      // $overwrite flag may indicate we should merge instead.
      drush_log(dt('Merging existing !dest directory', array('!dest' => $dest)));
    }
  }
  // $src readable?
  if (!is_readable($src)) {
    return drush_set_error('DRUSH_SOURCE_NOT_EXISTS', dt('Source directory !src is not readable or does not exist.', array('!src' => $src)));
  }
  // $dest writable?
  if (!is_writable(dirname($dest))) {
    return drush_set_error('DRUSH_DESTINATION_NOT_WRITABLE', dt('Destination directory !dest is not writable.', array('!dest' => dirname($dest))));
  }
  // Try to do a recursive copy.
  if (@drush_op('_basic_recursive_copy', $src, $dest)) {
    return TRUE;
  }

  return drush_set_error('DRUSH_COPY_DIR_FAILURE', dt('Unable to copy !src to !dest.', array('!src' => $src, '!dest' => $dest)));
}

/**
 * Internal function called by basic_copy_dir; do not use directly.
 */
function _basic_recursive_copy($src, $destination) {
  static $machine_name;
  if (!isset($machine_name)) {
    $machine_name = drush_get_option('machine-name');
  }
  // All subdirectories and contents.
  $ignore_directories = array(
    'node_modules',
  );
  $no_mask = '/^' . implode('|', $ignore_directories) . '$/';

  if (is_dir($src)) {
    if (!drush_mkdir($destination, TRUE)) {
      return FALSE;
    }
    $dir_handle = opendir($src);
    while ($file = readdir($dir_handle)) {
      if (($file[0] != '.' || $file == '.gitignore') && !preg_match($no_mask, $file)) {
        if (_basic_recursive_copy("$src/$file", "$destination/$file") !== TRUE) {
          return FALSE;
        }
      }
    }
    closedir($dir_handle);
  }
  elseif (is_link($src)) {
    symlink(readlink($src), $destination);
  }
  elseif (!copy($src, $destination)) {
    return FALSE;
  }

  // Rename files with basic.
  if (strpos($destination, 'basic') !== FALSE) {
    $renamed_destination = str_replace('basic', $machine_name, $destination);
    rename($destination, $renamed_destination);
  }

  // Preserve file modification time.
  touch($destination, filemtime($src));

  // Preserve execute permission.
  if (!is_link($src) && !drush_is_windows()) {
    // Get execute bits of $src.
    $execperms = fileperms($src) & 0111;
    // Apply execute permissions if any.
    if ($execperms > 0) {
      $perms = fileperms($destination) | $execperms;
      chmod($destination, $perms);
    }
  }

  return TRUE;
}

/**
 * Recursively removes all files within the provided directory.
 *
 * USE WITH CAUTION.
 *
 * @param string $dir
 *   File path for the directory you want to delete.
 */
function basic_rm_dir($dir) {
  if (is_dir($dir)) {
    $objects = scandir($dir);
    foreach ($objects as $object) {
      if ($object != "." && $object != "..") {
        if (is_dir($dir . "/" . $object)) {
          basic_rm_dir($dir . "/" . $object);
        }
        else {
          unlink($dir . "/" . $object);
        }
      }
    }
    rmdir($dir);
  }
}
