// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.idea.svn.diff;

import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.FileStatus;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vcs.changes.ContentRevision;
import com.intellij.openapi.vcs.changes.CurrentContentRevision;
import com.intellij.vcsUtil.VcsUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.svn.SvnUtil;
import org.jetbrains.idea.svn.WorkingCopyFormat;
import org.jetbrains.idea.svn.api.BaseSvnClient;
import org.jetbrains.idea.svn.api.NodeKind;
import org.jetbrains.idea.svn.api.Revision;
import org.jetbrains.idea.svn.api.Target;
import org.jetbrains.idea.svn.commandLine.CommandExecutor;
import org.jetbrains.idea.svn.commandLine.CommandUtil;
import org.jetbrains.idea.svn.commandLine.SvnBindException;
import org.jetbrains.idea.svn.commandLine.SvnCommandName;
import org.jetbrains.idea.svn.history.SvnRepositoryContentRevision;
import org.jetbrains.idea.svn.status.Status;
import org.jetbrains.idea.svn.status.StatusType;

import javax.xml.bind.JAXBException;
import javax.xml.bind.annotation.*;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

import static org.jetbrains.idea.svn.SvnBundle.message;

public class CmdDiffClient extends BaseSvnClient implements DiffClient {

  private static final Logger LOG = Logger.getInstance(CmdDiffClient.class);

  @NotNull
  @Override
  public List<Change> compare(@NotNull Target target1, @NotNull Target target2) throws VcsException {
    assertUrl(target1);
    if (target2.isFile()) {
      // Such combination (file and url) with "--summarize" option is supported only in svn 1.8.
      // For svn 1.7 "--summarize" is only supported when both targets are repository urls.
      assertDirectory(target2);

      WorkingCopyFormat format = WorkingCopyFormat.from(myFactory.createVersionClient().getVersion());
      if (format.less(WorkingCopyFormat.ONE_DOT_EIGHT)) {
        throw new SvnBindException(
          message("error.could.not.compare.local.file.and.remote.url.with.executable.for.svn.version", format.getDisplayName()));
      }
    }

    List<String> parameters = new ArrayList<>();
    CommandUtil.put(parameters, target1);
    CommandUtil.put(parameters, target2);
    parameters.add("--xml");
    parameters.add("--summarize");

    CommandExecutor executor = execute(myVcs, target1, SvnCommandName.diff, parameters, null);
    return parseOutput(target1, target2, executor);
  }

  @Override
  public void unifiedDiff(@NotNull Target target1, @NotNull Target target2, @NotNull OutputStream output) throws VcsException {
    assertUrl(target1);
    assertUrl(target2);

    List<String> parameters = new ArrayList<>();
    CommandUtil.put(parameters, target1);
    CommandUtil.put(parameters, target2);

    CommandExecutor executor = execute(myVcs, target1, SvnCommandName.diff, parameters, null);

    try {
      executor.getBinaryOutput().writeTo(output);
    }
    catch (IOException e) {
      throw new SvnBindException(e);
    }
  }

  @NotNull
  private List<Change> parseOutput(@NotNull Target target1, @NotNull Target target2, @NotNull CommandExecutor executor)
    throws SvnBindException {
    try {
      DiffInfo diffInfo = CommandUtil.parse(executor.getOutput(), DiffInfo.class);
      List<Change> result = new ArrayList<>();

      if (diffInfo != null) {
        for (DiffPath path : diffInfo.diffPaths) {
          result.add(createChange(target1, target2, path));
        }
      }

      return result;
    }
    catch (JAXBException e) {
      throw new SvnBindException(e);
    }
  }

  @NotNull
  private ContentRevision createRevision(@NotNull FilePath path,
                                         @NotNull FilePath localPath,
                                         @NotNull Revision revision,
                                         @NotNull FileStatus status) {
    ContentRevision result;

    if (path.isNonLocal()) {
    // explicitly use local path for deleted items - so these items will be correctly displayed as deleted under local working copy node
    // and not as deleted under remote branch node (in ChangesBrowser)
    // NOTE, that content is still retrieved using remotePath.
      result = SvnRepositoryContentRevision.create(myVcs, path, status == FileStatus.DELETED ? localPath : null, revision.getNumber());
    }
    else {
      result = CurrentContentRevision.create(path);
  }

    return result;
  }

  private static FilePath createFilePath(@NotNull Target target, boolean isDirectory) {
    return target.isFile()
           ? VcsUtil.getFilePath(target.getFile(), isDirectory)
           : VcsUtil.getFilePathOnNonLocal(SvnUtil.toDecodedString(target), isDirectory);
  }

  @NotNull
  private Change createChange(@NotNull Target target1, @NotNull Target target2, @NotNull DiffPath diffPath) throws SvnBindException {
    // TODO: 1) Unify logic of creating Change instance with SvnChangeProviderContext
    // TODO: 2) If some directory is switched, files inside it are returned as modified in "svn diff --summarize", even if they are equal
    // TODO: to branch files by content - possibly add separate processing of all switched files
    // TODO: 3) Properties change is currently not added as part of result change like in SvnChangeProviderContext.patchWithPropertyChange

    Target subTarget1 = SvnUtil.append(target1, diffPath.path, true);
    String relativePath = SvnUtil.getRelativeUrl(target1, subTarget1);

    if (relativePath == null) {
      throw new SvnBindException(message("error.could.not.get.relative.path.for.parent.and.child", target1, subTarget1));
    }

    Target subTarget2 = SvnUtil.append(target2, FileUtil.toSystemIndependentName(relativePath));

    FilePath target1Path = createFilePath(subTarget1, diffPath.isDirectory());
    FilePath target2Path = createFilePath(subTarget2, diffPath.isDirectory());

    FileStatus status = Status.convertStatus(getStatus(diffPath.itemStatus), getStatus(diffPath.propertiesStatus), false, false);

    // statuses determine changes needs to be done to "target1" to get "target2" state
    ContentRevision beforeRevision = status == FileStatus.ADDED
                                     ? null
                                     : createRevision(target1Path, target2Path, target1.getPegRevision(), status);
    ContentRevision afterRevision = status == FileStatus.DELETED
                                    ? null
                                    : createRevision(target2Path, target1Path, target2.getPegRevision(), status);

    return createChange(status, beforeRevision, afterRevision);
  }

  @NotNull
  private static Change createChange(@NotNull final FileStatus status,
                                     @Nullable final ContentRevision beforeRevision,
                                     @Nullable final ContentRevision afterRevision) {
    // isRenamed() and isMoved() are always false here not to have text like "moved from ..." in changes window - by default different
    // paths in before and after revisions are treated as move, but this is not the case for "Compare with Branch"
    return new Change(beforeRevision, afterRevision, status) {
      @Override
      public boolean isRenamed() {
        return false;
      }

      @Override
      public boolean isMoved() {
        return false;
      }
    };
  }

  @Nullable
  private static StatusType getStatus(@NotNull String code) {
    StatusType result = StatusType.forStatusOperation(code);

    if (result == null) {
      LOG.info("Unknown status type " + code);
    }

    return result;
  }

  @XmlRootElement(name = "diff")
  public static class DiffInfo {

    @XmlElementWrapper(name = "paths")
    @XmlElement(name = "path")
    public List<DiffPath> diffPaths = new ArrayList<>();
  }

  public static class DiffPath {

    @XmlAttribute(name = "kind", required = true)
    public NodeKind kind;

    @XmlAttribute(name = "props")
    public String propertiesStatus;

    @XmlAttribute(name = "item")
    public String itemStatus;

    @XmlValue
    public String path;

    public boolean isDirectory() {
      return kind.isDirectory();
    }
  }
}
