// Copyright (c) 2010 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.debug.ui.actions;

import java.io.IOException;
import java.util.Iterator;
import java.util.List;

import org.chromium.debug.core.model.DebugTargetImpl;
import org.chromium.debug.core.model.JavaScriptFormatter;
import org.chromium.debug.core.model.StringMappingData;
import org.chromium.debug.core.model.ConnectedTargetData;
import org.chromium.debug.core.model.VmResource;
import org.chromium.debug.core.model.WorkspaceBridge;
import org.chromium.debug.core.sourcemap.SourcePositionMapBuilder;
import org.chromium.debug.core.sourcemap.TextSectionMapping;
import org.chromium.debug.core.sourcemap.TextSectionMappingImpl;
import org.chromium.debug.core.util.ChromiumDebugPluginUtil;
import org.chromium.debug.ui.JsDebugModelPresentation;
import org.chromium.debug.ui.actions.FileBasedAction.FileFilter;
import org.chromium.debug.ui.editors.JsEditor;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.part.IShowInTarget;
import org.eclipse.ui.part.ShowInContext;

/**
 * An action delegate for "Temporarily format source". It formats downloaded JavaScript source,
 * i.e. a file under virtual project and puts the result into another file under virtual project.
 * It also registers a mapping between these 2 files.
 * <p>Additionally the action may work in another mode: "Delete formatted source". This mode
 * gets enabled when user selection contains files created in "Temporarily format source".
 */
public class TemporarilyFormatSourceAction
    extends FileBasedAction.Multiple<TemporarilyFormatSourceAction.ResourceData> {
  public TemporarilyFormatSourceAction() {
    super(RESOURCE_FILTER);
  }

  private static final FileFilter<ResourceData> RESOURCE_FILTER =
      new FileFilter<ResourceData>() {
    @Override
    ResourceData accept(IFile file) {
      for (ConnectedTargetData targetData : DebugTargetImpl.getAllConnectedTargetDatas()) {
        VmResource vmResource = targetData.getWorkspaceRelations().getVProjectVmResource(file);
        if (vmResource == null) {
          continue;
        }
        return new ResourceData(file, vmResource, targetData);
      }
      return null;
    }
  };

  @Override
  protected ActionRunnable createRunnable(
      final List<? extends ResourceData> resourceDataList) {

    // Choose between two modes of the action: actually format or delete already formatted file
    // (i.e. undo one of previous actions).
    Iterator<? extends ResourceData> iterator = resourceDataList.iterator();
    if (!iterator.hasNext()) {
      return null;
    }

    ResourceData firstElement = iterator.next();
    ActionMode firstElementMode = checkActionMode(firstElement);
    if (firstElementMode == ActionMode.UNAPPLICABLE) {
      return null;
    }
    while (iterator.hasNext()) {
      ResourceData otherElement = iterator.next();
      ActionMode otherElementMode = checkActionMode(otherElement);
      if (otherElementMode == ActionMode.UNAPPLICABLE) {
        return null;
      }
      if (otherElementMode != firstElementMode) {
        return null;
      }
    }

    if (firstElementMode == ActionMode.FORMAT) {
      JavaScriptFormatter formatter = JavaScriptFormatter.Access.getInstance();
      if (formatter == null) {
        return noFormatterActionStub;
      } else {
        return new FormatActionRunnable(resourceDataList, formatter);
      }
    } else {
      return new DeleteFormattedActionRunnable(resourceDataList);
    }
  }

  private abstract class ActionRunnableImpl implements ActionRunnable {
    private final List<? extends ResourceData> resourceDataList;

    ActionRunnableImpl(List<? extends ResourceData> resourceDataList) {
      this.resourceDataList = resourceDataList;
    }

    public void run(final Shell shell, final IWorkbenchPart workbenchPart) {
      Job job = new Job(getAction().getText()) {
        @Override
        protected IStatus run(IProgressMonitor monitor) {
          for (int i = 0; i < resourceDataList.size(); i++) {
            ResourceData resourceData = resourceDataList.get(i);
            try {
              runInWorkerThread(resourceData, shell, workbenchPart, i == 0);
            } catch (CoreException e) {
              throw new RuntimeException(e);
            } catch (IOException e) {
              throw new RuntimeException(e);
            }
          }
          return Status.OK_STATUS;
        }
      };
      job.schedule();
    }

    protected abstract void runInWorkerThread(ResourceData resourceData, Shell shell,
        IWorkbenchPart workbenchPart, boolean highlightResult) throws CoreException, IOException;
  }

  private class FormatActionRunnable extends ActionRunnableImpl {
    private final JavaScriptFormatter formatter;

    FormatActionRunnable(List<? extends ResourceData> resourceDataList,
        JavaScriptFormatter formatter) {
      super(resourceDataList);
      this.formatter = formatter;
    }

    public void adjustAction() {
      restoreActionText();
    }

    @Override
    protected void runInWorkerThread(ResourceData data, Shell shell, IWorkbenchPart workbenchPart,
        boolean highlightResult) throws CoreException, IOException {
      byte[] sourceBytes = ChromiumDebugPluginUtil.readFileContents(data.getFile());
      String sourceString = new String(sourceBytes);

      JavaScriptFormatter.Result result = formatter.format(sourceString);

      WorkspaceBridge workspaceRelations = data.getConnectedTargetData().getWorkspaceRelations();

      String proposedFileName = data.getVmResource().getLocalVisibleFileName() +
          Messages.TemporarilyFormatSourceAction_FORMATTER_SUFFIX;

      MetadataImpl metadata = new MetadataImpl();
      VmResource formattedResource =
          workspaceRelations.createTemporaryFile(metadata, proposedFileName);

      SourcePositionMapBuilder.MappingHandle mappingHandle;
      try {
        SourcePositionMapBuilder builder =
            data.getConnectedTargetData().getSourcePositionMapBuilder();

        // Unformatted text is a VM text.
        StringMappingData vmTextData = result.getInputTextData();

        // Formatter text is *like* original text in our case.
        StringMappingData originalTextData = result.getFormattedTextData();

        SourcePositionMapBuilder.ResourceSection vmResourceSection =
            new SourcePositionMapBuilder.ResourceSection(data.getVmResource().getId(), 0, 0,
                vmTextData.getEndLine(), vmTextData.getEndColumn());

        SourcePositionMapBuilder.ResourceSection originalResourceSection =
            new SourcePositionMapBuilder.ResourceSection(formattedResource.getId(), 0, 0,
                originalTextData.getEndLine(), originalTextData.getEndColumn());

        TextSectionMapping mapTable =
            new TextSectionMappingImpl(originalTextData, vmTextData);

        mappingHandle = builder.addMapping(originalResourceSection, vmResourceSection, mapTable);
      } catch (SourcePositionMapBuilder.CannotAddException e) {
        formattedResource.deleteResourceAndFile();
        throw new RuntimeException(e);
      }
      metadata.mappingHandle = mappingHandle;

      ChromiumDebugPluginUtil.writeFile(formattedResource.getVProjectFile(),
          result.getFormattedText());

      if (highlightResult) {
        highlightResult(formattedResource, workbenchPart, shell);
      }
    }

    private void highlightResult(final VmResource formattedResource,
        final IWorkbenchPart workbenchPart, Shell shell) {
      shell.getDisplay().asyncExec(new Runnable() {
        public void run() {
          if (workbenchPart instanceof IShowInTarget) {
            IShowInTarget showInTarget = (IShowInTarget) workbenchPart;
            ShowInContext showInContext =
                new ShowInContext(formattedResource.getVProjectFile(), null);
            showInTarget.show(showInContext);
          } else {
            openFileInEditorAsync(formattedResource, workbenchPart.getSite().getWorkbenchWindow());
          }
        }
      });
    }

    private void openFileInEditorAsync(VmResource formattedResource,
        IWorkbenchWindow workbenchWindow) {
      IEditorInput input =
          JsDebugModelPresentation.toEditorInput(formattedResource.getVProjectFile());
      try {
        workbenchWindow.getActivePage().openEditor(input, JsEditor.EDITOR_ID);
      } catch (PartInitException e) {
        throw new RuntimeException(e);
      }
    }
  }

  private class DeleteFormattedActionRunnable extends ActionRunnableImpl {
    DeleteFormattedActionRunnable(List<? extends ResourceData> resourceDataList) {
      super(resourceDataList);
    }

    public void adjustAction() {
      modifyActionText(Messages.TemporarilyFormatSourceAction_DELETE_FORMATTER_ACTION_NAME);
    }

    @Override
    protected void runInWorkerThread(ResourceData resourceData, Shell shell,
        IWorkbenchPart workbenchPart, boolean highlightResult)
        throws CoreException, IOException {
      MetadataImpl metadataImpl = (MetadataImpl) resourceData.getVmResource().getMetadata();
      metadataImpl.mappingHandle.delete();
      resourceData.getVmResource().deleteResourceAndFile();
    }
  }

  private final ActionRunnable noFormatterActionStub = new ActionRunnable() {
    public void adjustAction() {
      restoreActionText();
      String baseText = getAction().getText();
      modifyActionText(baseText +
          Messages.TemporarilyFormatSourceAction_NO_FORMATTER_DISABLED_SUFFIX);
      getAction().setEnabled(false);
    }

    public void run(Shell shell, IWorkbenchPart workbenchPart) {
      // Should be unreachable. Do nothing.
    }
  };

  /**
   * Depending on user selection, the action may work in 2 modes: format or
   * delete formatted.
   */
  private enum ActionMode {
    FORMAT, DELETE_FORMATTED, UNAPPLICABLE
  }

  private ActionMode checkActionMode(ResourceData data) {
    Object metadata = data.getVmResource().getMetadata();
    if (metadata instanceof VmResource.ScriptHolder) {
      return ActionMode.FORMAT;
    }
    if (metadata instanceof MetadataImpl) {
      return ActionMode.DELETE_FORMATTED;
    }
    return ActionMode.UNAPPLICABLE;
  }

  static class ResourceData {
    private final IFile file;
    private final VmResource vmResource;
    private final ConnectedTargetData connectedTargetData;

    ResourceData(IFile file, VmResource vmResource, ConnectedTargetData connectedTargetData) {
      this.file = file;
      this.vmResource = vmResource;
      this.connectedTargetData = connectedTargetData;
    }

    IFile getFile() {
      return file;
    }

    public VmResource getVmResource() {
      return vmResource;
    }

    public ConnectedTargetData getConnectedTargetData() {
      return connectedTargetData;
    }
  }

  /**
   * Keeps the data that helps us undo formatting later.
   */
  private static class MetadataImpl implements VmResource.Metadata {
    SourcePositionMapBuilder.MappingHandle mappingHandle;
  }
}
