/*
 * Copyright 2000-2015 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.intellij.diff.tools.external;

import com.intellij.diff.contents.*;
import com.intellij.diff.merge.MergeResult;
import com.intellij.diff.merge.ThreesideMergeRequest;
import com.intellij.diff.util.DiffUserDataKeysEx;
import com.intellij.diff.util.Side;
import com.intellij.diff.util.ThreeSide;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.configurations.GeneralCommandLine;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.CharsetToolkit;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileWithoutContent;
import com.intellij.util.ArrayUtil;
import com.intellij.util.LineSeparator;
import com.intellij.util.PathUtil;
import com.intellij.util.TimeoutUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.hash.HashMap;
import com.intellij.util.execution.ParametersListUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;

public class ExternalDiffToolUtil {
  public static boolean canCreateFile(@NotNull DiffContent content) {
    if (content instanceof EmptyContent) return true;
    if (content instanceof DocumentContent) return true;
    if (content instanceof FileContent) {
      VirtualFile file = ((FileContent)content).getFile();
      if (file instanceof VirtualFileWithoutContent) return false;
      return true;
    }
    if (content instanceof DirectoryContent) return ((DirectoryContent)content).getFile().isInLocalFileSystem();
    return false;
  }

  @NotNull
  private static InputFile createFile(@NotNull DiffContent content, @NotNull FileNameInfo fileName)
    throws IOException {

    if (content instanceof EmptyContent) {
      return new TempInputFile(createFile(new byte[0], fileName));
    }
    else if (content instanceof FileContent) {
      VirtualFile file = ((FileContent)content).getFile();

      Document document = FileDocumentManager.getInstance().getCachedDocument(file);
      if (document != null) {
        FileDocumentManager.getInstance().saveDocument(document);
      }

      if (file.isInLocalFileSystem()) {
        return new LocalInputFile(file);
      }

      return new TempInputFile(createTempFile(file, fileName));
    }
    else if (content instanceof DocumentContent) {
      return new TempInputFile(createTempFile((DocumentContent)content, fileName));
    }
    else if (content instanceof DirectoryContent) {
      VirtualFile file = ((DirectoryContent)content).getFile();
      if (file.isInLocalFileSystem()) {
        return new LocalInputFile(file);
      }

      throw new IllegalArgumentException(content.toString());
    }

    throw new IllegalArgumentException(content.toString());
  }

  @NotNull
  private static File createTempFile(@NotNull final DocumentContent content, @NotNull FileNameInfo fileName) throws IOException {
    FileDocumentManager.getInstance().saveDocument(content.getDocument());

    LineSeparator separator = content.getLineSeparator();
    if (separator == null) separator = LineSeparator.getSystemLineSeparator();

    Charset charset = content.getCharset();
    if (charset == null) charset = Charset.defaultCharset();

    Boolean hasBom = content.hasBom();
    if (hasBom == null) hasBom = CharsetToolkit.getMandatoryBom(charset) != null;

    String contentData = ReadAction.compute(() -> {
      return content.getDocument().getText();
    });
    if (separator != LineSeparator.LF) {
      contentData = StringUtil.convertLineSeparators(contentData, separator.getSeparatorString());
    }

    byte[] bytes = contentData.getBytes(charset);

    byte[] bom = hasBom ? CharsetToolkit.getPossibleBom(charset) : null;
    if (bom != null) {
      bytes = ArrayUtil.mergeArrays(bom, bytes);
    }

    return createFile(bytes, fileName);
  }

  @NotNull
  private static File createTempFile(@NotNull VirtualFile file, @NotNull FileNameInfo fileName) throws IOException {
    byte[] bytes = file.contentsToByteArray();
    return createFile(bytes, fileName);
  }

  @NotNull
  private static OutputFile createOutputFile(@NotNull DiffContent content, @NotNull FileNameInfo fileName) throws IOException {
    if (content instanceof FileContent) {
      VirtualFile file = ((FileContent)content).getFile();

      Document document = FileDocumentManager.getInstance().getCachedDocument(file);
      if (document != null) {
        FileDocumentManager.getInstance().saveDocument(document);
      }

      if (file.isInLocalFileSystem()) {
        return new LocalOutputFile(file);
      }

      File tempFile = createTempFile(file, fileName);
      return new NonLocalOutputFile(file, tempFile);
    }
    else if (content instanceof DocumentContent) {
      File tempFile = createTempFile(((DocumentContent)content), fileName);
      return new DocumentOutputFile(((DocumentContent)content).getDocument(), ((DocumentContent)content).getCharset(), tempFile);
    }
    throw new IllegalArgumentException(content.toString());
  }

  @NotNull
  private static File createFile(@NotNull byte[] bytes, @NotNull FileNameInfo fileName) throws IOException {
    File tempFile = FileUtil.createTempFile(fileName.prefix + "_", "_" + fileName.name, true);
    FileUtil.writeToFile(tempFile, bytes);
    return tempFile;
  }

  public static void execute(@NotNull ExternalDiffSettings settings,
                             @NotNull List<? extends DiffContent> contents,
                             @NotNull List<String> titles,
                             @Nullable String windowTitle)
    throws IOException, ExecutionException {
    assert contents.size() == 2 || contents.size() == 3;
    assert titles.size() == contents.size();

    List<InputFile> files = new ArrayList<>();
    for (int i = 0; i < contents.size(); i++) {
      DiffContent content = contents.get(i);
      FileNameInfo fileName = FileNameInfo.create(contents, titles, windowTitle, i);
      files.add(createFile(content, fileName));
    }

    Map<String, String> patterns = ContainerUtil.newHashMap();
    if (files.size() == 2) {
      patterns.put("%1", files.get(0).getPath());
      patterns.put("%2", files.get(1).getPath());
      patterns.put("%3", "");
    }
    else {
      patterns.put("%1", files.get(0).getPath());
      patterns.put("%2", files.get(2).getPath());
      patterns.put("%3", files.get(1).getPath());
    }


    execute(settings.getDiffExePath(), settings.getDiffParameters(), patterns);
  }

  public static void executeMerge(@Nullable Project project,
                                  @NotNull ExternalDiffSettings settings,
                                  @NotNull ThreesideMergeRequest request)
    throws IOException, ExecutionException {
    boolean success = false;
    OutputFile outputFile = null;
    List<InputFile> inputFiles = new ArrayList<>();
    try {
      DiffContent outputContent = request.getOutputContent();
      List<? extends DiffContent> contents = request.getContents();
      List<String> titles = request.getContentTitles();
      String windowTitle = request.getTitle();

      assert contents.size() == 3;
      assert titles.size() == contents.size();

      for (int i = 0; i < contents.size(); i++) {
        DiffContent content = contents.get(i);
        FileNameInfo fileName = FileNameInfo.create(contents, titles, windowTitle, i);
        inputFiles.add(createFile(content, fileName));
      }

      outputFile = createOutputFile(outputContent, FileNameInfo.createMergeResult(outputContent, windowTitle));

      Map<String, String> patterns = new HashMap<>();
      patterns.put("%1", inputFiles.get(0).getPath());
      patterns.put("%2", inputFiles.get(2).getPath());
      patterns.put("%3", inputFiles.get(1).getPath());
      patterns.put("%4", outputFile.getPath());

      final Process process = execute(settings.getMergeExePath(), settings.getMergeParameters(), patterns);

      if (settings.isMergeTrustExitCode()) {
        final Ref<Boolean> resultRef = new Ref<>();

        ProgressManager.getInstance().run(new Task.Modal(project, "Waiting for External Tool", true) {
          @Override
          public void run(@NotNull ProgressIndicator indicator) {
            final Semaphore semaphore = new Semaphore(0);

            final Thread waiter = new Thread("external process waiter") {
              @Override
              public void run() {
                try {
                  resultRef.set(process.waitFor() == 0);
                }
                catch (InterruptedException ignore) {
                }
                finally {
                  semaphore.release();
                }
              }
            };
            waiter.start();

            try {
              while (true) {
                indicator.checkCanceled();
                if (semaphore.tryAcquire(200, TimeUnit.MILLISECONDS)) break;
              }
            }
            catch (InterruptedException ignore) {
            }
            finally {
              waiter.interrupt();
            }
          }
        });

        success = resultRef.get() == Boolean.TRUE;
      }
      else {
        ProgressManager.getInstance().run(new Task.Modal(project, "Launching External Tool", false) {
          @Override
          public void run(@NotNull ProgressIndicator indicator) {
            indicator.setIndeterminate(true);
            TimeoutUtil.sleep(1000);
          }
        });

        success = Messages.showYesNoDialog(project,
                                           "Press \"Mark as Resolved\" when you finish resolving conflicts in the external tool",
                                           "Merge In External Tool", "Mark as Resolved", "Revert", null) == Messages.YES;
      }

      if (success) outputFile.apply();
    }
    finally {
      request.applyResult(success ? MergeResult.RESOLVED : MergeResult.CANCEL);

      if (outputFile != null) outputFile.cleanup();
      for (InputFile file : inputFiles) {
        file.cleanup();
      }
    }
  }

  @NotNull
  private static Process execute(@NotNull String exePath, @NotNull String parametersTemplate, @NotNull Map<String, String> patterns)
    throws ExecutionException {
    List<String> parameters = ParametersListUtil.parse(parametersTemplate, true);

    List<String> from = new ArrayList<>();
    List<String> to = new ArrayList<>();
    for (Map.Entry<String, String> entry : patterns.entrySet()) {
      from.add(entry.getKey());
      to.add(entry.getValue());
    }

    List<String> args = new ArrayList<>();
    for (String parameter : parameters) {
      String arg = StringUtil.replace(parameter, from, to);
      if (!StringUtil.isEmptyOrSpaces(arg)) args.add(arg);
    }

    GeneralCommandLine commandLine = new GeneralCommandLine();
    commandLine.setExePath(exePath);
    commandLine.addParameters(args);
    return commandLine.createProcess();
  }

  //
  // Helpers
  //


  private interface InputFile {
    @NotNull
    String getPath();

    void cleanup();
  }

  private interface OutputFile extends InputFile {
    void apply() throws IOException;
  }

  private static class LocalOutputFile extends LocalInputFile implements OutputFile {
    public LocalOutputFile(@NotNull VirtualFile file) {
      super(file);
    }

    @Override
    public void apply() {
      myFile.refresh(false, false);
    }
  }

  private static class NonLocalOutputFile extends TempInputFile implements OutputFile {
    @NotNull private final VirtualFile myFile;

    public NonLocalOutputFile(@NotNull VirtualFile file, @NotNull File localFile) {
      super(localFile);
      myFile = file;
    }

    @Override
    public void apply() throws IOException {
      myFile.setBinaryContent(FileUtil.loadFileBytes(myLocalFile));
    }
  }

  private static class DocumentOutputFile extends TempInputFile implements OutputFile {
    @NotNull private final Document myDocument;
    @NotNull private final Charset myCharset;

    public DocumentOutputFile(@NotNull Document document, @Nullable Charset charset, @NotNull File localFile) {
      super(localFile);
      myDocument = document;
      // TODO: potentially dangerous operation - we're using default charset
      myCharset = charset != null ? charset : Charset.defaultCharset();
    }

    @Override
    public void apply() throws IOException {
      final String content = StringUtil.convertLineSeparators(FileUtil.loadFile(myLocalFile, myCharset));
      ApplicationManager.getApplication().runWriteAction(() -> {
        myDocument.setText(content);
      });
    }
  }

  private static class LocalInputFile implements InputFile {
    @NotNull protected final VirtualFile myFile;

    public LocalInputFile(@NotNull VirtualFile file) {
      myFile = file;
    }

    @NotNull
    @Override
    public String getPath() {
      return myFile.getPath();
    }

    @Override
    public void cleanup() {
    }
  }

  private static class TempInputFile implements InputFile {
    @NotNull protected final File myLocalFile;

    public TempInputFile(@NotNull File localFile) {
      myLocalFile = localFile;
    }

    @NotNull
    @Override
    public String getPath() {
      return myLocalFile.getPath();
    }

    @Override
    public void cleanup() {
      FileUtil.delete(myLocalFile);
    }
  }

  private static class FileNameInfo {
    @NotNull public final String prefix;
    @NotNull public final String name;

    public FileNameInfo(@NotNull String prefix, @NotNull String name) {
      this.prefix = prefix;
      this.name = name;
    }

    @NotNull
    public static FileNameInfo create(@NotNull List<? extends DiffContent> contents,
                                      @NotNull List<String> titles,
                                      @Nullable String windowTitle,
                                      int index) {
      if (contents.size() == 2) {
        Side side = Side.fromIndex(index);
        DiffContent content = side.select(contents);
        String title = side.select(titles);
        String prefix = side.select("before", "after");

        String name = getFileName(content, title, windowTitle);
        return new FileNameInfo(prefix, name);
      }
      else if (contents.size() == 3) {
        ThreeSide side = ThreeSide.fromIndex(index);
        DiffContent content = side.select(contents);
        String title = side.select(titles);
        String prefix = side.select("left", "base", "right");

        String name = getFileName(content, title, windowTitle);
        return new FileNameInfo(prefix, name);
      }
      else {
        throw new IllegalArgumentException(String.valueOf(contents.size()));
      }
    }

    @NotNull
    public static FileNameInfo createMergeResult(@NotNull DiffContent content, @Nullable String windowTitle) {
      String name = getFileName(content, null, windowTitle);
      return new FileNameInfo("merge_result", name);
    }

    @NotNull
    private static String getFileName(@NotNull DiffContent content,
                                      @Nullable String title,
                                      @Nullable String windowTitle) {
      if (content instanceof EmptyContent) {
        return "no_content.tmp";
      }

      String fileName = content.getUserData(DiffUserDataKeysEx.FILE_NAME);

      if (fileName == null && content instanceof DocumentContent) {
        VirtualFile highlightFile = ((DocumentContent)content).getHighlightFile();
        fileName = highlightFile != null ? highlightFile.getName() : null;
      }

      if (fileName == null && content instanceof FileContent) {
        fileName = ((FileContent)content).getFile().getName();
      }

      if (!StringUtil.isEmptyOrSpaces(fileName)) {
        return fileName;
      }


      FileType fileType = content.getContentType();
      String ext = fileType != null ? fileType.getDefaultExtension() : null;
      if (StringUtil.isEmptyOrSpaces(ext)) ext = "tmp";

      String name = "";
      if (title != null && windowTitle != null) {
        name = title + "_" + windowTitle;
      }
      else if (title != null || windowTitle != null) {
        name = title != null ? title : windowTitle;
      }
      if (name.length() > 50) name = name.substring(0, 50);

      return PathUtil.suggestFileName(name + "." + ext, true, false);
    }
  }
}
