package org.eclipse.emf.examples.jet.article2.codegen;


import java.io.ByteArrayInputStream;
import java.io.InputStream;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.ResourceAttributes;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.Status;

import org.eclipse.emf.codegen.jet.JETEmitter;
import org.eclipse.emf.codegen.jet.JETException;
import org.eclipse.emf.codegen.merge.java.JControlModel;
import org.eclipse.emf.codegen.merge.java.JMerger;
import org.eclipse.emf.codegen.util.CodeGenUtil;
import org.eclipse.emf.common.util.BasicMonitor;
import org.eclipse.emf.common.util.DiagnosticException;
import org.eclipse.emf.common.util.Monitor;


/**
 * This class encapsulates access to the JET and JMerge packages.
 * 
 * @author Remko Popma
 * @version $Revision: 1.5 $ ($Date: 2006/12/29 21:12:36 $)
 */
public class JETGateway
{

  private Config mConfig = null;

  public JETGateway(Config config)
  {
    mConfig = config;
  }

  /**
   * Invokes the JET template specified in the <code>Config</code> with the
   * model specified in the <code>Config</code> and returns the generated text
   * as a String.
   * <p>
   * This implementation uses a <code>JETEmitter</code> to translate the
   * template to a Java implementation class. The translated class is created in
   * a hidden project called <code>.JETEmitters</code>.
   * <p>
   * In order to be able to compile the translated template implementation
   * class, the classes used by the model specified in the <code>Config</code>
   * must be available in the classpath. For this reason, this method sets the
   * first runtime library of the plugin specified in the <code>Config</code>
   * as a classpath variable to the <code>.JETEmitters</code> project.
   * 
   * @param monitor
   *          the progress monitor to use. May be <code>null</code>.
   * @return the source code text generated by the JET template
   * @throws CoreException
   */
  public String generate(IProgressMonitor monitor) throws CoreException
  {
    try
    {
      monitor = createIfNull(monitor);
      Config config = getConfig();
      JETEmitter emitter = new JETEmitter(config.getTemplateFullUri(), getClass().getClassLoader());
      emitter.addVariable(config.getClasspathVariable(), config.getPluginId());
  
      Monitor sub = new BasicMonitor.EclipseSubProgress(monitor, 1);
      String result = emitter.generate(sub, new Object []{ config.getModel() });
      monitor.worked(1);
      return result;
    }
    catch (JETException exception)
    {
      throw DiagnosticException.toCoreException(exception);
    }
  }

  /**
   * Merges the specified emitterResult with the contents of an existing file
   * and returns the result. The existing file is not modified.
   * <p>
   * The location of the file to merge with is found by finding or creating the
   * container (folder) for the <code>Config</code>'s package in the
   * <code>Config</code>'s target folder. The name of the file to merge with
   * is the <code>Config</code>'s target file.
   * 
   * @param monitor
   *          the progress monitor to use. May be <code>null</code>.
   * @param emitterResult
   *          generated content to merge with the existing content
   * @return the result of merging the specified generated contents with the
   *         existing file
   * @throws CoreException
   *           if an error occurs accessing the contents of the file
   */
  public String merge(IProgressMonitor monitor, String emitterResult) throws CoreException
  {
    monitor = createIfNull(monitor);

    Config config = getConfig();
    IContainer container = findOrCreateContainer(monitor, config.getTargetFolder(), config.getPackageName());
    if (container == null)
    {
      throw 
        new CoreException
          (new Status
             (IStatus.ERROR,
              "org.eclipse.emf.examples.jet.article2",
              0,
              "Could not find or create container for package " + config.getPackageName() + " in " + config.getTargetFolder(),
              null));
    }

    IFile targetFile = container.getFile(new Path(config.getTargetFile()));
    if (!targetFile.exists())
    {
      monitor.worked(1);
      return emitterResult;
    }

    JControlModel jControlModel = new JControlModel();
    jControlModel.initialize(CodeGenUtil.instantiateFacadeHelper(JMerger.DEFAULT_FACADE_HELPER_CLASS), config.getMergeXmlFullUri());
    JMerger jMerger = new JMerger(jControlModel);
    jMerger.setSourceCompilationUnit(jMerger.createCompilationUnitForContents(emitterResult));

    jMerger.setTargetCompilationUnit(jMerger.createCompilationUnitForInputStream(targetFile.getContents(true)));
    String oldContents = jControlModel.getFacadeHelper().getOriginalContents(jMerger.getTargetCompilationUnit());

    jMerger.merge();
    monitor.worked(1);

    String result = jMerger.getTargetCompilationUnitContents();
    if (oldContents.equals(result))
    {
      return result;
    }

    if (!targetFile.isReadOnly())
    {
      return result;
    }

    // The file may be read-only because it is checked out
    // by a VCM component. Here we ask permission to change the file.
    if (targetFile.getWorkspace().validateEdit(new IFile []{ targetFile }, BasicMonitor.subProgress(monitor, 1)).isOK())
    {

      jMerger.setTargetCompilationUnit(jMerger.createCompilationUnitForInputStream(targetFile.getContents(true)));
      jMerger.remerge();
      return jMerger.getTargetCompilationUnitContents();
    }
    return result;
  }

  /**
   * Saves the specified contents to a location specified by the
   * <code>Config</code> settings. The location of the file to save is found
   * by finding or creating the container (folder) for the <code>Config</code>
   * 's package in the <code>Config</code>'s target folder. The name of the
   * file to save is the <code>Config</code>'s target file.
   * 
   * @param monitor
   *          the progress monitor to use. May be <code>null</code>.
   * @param contents
   *          the byte contents of the file to save
   * @throws CoreException
   */
  public IFile save(IProgressMonitor monitor, byte[] contents) throws CoreException
  {
    monitor = createIfNull(monitor);

    Config config = getConfig();
    IContainer container = findOrCreateContainer(monitor, config.getTargetFolder(), config.getPackageName());
    if (container == null)
    {
      throw 
        new CoreException
          (new Status
             (IStatus.ERROR,
              "org.eclipse.emf.examples.jet.article2",
              0,
              "Could not find or create container for package " + config.getPackageName() + " in " + config.getTargetFolder(),
              null));
    }
    IFile targetFile = container.getFile(new Path(config.getTargetFile()));
    IFile result = getWritableTargetFile(targetFile, container, config.getTargetFile());

    InputStream newContents = new ByteArrayInputStream(contents);
    if (result.exists())
    {
      result.setContents(newContents, true, true, BasicMonitor.subProgress(monitor, 1));
    }
    else
    {
      result.create(newContents, true, BasicMonitor.subProgress(monitor, 1));
    }
    return result;
  }

  /**
   * Returns a non-null progress monitor.
   * 
   * @param monitor
   *          an existing progress monitor
   * @return a new <code>NullProgressMonitor</code> if the specified monitor
   *         is <code>null</code>, or the existing monitor otherwise
   */
  private IProgressMonitor createIfNull(IProgressMonitor monitor)
  {
    if (monitor == null)
    {
      return new NullProgressMonitor();
    }
    return monitor;
  }

  private IContainer findOrCreateContainer(IProgressMonitor progressMonitor, String targetDirectory, String packageName) throws CoreException
  {

    IPath outputPath = new Path(targetDirectory + "/" + packageName.replace('.', '/'));
    progressMonitor.beginTask("", 4);

    IProgressMonitor sub = BasicMonitor.subProgress(progressMonitor, 1);
    IPath localLocation = null; // use default
    IContainer container = CodeGenUtil.EclipseUtil.findOrCreateContainer(outputPath, true, localLocation, sub);
    return container;
  }

  /**
   * Returns a <code>IFile</code> that can be written to. If the specified
   * file is read-write, it is returned unchanged. If the specified file is
   * read-only and {@link Config#isForceOverwrite()}returns <code>true</code>,
   * the file is made writable, otherwise a new file is returned in the
   * specified container with filename <code>"." + fileName + ".new"</code>.
   * 
   * @param container
   *          container to create the new file in if the specified file cannot
   *          be made writable
   * @param targetFile
   *          the file to make writable
   * @param fileName
   *          used to create a new file name if the specified file cannot be
   *          made writable
   * @return a <code>IFile</code> that can be written to
   */
  private IFile getWritableTargetFile(IFile targetFile, IContainer container, String fileName)
  {
    if (targetFile.isReadOnly())
    {
      if (getConfig().isForceOverwrite())
      {
        ResourceAttributes attributes = targetFile.getResourceAttributes();
        attributes.setReadOnly(false);
        try
        {
          targetFile.setResourceAttributes(attributes);
        }
        catch (CoreException e)
        {
          // Ignore
        }
      }
      else
      {
        targetFile = container.getFile(new Path("." + fileName + ".new"));
      }
    }
    return targetFile;
  }

  /**
   * Returns the <code>Config</code> object
   * 
   * @return the <code>Config</code> object
   */
  protected Config getConfig()
  {
    return mConfig;
  }
}