/**
 * Copyright (c) 2004-2007 IBM Corporation and others.
 * All rights reserved.   This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v20.html
 *
 * Contributors:
 *   IBM - Initial API and implementation
 */
package org.eclipse.emf.test.tools.merger;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.util.regex.Pattern;

import org.eclipse.emf.codegen.ecore.generator.Generator;
import org.eclipse.emf.codegen.ecore.generator.GeneratorAdapterFactory;
import org.eclipse.emf.codegen.ecore.genmodel.GenModel;
import org.eclipse.emf.codegen.ecore.genmodel.GenModelFactory;
import org.eclipse.emf.codegen.ecore.genmodel.generator.GenModelGeneratorAdapterFactory;
import org.eclipse.emf.codegen.merge.java.JControlModel;
import org.eclipse.emf.codegen.merge.java.JMerger;
import org.eclipse.emf.codegen.merge.java.facade.FacadeHelper;
import org.eclipse.emf.common.EMFPlugin;
import org.eclipse.emf.test.common.TestUtil;
import org.eclipse.emf.test.tools.AllSuites;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.core.formatter.DefaultCodeFormatterConstants;
import org.junit.Before;
import org.junit.ComparisonFailure;
import org.junit.Test;


/**
 * Base class for all JMerger tests.
 * <p>
 * Merge source and target files are respectively <code>MergerSource.java</code> and <code>MergerTarget.java</code> in data directory.
 * <p>
 * Expected output file is either test specific output file (determined by {@link #getTestSpecificExpectedOutput()} or
 * <code>MergerExpected.java</code> in data directory.
 * <p>
 * Merge rules are used from <code>merge.xml</code> file in data directory if it exists, otherwise default EMF merge rules are used.
 */
public abstract class JMergerTest
{
  private static boolean UPDATE_EXPECTED_RESULT_ON_FAILURE = System.getProperty("org.eclipse.emf.test.tools.ant.Update") != null;

  /**
   * Default root data directory containing java versions subdirectories.
   * <p>
   * Default is /data/merge.input
   * @see JMergerTest#DIRECTORY_NAMES_TO_JAVA_VERSIONS
   */
  protected static final File DEFAULT_ROOT_DIRECTORY = new File(TestUtil.getPluginDirectory(AllSuites.PLUGIN_ID) + File.separator + "data" + File.separator + "merge.input");

  /**
   * Default name of the expected output file.
   */
  public static final String DEFAULT_EXPECTED_OUTPUT_FILENAME = "MergerExpected.java";

  /**
   * Map of directory names to versions of Java to be used to overwrite settings in JavaCore.
   */
  public static final Map<String, String> DIRECTORY_NAMES_TO_JAVA_VERSIONS;

  static
  {
    Map<String, String> directoryNamesMap = new HashMap<String, String>(4);
    directoryNamesMap.put("java1.4", JavaCore.VERSION_1_4);
    directoryNamesMap.put("java5", JavaCore.VERSION_1_5);
    DIRECTORY_NAMES_TO_JAVA_VERSIONS = Collections.unmodifiableMap(directoryNamesMap);
  }

  public static Collection<Object[]> parameters(String prefix)
  {
    return populateSuite(prefix);
  }

  /**
   * Verifies that target contents matches the contents of expected output file.
   *
   * @param expectedOutput
   * @param targetContents
   */
  protected static void verifyMerge(File expectedOutput, String targetContents)
  {
    // extract merged contents
    StringBuilder mergeResult = new StringBuilder(targetContents);

    // The merge expected file was saved without any '\r' so
    // we need to remove it from the mergedResult
    for (int i = mergeResult.length() - 1; i >= 0; i--)
    {
      if ('\r' == mergeResult.charAt(i))
      {
        mergeResult.deleteCharAt(i);
      }
    }

    String expectedMerge = TestUtil.readFile(expectedOutput, false);
    String actualMerge = mergeResult.toString();
    File updateFile = null;
    try
    {
      assertEquals("Make sure the line breaks are OK.  The expected merge should have no '\\r'", expectedMerge, actualMerge);
    }
    catch (ComparisonFailure exception)
    {
      File alternative = new File(expectedOutput.toString().replace(".java", "Alt.java"));
      if (alternative.exists())
      {
        expectedMerge = TestUtil.readFile(alternative, false);
        try
        {
          assertEquals("Make sure the line breaks are OK.  The expected merge should have no '\\r'", expectedMerge, actualMerge);
        }
        catch (ComparisonFailure exceptionAlt)
        {
          if (UPDATE_EXPECTED_RESULT_ON_FAILURE)
          {
            updateFile = alternative;
          }
        }
      }
      else
      {
        if (UPDATE_EXPECTED_RESULT_ON_FAILURE)
        {
          updateFile = alternative;
        }

        throw exception;
      }
    }
    finally
    {
      if (updateFile != null)
      {
        OutputStream out = null;
        try
        {
          out = new FileOutputStream(updateFile);
          out.write(actualMerge.getBytes("UTF-8"));
        }
        catch (Exception exception)
        {
        }
        finally
        {
          try
          {
            out.close();
          }
          catch (IOException exception)
          {
          }
        }
      }
    }
  }

  /**
   * If <code>true</code>, editor options are set from default options in genmodel.
   */
  protected boolean applyGenModelEditorFormatting = false;

  protected File dataDirectory = null;

  protected File expectedOutput = null;

  /**
   * URI of merge rules file to be used by {@link JControlModel#initialize(FacadeHelper, String)}
   */
  protected String mergeRulesURI = null;

  protected File source = null;

  protected File target = null;

  public JMergerTest(File dataDirectory)
  {
    this.dataDirectory = dataDirectory;
  }

  /**
   * @param javaVersion (one of {@link JavaCore#VERSION_1_1} to {@link JavaCore#VERSION_1_6}
   */
  @SuppressWarnings({"unchecked", "rawtypes"})
  protected void adjustSourceCompatibility(String javaVersion)
  {
    Hashtable map = JavaCore.getOptions();
    map.put(JavaCore.COMPILER_SOURCE, javaVersion);
    JavaCore.setOptions(map);
  }

  /**
   * Adjusts JavaCore source compatibility options based on {@link #computeJavaVersion()}.
   *
   * @see #adjustSourceCompatibility(String)
   */
  protected void adjustSourceCompatibility()
  {
    String javaVersion = computeJavaVersion();
    assertNotNull("Unable to determine Java version from directory name. ", javaVersion);
    adjustSourceCompatibility(javaVersion);
  }

  /**
   * Determines java version based on the name of the parent of data directory.
   * <p>
   * Parent directory must match one of {@link #DIRECTORY_NAMES_TO_JAVA_VERSIONS}
   * @return java version or <code>null</code> if can not be determined
   */
  protected String computeJavaVersion()
  {
    return computeJavaVersion(dataDirectory);
  }

  protected static String computeJavaVersion(File directory)
  {
    File parentDirectory = directory;
    String javaVersion = null;
    do
    {
      javaVersion = DIRECTORY_NAMES_TO_JAVA_VERSIONS.get(parentDirectory.getName());
      parentDirectory = parentDirectory.getParentFile();
    }
    while (javaVersion == null && parentDirectory != null);
    return javaVersion;
  }

  /**
   * @return test specific expected output file or default expected output file, but never <code>null</code>.
   * @see #getTestSpecificExpectedOutput()
   */
  protected File computeExpectedOutputFile()
  {
    File expectedOutput = getTestSpecificExpectedOutput();
    if (expectedOutput != null && expectedOutput.exists())
    {
      return expectedOutput;
    }
    else
    {
      return new File(dataDirectory, DEFAULT_EXPECTED_OUTPUT_FILENAME).getAbsoluteFile();
    }
  }

  /**
   * Returns unique name for the expected output file.
   * <p>
   * Expected to be overwritten by subclasses.
   * <p>
   * This implementation returns <code>null</code>.
   * @return expected output file, or <code>null</code> if only default file should be used
   */
  protected File getTestSpecificExpectedOutput()
  {
    return null;
  }

  /**
   * @param jControlModel
   */
  @SuppressWarnings("rawtypes")
  protected void applyGenModelEditorFormattingSettings(org.eclipse.emf.codegen.merge.java.JControlModel jControlModel)
  {
    if (EMFPlugin.IS_ECLIPSE_RUNNING)
    {
      Map options = JavaCore.getOptions();
      String tabSize = (String)options.get(DefaultCodeFormatterConstants.FORMATTER_TAB_SIZE);
      String braceStyle = (String)options.get(DefaultCodeFormatterConstants.FORMATTER_BRACE_POSITION_FOR_TYPE_DECLARATION);
      String tabCharacter = (String)options.get(DefaultCodeFormatterConstants.FORMATTER_TAB_CHAR);
      if (JavaCore.TAB.equals(tabCharacter))
      {
        jControlModel.setLeadingTabReplacement("\t");
      }
      else
      {
        String spaces = "";
        for (int i = Integer.parseInt(tabSize); i > 0; --i)
        {
          spaces += " ";
        }
        jControlModel.setLeadingTabReplacement(spaces);
      }
      jControlModel.setConvertToStandardBraceStyle(DefaultCodeFormatterConstants.END_OF_LINE.equals(braceStyle));
    }
  }

  /**
   * @return URI of EMF merge rules resource
   */
  protected String getEMFMergeRulesURI()
  {
    // create model
    GenModel genModel = GenModelFactory.eINSTANCE.createGenModel();

    // create adapter factory
    GeneratorAdapterFactory adapterFactory = GenModelGeneratorAdapterFactory.DESCRIPTOR.createAdapterFactory();
    adapterFactory.setGenerator(new Generator());
    adapterFactory.initialize(genModel);

    // get merge rules URI
    return adapterFactory.getGenerator().getOptions().mergeRulesURI;
  }

  /**
   * @return the expectedOutput
   */
  public File getExpectedOutput()
  {
    return expectedOutput;
  }

  /**
   * @return the mergeRulesURI
   */
  public String getMergeRulesURI()
  {
    return mergeRulesURI;
  }

  /**
   * @param expectedOutput the expectedOutput to set
   */
  public void setExpectedOutput(File expectedOutput)
  {
    this.expectedOutput = expectedOutput;
  }

  /**
   * @param mergeRulesURI the mergeRulesURI to set
   */
  public void setMergeRulesURI(String mergeRulesURI)
  {
    this.mergeRulesURI = mergeRulesURI;
  }

  /**
   * Tests whether the facade helper is of correct type
   * @param facadeHelper
   */
  protected abstract void instanceTest(FacadeHelper facadeHelper);

  /**
   * @return facade helper instance
   */
  protected abstract FacadeHelper instanciateFacadeHelper();

  /**
   * Perform and verify merge. To be used in merge tests by subclasses.
   * <b>
   * Before performing merge, java compiler source version is set based on data directory.
   *
   * @throws Exception
   * @see #adjustSourceCompatibility()
   */
  @Test
  public void merge() throws Exception
  {
    adjustSourceCompatibility();
    verifyMerge(expectedOutput, mergeFiles());
  }

  /**
   * @return contents after merging contents of source and target files
   * @throws Exception
   */
  protected String mergeFiles() throws Exception
  {
    String sourceCompatibility = JavaCore.getOption(JavaCore.COMPILER_SOURCE);

    FacadeHelper facadeHelper = instanciateFacadeHelper();
    instanceTest(facadeHelper);

    JControlModel controlModel = new JControlModel();
    assertFalse(controlModel.canMerge());
    controlModel.initialize(facadeHelper, mergeRulesURI);
    if (applyGenModelEditorFormatting)
    {
      applyGenModelEditorFormattingSettings(controlModel);
    }
    assertTrue(controlModel.canMerge());
    JMerger jMerger = new JMerger(controlModel);

    // set source
    jMerger.setSourceCompilationUnit(jMerger.createCompilationUnitForContents(TestUtil.readFile(source, true)));

    // set target
    if (target.isFile())
    {
      jMerger.setTargetCompilationUnit(jMerger.createCompilationUnitForInputStream(new FileInputStream(target)));
    }

    // merge source and target
    jMerger.merge();

    // Ensure the facade is returning the COMPILER_SOURCE to the original value.
    assertEquals(sourceCompatibility, JavaCore.getOption(JavaCore.COMPILER_SOURCE));

    return jMerger.getTargetCompilationUnitContents();
  }

  /**
   * Sets up data directory, source, target, expected output, and merge rules attributes.
   * <p>
   * If <code>merge.xml</code> is not available in data directory, default EMF merge rules are used.
   *
   * @see #computeExpectedOutputFile()
   * @see #getEMFMergeRulesURI()
   */
  @Before
  public void setUp() throws Exception
  {
    assertTrue("Merge Data directory is not available - " + dataDirectory.getAbsolutePath(), dataDirectory.isDirectory());

    if (mergeRulesURI == null)
    {
      File mergeXML = new File(dataDirectory, "merge.xml").getAbsoluteFile();
      if (!mergeXML.exists())
      {
        mergeRulesURI = getEMFMergeRulesURI();
      }
      else
      {
        mergeRulesURI = mergeXML.getAbsolutePath();
      }
    }

    source = new File(dataDirectory, "MergerSource.java").getAbsoluteFile();
    assertTrue("Merge Source file is not available - " + source.getAbsolutePath(), source.isFile());

    target = new File(dataDirectory, "MergerTarget.java").getAbsoluteFile();

    if (expectedOutput == null)
    {
      expectedOutput = computeExpectedOutputFile();
    }
    assertTrue("Merge Result file is not available - " + expectedOutput.getAbsolutePath(), expectedOutput.isFile());
  }

  /**
   * Filter for the directories that will be used as data directories for tests.
   */
  protected static class JMergerDataDirectoryFilter implements FilenameFilter
  {
    protected static final Pattern EXCLUDE_DATA_DIRECTORY_NAME_PATTERN = Pattern.compile("^(?:CVS)|(?:\\..*)$");
    protected static final Pattern INCLUDE_DATA_DIRECTORY_NAME_PATTERN = Pattern.compile(".*");
    // use something like this line to restrict the test
    // protected static final Pattern INCLUDE_DATA_DIRECTORY_NAME_PATTERN = Pattern.compile("(bugzilla|178183)");

    public boolean accept(File dir, String name)
    {
      // must be a directory and must match the pattern
      File dataDirectoryCandidate = new File(dir, name);
      return dataDirectoryCandidate.isDirectory() &&
        !EXCLUDE_DATA_DIRECTORY_NAME_PATTERN.matcher(name).matches() &&
        INCLUDE_DATA_DIRECTORY_NAME_PATTERN.matcher(name).matches();
    }
  }

  private static final FilenameFilter FILTER = new JMergerDataDirectoryFilter();

  /**
   * Populates suite with test cases for each data directory.
   * @param prefix
   */
  private static Collection<Object[]> populateSuite(String prefix)
  {
    assertTrue("Directory " + DEFAULT_ROOT_DIRECTORY.getAbsolutePath() + " does not exist.", DEFAULT_ROOT_DIRECTORY.exists());
    assertTrue("Directory " + DEFAULT_ROOT_DIRECTORY.getAbsolutePath() + " is not a directory.", DEFAULT_ROOT_DIRECTORY.isDirectory());

    Collection<Object[]> tests = new ArrayList<Object[]>();
    // Loop for all directories of all java versions.
    for (String javaVersionDirectoryName : JMergerTest.DIRECTORY_NAMES_TO_JAVA_VERSIONS.keySet())
    {
      File dataDirsDirectory = new File(DEFAULT_ROOT_DIRECTORY, javaVersionDirectoryName);
      if (dataDirsDirectory.isDirectory())
      {
        createTestSuiteRecursively(prefix, tests, dataDirsDirectory, "");
      }
    }

    assertFalse("Subdirectories " + JMergerTest.DIRECTORY_NAMES_TO_JAVA_VERSIONS.keySet().toString() + " under "
      + DEFAULT_ROOT_DIRECTORY.getAbsolutePath() + " must contain subdirectories with source, target and output files.", tests.isEmpty());

    return tests;
  }

  /**
   * Creates a test suite recursively for all directories in the directory tree.
   * Directories used as input must be accepted by {@link JMergerDataDirectoryFilter}.
   * @param prefix
   * @param rootDirectory root directory to create test suite for
   */
  private static void createTestSuiteRecursively(String prefix, Collection<Object[]> result, File rootDirectory, String subDirectory)
  {
    // If there are no test cases for this directory, try to create test suites from subdirectories.
    Object[] test = createSingleInputTestSuite(prefix, rootDirectory, subDirectory);
    if (test == null)
    {
      String[] dataDirectoriesNames = new File(rootDirectory, subDirectory).list(FILTER);
      if (dataDirectoriesNames.length > 0)
      {
        Arrays.sort(dataDirectoriesNames);
        for (String dataDirectoryName : dataDirectoriesNames)
        {
          String subSubDirectory = subDirectory.length() == 0 ? dataDirectoryName : subDirectory + "/" + dataDirectoryName;
          File dataDirectory = new File(rootDirectory, subSubDirectory);
          if (!prefix.equals("JDOM") || !"1.5".equals(computeJavaVersion(dataDirectory)))
          {
            createTestSuiteRecursively(prefix, result, rootDirectory, subSubDirectory);
          }
        }
      }
    }
    else
    {
      result.add(test);
    }
  }

  /**
   * Creates and returns test suite for a single input directory, or null if it's not appropriate for the prefix.
   */
  private static Object[] createSingleInputTestSuite(String prefix, File rootDirectory, String subDirectory)
  {
    File dataDirectory = new File(rootDirectory, subDirectory);
    if (new File(dataDirectory, "MergerExpected.java").isFile() || new File(dataDirectory, prefix + "MergerExpected.java").isFile())
    {
      return new Object[] { subDirectory, dataDirectory };
    }
    else
    {
      return null;
    }
  }
}