<?php

/**
 * @file An admin-only utility to test image styles and effects.
 *
 * It provides a page Test Suite in Administration > Configuration > Media >
 * Image Styles (admin/config/media/image-styles/testsuite) that displays
 * results for all existing image styles for all toolkits as well as for a set
 * of test image styles defined in the various modules.
 */

include_once('imagecache_testsuite.features.inc');

/**
 * Implementation of hook_menu().
 */
function imagecache_testsuite_menu() {
  $items = array();
  $items['admin/config/media/image-styles/testsuite'] = array(
    'title' => 'Test Suite',
    'page callback' => 'imagecache_testsuite_page',
    'access arguments' => array('administer image styles'),
    'type' => MENU_LOCAL_TASK,
    'weight' => 10,
  );
  $items['admin/config/media/image-styles/testsuite/%/%'] = array(
    'title' => 'Test Suite Image',
    'page callback' => 'imagecache_testsuite_generate',
    'page arguments' => array(5, 6),
    'access arguments' => array('administer image styles'),
    'type' => MENU_CALLBACK,
  );
  $items['admin/config/media/image-styles/testsuite/positioning_test'] = array(
    'title' => 'Positioning Test',
    'page callback' => 'imagecache_testsuite_positioning',
    'access arguments' => array('administer image styles'),
    'type' => MENU_LOCAL_TASK,
  );
  return $items;
}

/**
 * Implementation  of hook_help()
 */
function imagecache_testsuite_help($path /*, $arg*/) {
  switch ($path) {
    // @todo: this path does not exist anymore.
    case 'admin/build/imagecache/test' :
      $output = file_get_contents(drupal_get_path('module', 'imagecache_testsuite') . "/README.txt");
      return _filter_autop($output);
      break;
    case 'admin/config/media/image-styles/testsuite' :
      return t("<p>
        This page displays a number of examples of image effects.
        Illustrated are both the expected result and the actual result.
        </p><p>
        This page is just for debugging to confirm that this behavior doesn't
        change as the code gets updated.
        If the two illustrations do not match, there is probably something
        that needs fixing.
        </p><p>
        More actions are provided by each of the imagecache actions submodules
        and will be shown as you enable them.
        </p>");
      break;
    case 'admin/config/media/image-styles' :
      return t('
        A number of styles here are provided by the Imagecache
        Testsuite module as examples.
        Disable this module to make them go away.
      ');
      break;
  }
  return '';
}

/**
 * Returns the test suite page.
 *
 * The test suite page contians img links to all image derivatives to create as
 * part of the test suite.
 *
 * Samples to test are scanned from:
 * - The existing image styles.
 * - The file features.inc attached to this module. (@todo: no longer eisting?)
 * - Individual *.imagecache_preset.inc files found near any known modules.
 * Images illustrating the named preset are looked for also.
 *
 * Flushes the entire test cache every time anything is done.
 *
 * @return string
 *   The html for the page.
 */
function imagecache_testsuite_page() {
  module_load_include('inc', 'image', 'image.admin');
  module_load_include('inc', 'image', 'image.effects');

  $tests = array_merge(image_styles(), imagecache_testsuite_get_tests());
  $toolkits = image_get_available_toolkits();

  // Present the all-in-one overview page.
  $sample_folders = imagecache_testsuite_get_folders();

  // Draw the admin table.
  $test_table = array();
  foreach ($tests as $style_name => $style) {
    // Firstly, remove any previous images for the current style
    image_style_flush($style);

    $row = array();
    $row_class = 'test';
    $details_list = array();

    // Render the details.
    foreach ($style['effects'] as $effect) {
      if (!isset($effect['name'])) {
        // badness
        watchdog('imagecache_testsuite', 'invalid testcase within %style_name. No effect name', array('%style_name' => $style_name), WATCHDOG_ERROR);
        $details_list[] = '<div>Unidentified effect</div>';
        $row_class = 'error';
        continue;
      }
      $effect_definition = image_effect_definition_load($effect['name']);
      if (function_exists($effect_definition['effect callback'])) {
        $description = "<strong>{$effect_definition['label']}</strong> ";
        $description .= isset($effect_definition['summary theme']) ? theme($effect_definition['summary theme'], array('data' => $effect['data'])) : '';
        $details_list[] = "<div>$description</div>";
      }
      else {
        // Probably an action that requires a module that is not installed.
        $strings = array(
          '%action' => $effect['name'],
          '%module' => $effect['module'],
        );
        $details_list[$effect['name']] = t("<div><b>%action unavailable</b>. Please enable %module module.</div>", $strings);
        $row_class = 'error';
      }
    }
    $row['details'] = "<h3>{$style['name']}</h3><p>" . implode($details_list) . "</p>";

    // Look for a sample image. May also be defined by the definition itself,
    // but normally assume a file named after the image style, in (one of the)
    // directories with test styles.
    foreach ($sample_folders as $sample_folder) {
      if (file_exists("{$sample_folder}/{$style_name}.png")) {
        $style['sample'] = "{$sample_folder}/{$style_name}.png";
      }
      elseif (file_exists("{$sample_folder}/{$style_name}.jpg")) {
        $style['sample'] = "{$sample_folder}/{$style_name}.jpg";
      }
    }

    if (isset($style['sample']) && file_exists($style['sample'])) {
      $sample_img = theme('image', array('path' => $style['sample']));
      // I was having trouble with permissions on an OSX dev machine.
      if (!is_readable($style['sample'])) {
        $sample_img = "FILE UNREADABLE: {$style['sample']}";
      }
    }
    else {
      $sample_img = "[no sample]";
    }
    $row['sample'] = $sample_img;

    // Generate a result for each available toolkit.
    foreach ($toolkits as $toolkit => $toolkit_info) {
      $test_url = "admin/config/media/image-styles/testsuite/$style_name/$toolkit";
      $test_img = theme('image', array(
        'path' => $test_url,
        'alt' => "$style_name/$toolkit"
      ));
      $row[$toolkit] = l($test_img, $test_url, array('html' => TRUE));
    }
    $test_table[$style_name] = array(
      'data' => $row,
      'class' => array($row_class)
    );
  }

  $header = array_merge(array('test', 'sample'), array_keys($toolkits));
  $output = theme('table', array(
    'header' => $header,
    'rows' => $test_table,
    'id' => 'imagecache-testsuite'
  ));

  // @todo: zebra striping can be disabled in D7.
  // Default system zebra-striping fails to show my transparency on white.
  drupal_add_html_head('<style  type="text/css" >#imagecache-testsuite tr.even{background-color:#EEEEEE !important;} #imagecache-testsuite td{vertical-align:top;}  #imagecache-testsuite tr.error{background-color:#FFCCCC !important;}</style>');

  return $output;
}

/**
 * Returns the requested image derivative.
 *
 * If the image derivative generation is successful, the function does not
 * return but exits processing using drupal_exit().
 *
 * Flushes the entire test cache every time anything is done.
 *
 * @param string $test_id
 *   The id of the test to generate the derivative for.
 * @param string $toolkit
 *   The toolkit to use, or empty for the default toolkit
 *
 * @return string|bool
 *   - The html for the page ($test_id is empty)
 *   - False when the image derivative could not be created.
 */
function imagecache_testsuite_generate($test_id = '', $toolkit = '') {
  module_load_include('inc', 'image', 'image.admin');
  module_load_include('inc', 'image', 'image.effects');

  if (empty($toolkit)) {
    $toolkit = image_get_toolkit();
  }
  else {
    // Set the toolkit for this invocation only, so do not use variable_set.
    global $conf;
    $conf['image_toolkit'] = $toolkit;
    if ($toolkit === 'gd') {
      // This seems not to be done automatically elsewhere.
      include_once DRUPAL_ROOT . '/' . drupal_get_path('module', 'system') . '/' . 'image.gd.inc';
    }
  }

  $target = 'module://imagecache_testsuite/sample.jpg';
  $tests = array_merge(image_styles(), imagecache_testsuite_get_tests());

  // Run the process and return the image, @see image_style_create_derivative().
  $style = $tests[$test_id];
  if (!$style) {
    trigger_error("Unknown test style preset '$test_id' ", E_USER_ERROR);
    return FALSE;
  }

  // @todo: should we let the image style system do its work and just interfere on hook_init with setting the toolkit?
  // @todo: this would make the page generator easier as well and keep it working with secure image derivatives.
  // Start emulating image_style_create_derivative()
  // The main difference being I determine the toolkit I want to use.
  // SOME of this code is probably redundant, was a lot of copy&paste without true understanding of the new image.module
  if (!$image = image_load($target, $toolkit)) {
    trigger_error("Failed to open original image $target with toolkit $toolkit", E_USER_ERROR);
    return FALSE;
  }

  // Need to save the result before returning it - to stay compatible with imagemagick
  $filename = "$test_id-$toolkit.{$image->info['extension']}";
  $derivative_uri = image_style_path($style['name'], $filename);
  $directory = dirname($derivative_uri);
  file_prepare_directory($directory, FILE_CREATE_DIRECTORY);
  watchdog('imagecache_testsuite', 'Checking a save dir %dir', array('%dir' => dirname($derivative_uri)), WATCHDOG_DEBUG);

  // Imagemagick is not quite right? place a file where the file is supposed to go
  // before I put the real path there? else drupal_realpath() says nuh.
  #file_save_data('touch this for imagemagick', $derivative_uri, FILE_EXISTS_REPLACE);

  foreach ($style['effects'] as $effect) {
    // Need to load the full effect definitions, our test ones don't know all the callback info
    $effect_definition = image_effect_definition_load($effect['name']);
    if (empty($effect_definition)) {
      watchdog('imagecache_testsuite', 'I have no idea what %name is', array('%name' => $effect['name']), WATCHDOG_ERROR);
      continue;
    }
    $full_effect = array_merge($effect_definition, array('data' => $effect['data']));

    // @todo: effects that involve other images (overlay, underlay) will load that image with the default toolkit which may differ from the toolkit tested here.
    if (!image_effect_apply($image, $full_effect)) {
      watchdog('imagecache_testsuite', 'action: %action (%callback) failed for %src', array(
        '%action' => $full_effect['label'],
        '%src' => $target,
        '%callback' => $full_effect['effect callback']
      ), WATCHDOG_ERROR);
    }
  }

  if (!image_save($image, $derivative_uri)) {
    watchdog('imagecache_testsuite', 'saving image %label failed for %derivative_uri', array(
      '%derivative_uri' => $derivative_uri,
      '%label' => isset($style['label']) ? $style['label'] : $style['name']
    ), WATCHDOG_ERROR);
    return FALSE;
  }

  if ($result_image = image_load($derivative_uri)) {
    file_transfer($result_image->source, array(
      'Content-Type' => $result_image->info['mime_type'],
      'Content-Length' => $result_image->info['file_size']
    ));
    drupal_exit();
  }
  return "Failed to load the expected result from $derivative_uri";
}

/**
 * Implements hook_image_default_styles().
 *
 * Lists all our individual test cases and makes them available
 * as enabled styles
 */
function imagecache_testsuite_image_default_styles() {
  $styles = imagecache_testsuite_get_tests();

  // Need to filter out the invalid test cases
  // (ones that use unavailable actions)
  // or the core complains with notices.
//  foreach ($styles as $id => $style) {
//    foreach ($style['effects'] as $delta => $action) {
//      if (!empty($action['module']) && ($action['module'] != 'imagecache') && !module_exists($action['module'])) {
//        unset($styles[$id]);
//        break;
//      }
//    }
//  }
  return $styles;
}

/**
 * Retrieve the list of presets, each of which contain actions and action
 * definitions.
 *
 * Scans all the module folders for files named *.imagecache_preset.inc
 *
 * It seems that the required shape in D7 is
 * $style=>array(
 *   'effects' => array(
 *     0 => array('name' => 'something', 'data' => array())
 *   )
 * )
 */
function imagecache_testsuite_get_tests() {
  $presets = array();
  $folders = imagecache_testsuite_get_folders();
  foreach ($folders as $folder) {
    $preset_files = file_scan_directory($folder, "/.*.imagecache_preset.inc/");
    // Setting filepath in this scope allows the tests to know where they are.
    // The inc files may use it to create their rules.
    $filepath = $folder;
    foreach ($preset_files as $preset_file) {
      include_once($preset_file->uri);
    }
  }
  uasort($presets, 'element_sort');
  return $presets;
}

/**
 * Places to scan for test presets and sample images.
 *
 * @return array
 *   an array of folder names of everything that implements imagecache_actions.
 */
function imagecache_testsuite_get_folders() {
  $folders = array(drupal_get_path('module', 'imagecache_testsuite'));
  foreach (module_implements('image_effect_info') as $module_name) {
    $folders[] = drupal_get_path('module', $module_name) . '/tests';
  }
  return $folders;
}

/**
 * Display a page demonstrating a number of positioning tests
 *
 * Tests both styles of positioning - the x=, y= original, used in most places,
 * pls the css-like left=, top= version also.
 */
function imagecache_testsuite_positioning() {
  module_load_include('inc', 'imagecache_actions', 'utility');
  drupal_set_title("Testing the positioning algorithm");
  $tests = imagecache_testsuite_positioning_get_tests();
  $table = array();
  // $dst_image represents tha field or canvas.
  // $src_image is the item being placed on it.
  // Both these represent an imageapi-type image resource handle, but contain just dimensions
  $src_image = new stdClass();
  $src_image->info = array('width' => 75, 'height' => 100);
  $dst_image = new stdClass();
  $dst_image->info = array('width' => 200, 'height' => 150);

  foreach ($tests as $testname => $test) {
    // calc it, using either old or new method
    if (isset($test['parameters']['x']) || isset($test['parameters']['y'])) {
      $result['x'] = imagecache_actions_keyword_filter($test['parameters']['x'], $dst_image->info['width'], $src_image->info['width']);
      $result['y'] = imagecache_actions_keyword_filter($test['parameters']['y'], $dst_image->info['height'], $src_image->info['height']);
    }
    else {
      // use css style
      $result = imagecache_actions_calculate_relative_position($dst_image, $src_image, $test['parameters']);
    }
    $expected_illustration = theme_positioning_test($test['expected']['x'], $test['expected']['y']);
    $result_illustration = theme_positioning_test($result['x'], $result['y']);
    $row = array();
    $row['name'] = array('data' => '<h3>' . $testname . '</h3>' . $test['description']);
    $row['parameters'] = theme_positioning_parameters($test['parameters']);
    $row['expected'] = theme_positioning_parameters($test['expected']);
    $row['expected_image'] = $expected_illustration;
    $row['result'] = theme_positioning_parameters($result);
    $row['result_image'] = $result_illustration;
    $table[] = $row;
  }
  return 'Result of test:' . theme('table', array(
    'test',
    'parameters',
    'expected',
    'image',
    'result',
    'actual image',
    'status'
  ), $table);

}

function theme_positioning_test($x, $y) {
  $inner = "<div style='background-color:red; width:75px; height:100px; position:absolute; left:{$x}px; top:{$y}px'>";
  $outer = "<div style='background-color:blue; width:200px; height:150px; position:absolute; left:25px; top:25px'><div style='position:relative'>$inner</div></div>";
  $wrapper = "<div style='background-color:#CCCCCC; width:250px; height:200px; position:relative'>$outer</div>";
  return $wrapper;
}

function theme_positioning_parameters($parameters) {
  $outputs = array();
  foreach ($parameters as $key => $value) {
    $outputs[] = "[$key] => $value";
  }
  return '<pre>' . join("\n", $outputs) . '</pre>';
}

function imagecache_testsuite_positioning_get_tests() {
  return array(
    'base' => array(
      'parameters' => array(
        'x' => '0',
        'y' => '0',
      ),
      'description' => '0 is top left.',
      'expected' => array(
        'x' => '0',
        'y' => '0',
      ),
    ),
    'numbers' => array(
      'parameters' => array(
        'x' => '50',
        'y' => '-50',
      ),
      'description' => 'Basic numbers indicate distance and direction from top left.',
      'expected' => array(
        'x' => '50',
        'y' => '-50',
      ),
    ),
    'keywords' => array(
      'parameters' => array(
        'x' => 'center',
        'y' => 'bottom',
      ),
      'description' => "Plain keywords will align against the region",
      'expected' => array(
        'x' => '62.5',
        'y' => '50',
      ),
    ),
    'keyword with offsets' => array(
      'parameters' => array(
        'x' => 'right+10',
        'y' => 'bottom+10',
      ),
      'description' => "Keywords can be used with offsets. Positive numbers are in from the named side",
      'expected' => array(
        'x' => '115',
        'y' => '40',
      ),
    ),
    'keyword with negative offsets' => array(
      'parameters' => array(
        'x' => 'right-10',
        'y' => 'bottom-10',
      ),
      'description' => "Negative numbers place the item outside the boundry",
      'expected' => array(
        'x' => '135',
        'y' => '60',
      ),
    ),
    'percent' => array(
      'parameters' => array(
        'x' => '50%',
        'y' => '50%',
      ),
      'description' => "Percentages on their own will CENTER on both the source and destination items",
      'expected' => array(
        'x' => '62.5',
        'y' => '25',
      ),
    ),
    'keyword with percent' => array(
      'parameters' => array(
        'x' => 'right+10%',
        'y' => 'bottom+10%',
      ),
      'description' => "Percentages can be used with keywords, though the placed image will be centered on the calculated position.",
      'expected' => array(
        'x' => '142.5',
        'y' => '85',
      ),
    ),
    'css styles' => array(
      'parameters' => array(
        'left' => '10px',
        'bottom' => '10px',
      ),
      'description' => "A different method uses css-like parameters.",
      'expected' => array(
        'x' => '10',
        'y' => '40',
      ),
    ),
    'css negatives' => array(
      'parameters' => array(
        'left' => '-10px',
        'bottom' => '-10px',
      ),
      'description' => "Negative numbers from sides always move outside of the boundries.",
      'expected' => array(
        'x' => '-10',
        'y' => '60',
      ),
    ),
    'css with percents' => array(
      'parameters' => array(
        'right' => '+10%',
        'bottom' => '+10%',
      ),
      'description' => "Using percents with sides calculates the percent location on the base, then centers the source item on that point.",
      'expected' => array(
        'x' => '142.5',
        'y' => '85',
      ),
    ),
    'css centering' => array(
      'parameters' => array(
        'right' => '50%',
        'top' => '50%',
      ),
      'description' => "The auto-centering that happens when percents are used means you can easily center things at 50%.",
      'expected' => array(
        'x' => '62.5',
        'y' => '25',
      ),
    ),
    'css positioning' => array(
      'parameters' => array(
        'right' => 'left+20',
        'top' => 'bottom-20',
      ),
      'description' => "It's also possible to use keywords there, though it's not smart to do so",
      'expected' => array(
        'x' => '-55',
        'y' => '130',
      ),
    ),

  );
}
