/*
 * Copyright 2000-2016 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.openapi.vcs.changes.actions.diff;

import com.intellij.diff.DiffContentFactory;
import com.intellij.diff.DiffContentFactoryEx;
import com.intellij.diff.DiffRequestFactory;
import com.intellij.diff.DiffRequestFactoryImpl;
import com.intellij.diff.chains.DiffRequestProducer;
import com.intellij.diff.chains.DiffRequestProducerException;
import com.intellij.diff.contents.DiffContent;
import com.intellij.diff.impl.DiffViewerWrapper;
import com.intellij.diff.merge.MergeUtil;
import com.intellij.diff.requests.DiffRequest;
import com.intellij.diff.requests.ErrorDiffRequest;
import com.intellij.diff.requests.SimpleDiffRequest;
import com.intellij.diff.util.DiffUserDataKeys;
import com.intellij.diff.util.DiffUserDataKeysEx;
import com.intellij.diff.util.DiffUtil;
import com.intellij.diff.util.Side;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.UserDataHolder;
import com.intellij.openapi.vcs.AbstractVcs;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.VcsDataKeys;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.*;
import com.intellij.openapi.vcs.merge.MergeData;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ThreeState;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;

public class ChangeDiffRequestProducer implements DiffRequestProducer {
  private static final Logger LOG = Logger.getInstance(ChangeDiffRequestProducer.class);

  public static Key<Change> CHANGE_KEY = Key.create("DiffRequestPresentable.Change");

  @Nullable private final Project myProject;
  @NotNull private final Change myChange;
  @NotNull private final Map<Key, Object> myChangeContext;

  private ChangeDiffRequestProducer(@Nullable Project project, @NotNull Change change, @NotNull Map<Key, Object> changeContext) {
    myChange = change;
    myProject = project;
    myChangeContext = changeContext;
  }

  @NotNull
  public Change getChange() {
    return myChange;
  }

  @Nullable
  public Project getProject() {
    return myProject;
  }

  @NotNull
  @Override
  public String getName() {
    return ChangesUtil.getFilePath(myChange).getPath();
  }

  public static boolean isEquals(@NotNull Change change1, @NotNull Change change2) {
    if (!Comparing.equal(ChangesUtil.getBeforePath(change1), ChangesUtil.getBeforePath(change2)) ||
        !Comparing.equal(ChangesUtil.getAfterPath(change1), ChangesUtil.getAfterPath(change2))) {
      // we use file paths for hashCode, so removing this check might violate comparison contract
      return false;
    }

    for (ChangeDiffViewerWrapperProvider provider : ChangeDiffViewerWrapperProvider.EP_NAME.getExtensions()) {
      ThreeState equals = provider.isEquals(change1, change2);
      if (equals == ThreeState.NO) return false;
    }
    for (ChangeDiffRequestProvider provider : ChangeDiffRequestProvider.EP_NAME.getExtensions()) {
      ThreeState equals = provider.isEquals(change1, change2);
      if (equals == ThreeState.YES) return true;
      if (equals == ThreeState.NO) return false;
    }

    if (!Comparing.equal(change1.getClass(), change2.getClass())) return false;
    if (!isEquals(change1.getBeforeRevision(), change2.getBeforeRevision())) return false;
    if (!isEquals(change1.getAfterRevision(), change2.getAfterRevision())) return false;

    return true;
  }

  private static boolean isEquals(@Nullable ContentRevision revision1, @Nullable ContentRevision revision2) {
    if (Comparing.equal(revision1, revision2)) return true;
    if (revision1 instanceof CurrentContentRevision && revision2 instanceof CurrentContentRevision) {
      VirtualFile vFile1 = ((CurrentContentRevision)revision1).getVirtualFile();
      VirtualFile vFile2 = ((CurrentContentRevision)revision2).getVirtualFile();
      return Comparing.equal(vFile1, vFile2);
    }
    return false;
  }

  public static int hashCode(@NotNull Change change) {
    return hashCode(change.getBeforeRevision()) + 31 * hashCode(change.getAfterRevision());
  }

  private static int hashCode(@Nullable ContentRevision revision) {
    return revision != null ? revision.getFile().hashCode() : 0;
  }

  @Nullable
  public static ChangeDiffRequestProducer create(@Nullable Project project, @NotNull Change change) {
    return create(project, change, Collections.<Key, Object>emptyMap());
  }

  @Nullable
  public static ChangeDiffRequestProducer create(@Nullable Project project,
                                                 @NotNull Change change,
                                                 @NotNull Map<Key, Object> changeContext) {
    if (!canCreate(project, change)) return null;
    return new ChangeDiffRequestProducer(project, change, changeContext);
  }

  public static boolean canCreate(@Nullable Project project, @NotNull Change change) {
    for (ChangeDiffViewerWrapperProvider provider : ChangeDiffViewerWrapperProvider.EP_NAME.getExtensions()) {
      if (provider.canCreate(project, change)) return true;
    }
    for (ChangeDiffRequestProvider provider : ChangeDiffRequestProvider.EP_NAME.getExtensions()) {
      if (provider.canCreate(project, change)) return true;
    }

    ContentRevision bRev = change.getBeforeRevision();
    ContentRevision aRev = change.getAfterRevision();

    if (bRev == null && aRev == null) return false;
    if (bRev != null && bRev.getFile().isDirectory()) return false;
    if (aRev != null && aRev.getFile().isDirectory()) return false;

    return true;
  }

  @NotNull
  @Override
  public DiffRequest process(@NotNull UserDataHolder context,
                             @NotNull ProgressIndicator indicator) throws DiffRequestProducerException, ProcessCanceledException {
    try {
      return loadCurrentContents(context, indicator);
    }
    catch (ProcessCanceledException | DiffRequestProducerException e) {
      throw e;
    }
    catch (Exception e) {
      LOG.warn(e);
      throw new DiffRequestProducerException(e.getMessage());
    }
  }

  @NotNull
  protected DiffRequest loadCurrentContents(@NotNull UserDataHolder context,
                                            @NotNull ProgressIndicator indicator) throws DiffRequestProducerException {
    DiffRequestProducerException wrapperException = null;
    DiffRequestProducerException requestException = null;

    DiffViewerWrapper wrapper = null;
    try {
      for (ChangeDiffViewerWrapperProvider provider : ChangeDiffViewerWrapperProvider.EP_NAME.getExtensions()) {
        if (provider.canCreate(myProject, myChange)) {
          wrapper = provider.process(this, context, indicator);
          break;
        }
      }
    }
    catch (DiffRequestProducerException e) {
      wrapperException = e;
    }

    DiffRequest request = null;
    try {
      for (ChangeDiffRequestProvider provider : ChangeDiffRequestProvider.EP_NAME.getExtensions()) {
        if (provider.canCreate(myProject, myChange)) {
          request = provider.process(this, context, indicator);
          break;
        }
      }
      if (request == null) request = createRequest(myProject, myChange, context, indicator);
    }
    catch (DiffRequestProducerException e) {
      requestException = e;
    }

    if (requestException != null && wrapperException != null) {
      String message = requestException.getMessage() + "\n\n" + wrapperException.getMessage();
      throw new DiffRequestProducerException(message);
    }
    if (requestException != null) {
      request = new ErrorDiffRequest(getRequestTitle(myChange), requestException);
      LOG.info("Request: " + requestException.getMessage());
    }
    if (wrapperException != null) {
      LOG.info("Wrapper: " + wrapperException.getMessage());
    }

    request.putUserData(CHANGE_KEY, myChange);
    request.putUserData(DiffViewerWrapper.KEY, wrapper);

    for (Map.Entry<Key, Object> entry : myChangeContext.entrySet()) {
      request.putUserData(entry.getKey(), entry.getValue());
    }

    DiffUtil.putDataKey(request, VcsDataKeys.CURRENT_CHANGE, myChange);

    return request;
  }

  @NotNull
  private DiffRequest createRequest(@Nullable Project project,
                                    @NotNull Change change,
                                    @NotNull UserDataHolder context,
                                    @NotNull ProgressIndicator indicator) throws DiffRequestProducerException {
    if (ChangesUtil.isTextConflictingChange(change)) { // three side diff
      // FIXME: This part is ugly as a VCS merge subsystem itself.

      FilePath path = ChangesUtil.getFilePath(change);
      VirtualFile file = path.getVirtualFile();
      if (file == null) {
        file = LocalFileSystem.getInstance().refreshAndFindFileByPath(path.getPath());
      }
      if (file == null) throw new DiffRequestProducerException("Can't show merge conflict - file not found");

      if (project == null) {
        throw new DiffRequestProducerException("Can't show merge conflict - project is unknown");
      }
      final AbstractVcs vcs = ChangesUtil.getVcsForChange(change, project);
      if (vcs == null || vcs.getMergeProvider() == null) {
        throw new DiffRequestProducerException("Can't show merge conflict - operation nos supported");
      }
      try {
        // FIXME: loadRevisions() can call runProcessWithProgressSynchronously() inside
        final Ref<Throwable> exceptionRef = new Ref<>();
        final Ref<MergeData> mergeDataRef = new Ref<>();
        final VirtualFile finalFile = file;
        ApplicationManager.getApplication().invokeAndWait(() -> {
          try {
            mergeDataRef.set(vcs.getMergeProvider().loadRevisions(finalFile));
          }
          catch (VcsException e) {
            exceptionRef.set(e);
          }
        });
        if (!exceptionRef.isNull()) {
          Throwable e = exceptionRef.get();
          if (e instanceof VcsException) throw (VcsException)e;
          if (e instanceof Error) throw (Error)e;
          if (e instanceof RuntimeException) throw (RuntimeException)e;
          throw new RuntimeException(e);
        }
        MergeData mergeData = mergeDataRef.get();

        ContentRevision bRev = change.getBeforeRevision();
        ContentRevision aRev = change.getAfterRevision();
        String beforeRevisionTitle = getRevisionTitle(bRev, "Your version");
        String afterRevisionTitle = getRevisionTitle(aRev, "Server version");

        String title = DiffRequestFactory.getInstance().getTitle(file);
        List<String> titles = ContainerUtil.list(beforeRevisionTitle, "Base Version", afterRevisionTitle);

        DiffContentFactory contentFactory = DiffContentFactory.getInstance();
        List<DiffContent> contents = ContainerUtil.list(
          contentFactory.createFromBytes(project, mergeData.CURRENT, file),
          contentFactory.createFromBytes(project, mergeData.ORIGINAL, file),
          contentFactory.createFromBytes(project, mergeData.LAST, file)
        );

        SimpleDiffRequest request = new SimpleDiffRequest(title, contents, titles);
        MergeUtil.putRevisionInfos(request, mergeData);

        return request;
      }
      catch (VcsException | IOException e) {
        LOG.info(e);
        throw new DiffRequestProducerException(e);
      }
    }
    else {
      ContentRevision bRev = change.getBeforeRevision();
      ContentRevision aRev = change.getAfterRevision();

      if (bRev == null && aRev == null) {
        LOG.warn("Both revision contents are empty");
        throw new DiffRequestProducerException("Bad revisions contents");
      }
      if (bRev != null) checkContentRevision(project, bRev, context, indicator);
      if (aRev != null) checkContentRevision(project, aRev, context, indicator);

      String title = getRequestTitle(change);

      indicator.setIndeterminate(true);
      DiffContent content1 = createContent(project, bRev, context, indicator);
      DiffContent content2 = createContent(project, aRev, context, indicator);

      final String userLeftRevisionTitle = (String)myChangeContext.get(DiffUserDataKeysEx.VCS_DIFF_LEFT_CONTENT_TITLE);
      String beforeRevisionTitle = userLeftRevisionTitle != null ? userLeftRevisionTitle : getRevisionTitle(bRev, "Base version");
      final String userRightRevisionTitle = (String)myChangeContext.get(DiffUserDataKeysEx.VCS_DIFF_RIGHT_CONTENT_TITLE);
      String afterRevisionTitle = userRightRevisionTitle != null ? userRightRevisionTitle : getRevisionTitle(aRev, "Your version");

      SimpleDiffRequest request = new SimpleDiffRequest(title, content1, content2, beforeRevisionTitle, afterRevisionTitle);

      boolean bRevCurrent = bRev instanceof CurrentContentRevision;
      boolean aRevCurrent = aRev instanceof CurrentContentRevision;
      if (bRevCurrent && !aRevCurrent) request.putUserData(DiffUserDataKeys.MASTER_SIDE, Side.LEFT);
      if (!bRevCurrent && aRevCurrent) request.putUserData(DiffUserDataKeys.MASTER_SIDE, Side.RIGHT);

      return request;
    }
  }

  @NotNull
  public static String getRequestTitle(@NotNull Change change) {
    ContentRevision bRev = change.getBeforeRevision();
    ContentRevision aRev = change.getAfterRevision();

    assert bRev != null || aRev != null;
    if (bRev != null && aRev != null) {
      FilePath bPath = bRev.getFile();
      FilePath aPath = aRev.getFile();
      if (bPath.equals(aPath)) {
        return DiffRequestFactoryImpl.getContentTitle(bPath);
      }
      else {
        return DiffRequestFactoryImpl.getTitle(bPath, aPath, " -> ");
      }
    }
    else if (bRev != null) {
      return DiffRequestFactoryImpl.getContentTitle(bRev.getFile());
    }
    else {
      return DiffRequestFactoryImpl.getContentTitle(aRev.getFile());
    }
  }

  @NotNull
  public static String getRevisionTitle(@Nullable ContentRevision revision, @NotNull String defaultValue) {
    if (revision == null) return defaultValue;
    String title = revision.getRevisionNumber().asString();
    if (title == null || title.isEmpty()) return defaultValue;
    return title;
  }

  @NotNull
  public static DiffContent createContent(@Nullable Project project,
                                          @Nullable ContentRevision revision,
                                          @NotNull UserDataHolder context,
                                          @NotNull ProgressIndicator indicator) throws DiffRequestProducerException {
    try {
      indicator.checkCanceled();

      if (revision == null) return DiffContentFactory.getInstance().createEmpty();
      FilePath filePath = revision.getFile();
      DiffContentFactoryEx contentFactory = DiffContentFactoryEx.getInstanceEx();

      if (revision instanceof CurrentContentRevision) {
        VirtualFile vFile = ((CurrentContentRevision)revision).getVirtualFile();
        if (vFile == null) throw new DiffRequestProducerException("Can't get current revision content");
        return contentFactory.create(project, vFile);
      }

      if (revision instanceof BinaryContentRevision) {
        byte[] content = ((BinaryContentRevision)revision).getBinaryContent();
        if (content == null) {
          throw new DiffRequestProducerException("Can't get binary revision content");
        }
        return contentFactory.createFromBytes(project, content, filePath);
      }

      if (revision instanceof ByteBackedContentRevision) {
        byte[] revisionContent = ((ByteBackedContentRevision)revision).getContentAsBytes();
        if (revisionContent == null) throw new DiffRequestProducerException("Can't get revision content");
        return contentFactory.createFromBytes(project, revisionContent, filePath);
      }
      else {
        String revisionContent = revision.getContent();
        if (revisionContent == null) throw new DiffRequestProducerException("Can't get revision content");
        return contentFactory.create(project, revisionContent, filePath);
      }
    }
    catch (IOException | VcsException e) {
      LOG.info(e);
      throw new DiffRequestProducerException(e);
    }
  }

  public static void checkContentRevision(@Nullable Project project,
                                          @NotNull ContentRevision rev,
                                          @NotNull UserDataHolder context,
                                          @NotNull ProgressIndicator indicator) throws DiffRequestProducerException {
    if (rev.getFile().isDirectory()) {
      throw new DiffRequestProducerException("Can't show diff for directory");
    }
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    ChangeDiffRequestProducer that = (ChangeDiffRequestProducer)o;

    return myChange.equals(that.myChange);
  }

  @Override
  public int hashCode() {
    return myChange.hashCode();
  }
}
