// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.idea.svn.difftool.properties;

import com.intellij.diff.DiffContentFactory;
import com.intellij.diff.DiffContext;
import com.intellij.diff.comparison.ComparisonManager;
import com.intellij.diff.comparison.ComparisonPolicy;
import com.intellij.diff.comparison.DiffTooBigException;
import com.intellij.diff.contents.DiffContent;
import com.intellij.diff.contents.DocumentContent;
import com.intellij.diff.fragments.DiffFragment;
import com.intellij.diff.fragments.LineFragment;
import com.intellij.diff.fragments.LineFragmentImpl;
import com.intellij.diff.requests.ContentDiffRequest;
import com.intellij.diff.requests.ProxySimpleDiffRequest;
import com.intellij.diff.tools.util.BaseSyncScrollable;
import com.intellij.diff.tools.util.DiffSplitter;
import com.intellij.diff.tools.util.SyncScrollSupport;
import com.intellij.diff.tools.util.side.TwosideTextDiffViewer;
import com.intellij.diff.util.*;
import com.intellij.openapi.actionSystem.DataSink;
import com.intellij.openapi.actionSystem.PlatformCoreDataKeys;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.editor.EditorSettings;
import com.intellij.openapi.editor.LogicalPosition;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.ex.DocumentEx;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.impl.DocumentImpl;
import com.intellij.openapi.editor.markup.SeparatorPlacement;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.util.EmptyRunnable;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.FileStatus;
import com.intellij.ui.JBColor;
import com.intellij.ui.components.JBLabel;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.StartupUiUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.svn.properties.PropertyData;
import org.jetbrains.idea.svn.properties.PropertyValue;

import javax.swing.*;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.util.*;
import java.util.List;

public final class SvnPropertiesDiffViewer extends TwosideTextDiffViewer {
  private static final @NonNls String HELP_ID = "topicId758145";

  private final @NotNull List<DiffChange> myDiffChanges;

  private boolean myFirstRediff = true;

  public static @NotNull SvnPropertiesDiffViewer create(@NotNull DiffContext context, @NotNull SvnPropertiesDiffRequest request) {
    return create(context, request, false);
  }

  public static @NotNull SvnPropertiesDiffViewer create(@NotNull DiffContext context, @NotNull SvnPropertiesDiffRequest request, boolean embedded) {
    Pair<ContentDiffRequest, List<DiffChange>> pair = convertRequest(request, embedded);
    return new SvnPropertiesDiffViewer(context, pair.first, pair.second);
  }

  private SvnPropertiesDiffViewer(@NotNull DiffContext context,
                                  @NotNull ContentDiffRequest request,
                                  @NotNull List<DiffChange> diffChanges) {
    super(context, request);
    myDiffChanges = diffChanges;

    for (EditorEx editor : getEditors()) {
      if (editor == null) continue;
      EditorSettings settings = editor.getSettings();

      settings.setAdditionalLinesCount(0);
      settings.setAdditionalColumnsCount(1);
      settings.setRightMarginShown(false);
      settings.setFoldingOutlineShown(false);
      settings.setLineNumbersShown(false);
      settings.setLineMarkerAreaShown(false);
      settings.setIndentGuidesShown(false);
      settings.setVirtualSpace(false);
      settings.setWheelFontChangeEnabled(false);
      settings.setAdditionalPageAtBottom(false);
      settings.setCaretRowShown(false);
      settings.setUseSoftWraps(false);
      editor.reinitSettings();
    }

    for (DiffChange change : myDiffChanges) {
      DiffDrawUtil.createBorderLineMarker(getEditor(Side.LEFT), change.myEndLine1, SeparatorPlacement.TOP);
      DiffDrawUtil.createBorderLineMarker(getEditor(Side.RIGHT), change.myEndLine2, SeparatorPlacement.TOP);
    }

    DiffSplitter splitter = myContentPanel.getSplitter();
    splitter.setDividerWidth(120);
    splitter.setShowDividerIcon(false);
  }

  @Override
  protected void onInit() {
    super.onInit();
    myContentPanel.setPainter(new MyDividerPainter());
  }

  @Override
  protected @NotNull Runnable performRediff(@NotNull ProgressIndicator indicator) {
    if (!myFirstRediff) return new EmptyRunnable();
    myFirstRediff = false;

    for (DiffChange change : myDiffChanges) {
      PropertyRecord record = change.getRecord();
      String before = record.getBefore();
      String after = record.getAfter();

      assert before != null || after != null;
      if (before == null) {
        change.setFragments(createEverythingChanged(0, after.length(), 0, StringUtil.countNewLines(after) + 1));
        continue;
      }
      if (after == null) {
        change.setFragments(createEverythingChanged(before.length(), 0, StringUtil.countNewLines(before) + 1, 0));
        continue;
      }

      try {
        ComparisonManager manager = ComparisonManager.getInstance();
        change.setFragments(manager.squash(manager.compareLinesInner(before, after, ComparisonPolicy.DEFAULT, indicator)));
      }
      catch (DiffTooBigException e) {
        change.setFragments(createEverythingChanged(before.length(), after.length(),
                                                    StringUtil.countNewLines(before) + 1, StringUtil.countNewLines(after) + 1));
      }
    }

    return () -> {
      for (DiffChange change : myDiffChanges) {
        setupHighlighting(change, Side.LEFT);
        setupHighlighting(change, Side.RIGHT);
      }
    };
  }

  private void setupHighlighting(@NotNull DiffChange change, @NotNull Side side) {
    List<? extends LineFragment> fragments = change.getFragments();
    assert fragments != null;

    EditorEx editor = getEditor(side);
    DocumentEx document = editor.getDocument();
    int changeStartLine = change.getStartLine(side);

    for (LineFragment fragment : fragments) {
      List<DiffFragment> innerFragments = fragment.getInnerFragments();

      int startLine = side.getStartLine(fragment) + changeStartLine;
      int endLine = side.getEndLine(fragment) + changeStartLine;

      int start = document.getLineStartOffset(startLine);
      TextDiffType type = DiffUtil.getLineDiffType(fragment);

      DiffDrawUtil.createHighlighter(editor, startLine, endLine, type, innerFragments != null);

      // TODO: we can paint LineMarker here, but it looks ugly for small editors
      if (innerFragments != null) {
        for (DiffFragment innerFragment : innerFragments) {
          int innerStart = side.getStartOffset(innerFragment);
          int innerEnd = side.getEndOffset(innerFragment);
          TextDiffType innerType = DiffUtil.getDiffType(innerFragment);

          innerStart += start;
          innerEnd += start;

          DiffDrawUtil.createInlineHighlighter(editor, innerStart, innerEnd, innerType);
        }
      }
    }
  }

  private static @NotNull List<? extends LineFragment> createEverythingChanged(int length1, int length2, int lines1, int lines2) {
    return Collections.singletonList(new LineFragmentImpl(0, lines1, 0, lines2, 0, length1, 0, length2));
  }

  private class MyDividerPainter implements DiffSplitter.Painter, DiffDividerDrawUtil.DividerPaintable {
    private final @NotNull JBLabel myLabel;

    MyDividerPainter() {
      myLabel = new JBLabel();
      myLabel.setFont(StartupUiUtil.getLabelFont());
      myLabel.setHorizontalAlignment(SwingConstants.CENTER);
      myLabel.setVerticalAlignment(SwingConstants.TOP);
    }

    @Override
    public void paint(@NotNull Graphics g, @NotNull JComponent divider) {
      ReadAction.run(() -> {
        Graphics2D gg = DiffDividerDrawUtil.getDividerGraphics(g, divider, getEditor1().getComponent());
        Rectangle clip = gg.getClipBounds();
        if (clip == null) return;

        gg.setColor(DiffDrawUtil.getDividerColor());
        gg.fill(clip);

        EditorEx editor1 = getEditor1();
        EditorEx editor2 = getEditor2();

        JComponent header1 = editor1.getHeaderComponent();
        JComponent header2 = editor2.getHeaderComponent();
        int headerOffset1 = header1 == null ? 0 : header1.getHeight();
        int headerOffset2 = header2 == null ? 0 : header2.getHeight();

        // TODO: painting is ugly if shift1 != shift2 (ex: search field is opened for one of editors)
        int shift1 = editor1.getScrollingModel().getVerticalScrollOffset() - headerOffset1;
        int shift2 = editor2.getScrollingModel().getVerticalScrollOffset() - headerOffset2;
        double rotate = shift1 == shift2 ? 0 : Math.atan2(shift2 - shift1, clip.width);

        DiffDividerDrawUtil.paintPolygons(gg, divider.getWidth(), rotate == 0, editor1, editor2, this);

        for (DiffChange change : myDiffChanges) {
          int y1 = editor1.logicalPositionToXY(new LogicalPosition(change.getStartLine(Side.LEFT), 0)).y - shift1;
          int y2 = editor2.logicalPositionToXY(new LogicalPosition(change.getStartLine(Side.RIGHT), 0)).y - shift2;
          int endY1 = editor1.logicalPositionToXY(new LogicalPosition(change.getEndLine(Side.LEFT), 0)).y - shift1;
          int endY2 = editor2.logicalPositionToXY(new LogicalPosition(change.getEndLine(Side.RIGHT), 0)).y - shift2;

          AffineTransform oldTransform = gg.getTransform();
          gg.translate(0, y1);
          if (rotate != 0) gg.rotate(-rotate);
          myLabel.setText(change.getRecord().getName());
          myLabel.setForeground(getRecordTitleColor(change));
          myLabel.setBounds(clip);
          myLabel.paint(gg);
          gg.setTransform(oldTransform);

          gg.setColor(JBColor.border());
          gg.drawLine(0, y1 - 1, clip.width, y2 - 1);
          gg.drawLine(0, endY1 - 1, clip.width, endY2 - 1);
        }
        gg.dispose();
      });
    }

    @Override
    public void process(@NotNull Handler handler) {
      for (DiffChange diffChange : myDiffChanges) {
        TextDiffType type = getDiffType(diffChange);
        if (type == null) continue;
        int shift1 = diffChange.getStartLine(Side.LEFT);
        int shift2 = diffChange.getStartLine(Side.RIGHT);

        List<? extends LineFragment> fragments = diffChange.getFragments();
        if (fragments == null) continue;

        for (LineFragment fragment : diffChange.getFragments()) {
          if (!handler.process(Side.LEFT.getStartLine(fragment) + shift1, Side.LEFT.getEndLine(fragment) + shift1,
                               Side.RIGHT.getStartLine(fragment) + shift2, Side.RIGHT.getEndLine(fragment) + shift2,
                               DiffUtil.getLineDiffType(fragment))) {
            return;
          }
        }
      }
    }

    private @Nullable Color getRecordTitleColor(@NotNull DiffChange change) {
      TextDiffType type = getDiffType(change);
      if (type == TextDiffType.INSERTED) return FileStatus.ADDED.getColor();
      if (type == TextDiffType.DELETED) return FileStatus.DELETED.getColor();
      if (type == TextDiffType.MODIFIED) return FileStatus.MODIFIED.getColor();
      return JBColor.black; // unchanged
    }

    public @Nullable TextDiffType getDiffType(@NotNull DiffChange change) {
      if (change.getRecord().getBefore() == null) return TextDiffType.INSERTED;
      if (change.getRecord().getAfter() == null) return TextDiffType.DELETED;
      if (change.getFragments() != null && !change.getFragments().isEmpty()) return TextDiffType.MODIFIED;
      return null; // unchanged
    }
  }

  @Override
  protected void onDocumentChange(@NotNull DocumentEvent event) {
    LOG.warn("Document changes are not supported");
  }

  //
  // Impl
  //


  @Override
  protected @Nullable SyncScrollSupport.SyncScrollable getSyncScrollable() {
    return new SyncScrollSupport.SyncScrollable() {
      @Override
      public boolean isSyncScrollEnabled() {
        return true;
      }

      @Override
      public int transfer(@NotNull Side side, int line) {
        return line;
      }

      @Override
      public @NotNull Range getRange(@NotNull Side baseSide, int line) {
        return BaseSyncScrollable.idRange(line);
      }
    };
  }

  @Override
  public void uiDataSnapshot(@NotNull DataSink sink) {
    super.uiDataSnapshot(sink);
    sink.set(PlatformCoreDataKeys.HELP_ID, HELP_ID);
  }

  //
  // Initial step
  //

  private static @NotNull Pair<ContentDiffRequest, List<DiffChange>> convertRequest(@NotNull SvnPropertiesDiffRequest request, boolean embedded) {
    List<PropertyRecord> records = collectRecords(request);

    StringBuilder builder1 = new StringBuilder();
    StringBuilder builder2 = new StringBuilder();
    List<DiffChange> diffChanges = new ArrayList<>();

    int totalLines = 0;
    for (PropertyRecord record : records) {
      int start = totalLines;

      String before = StringUtil.notNullize(record.getBefore());
      String after = StringUtil.notNullize(record.getAfter());
      builder1.append(before);
      builder2.append(after);

      int lines1 = StringUtil.countNewLines(before);
      int lines2 = StringUtil.countNewLines(after);

      int appendedLines = Math.max(lines1, lines2) + 1;
      totalLines += appendedLines;

      builder1.append("\n".repeat(appendedLines - lines1));
      builder2.append("\n".repeat(appendedLines - lines2));

      diffChanges.add(new DiffChange(record, start, totalLines, start, totalLines));
    }

    DocumentContent content1 = DiffContentFactory.getInstance().create(null, new DocumentImpl(builder1));
    DocumentContent content2 = DiffContentFactory.getInstance().create(null, new DocumentImpl(builder2));
    List<DiffContent> contents = Arrays.asList(content1, content2);
    List<String> titles = embedded ? Arrays.asList(null, null) : request.getContentTitles();

    ProxySimpleDiffRequest proxyRequest = new ProxySimpleDiffRequest(request.getTitle(), contents, titles, request);
    proxyRequest.putUserData(DiffUserDataKeys.FORCE_READ_ONLY, true);
    return Pair.create(proxyRequest, diffChanges);
  }

  private static @NotNull List<PropertyRecord> collectRecords(@NotNull SvnPropertiesDiffRequest request) {
    List<DiffContent> originalContents = request.getContents();
    List<PropertyData> properties1 = getProperties(originalContents.get(0));
    List<PropertyData> properties2 = getProperties(originalContents.get(1));

    Map<String, PropertyValue> before = new HashMap<>();
    Map<String, PropertyValue> after = new HashMap<>();
    if (properties1 != null) {
      for (PropertyData data : properties1) {
        before.put(data.getName(), data.getValue());
      }
    }
    if (properties2 != null) {
      for (PropertyData data : properties2) {
        after.put(data.getName(), data.getValue());
      }
    }

    List<PropertyRecord> records = new ArrayList<>();
    for (String name : ContainerUtil.union(before.keySet(), after.keySet())) {
      records.add(createRecord(name, before.get(name), after.get(name)));
    }

    ContainerUtil.sort(records, (o1, o2) -> StringUtil.naturalCompare(o1.getName(), o2.getName()));

    return records;
  }

  private static @Nullable List<PropertyData> getProperties(@NotNull DiffContent content) {
    if (content instanceof SvnPropertiesDiffRequest.PropertyContent) {
      return ((SvnPropertiesDiffRequest.PropertyContent)content).getProperties();
    }
    return null;
  }

  private static @NotNull PropertyRecord createRecord(@NotNull String name, @Nullable PropertyValue value1, @Nullable PropertyValue value2) {
    assert value1 != null || value2 != null;

    String text1 = value1 != null ? value1.toString() : null;
    String text2 = value2 != null ? value2.toString() : null;

    // TODO: show differences in line separators ?
    if (text1 != null) text1 = StringUtil.convertLineSeparators(text1);
    if (text2 != null) text2 = StringUtil.convertLineSeparators(text2);

    return new PropertyRecord(name, text1, text2);
  }

  //
  // Helpers
  //

  private static class PropertyRecord {
    private final @NotNull String myName;
    private final @Nullable String myBefore;
    private final @Nullable String myAfter;

    PropertyRecord(@NotNull String name, @Nullable String before, @Nullable String after) {
      assert before != null || after != null;

      myName = name;
      myBefore = before;
      myAfter = after;
    }

    public @NlsSafe @NotNull String getName() {
      return myName;
    }

    public @Nullable String getBefore() {
      return myBefore;
    }

    public @Nullable String getAfter() {
      return myAfter;
    }
  }

  private static class DiffChange {
    private final @NotNull PropertyRecord myRecord;
    private final int myStartLine1;
    private final int myEndLine1;
    private final int myStartLine2;
    private final int myEndLine2;

    private @Nullable List<? extends LineFragment> myFragments;

    DiffChange(@NotNull PropertyRecord record, int startLine1, int endLine1, int startLine2, int endLine2) {
      myRecord = record;
      myStartLine1 = startLine1;
      myEndLine1 = endLine1;
      myStartLine2 = startLine2;
      myEndLine2 = endLine2;
    }

    public @NotNull PropertyRecord getRecord() {
      return myRecord;
    }

    public int getStartLine(@NotNull Side side) {
      return side.select(myStartLine1, myStartLine2);
    }

    public int getEndLine(@NotNull Side side) {
      return side.select(myEndLine1, myEndLine2);
    }

    public void setFragments(@Nullable List<? extends LineFragment> fragments) {
      myFragments = fragments;
    }

    public @Nullable List<? extends LineFragment> getFragments() {
      return myFragments;
    }
  }
}