package org.jetbrains.idea.svn.difftool;

import com.intellij.diff.DiffContext;
import com.intellij.diff.FrameDiffTool.DiffViewer;
import com.intellij.diff.FrameDiffTool.ToolbarComponents;
import com.intellij.diff.contents.DiffContent;
import com.intellij.diff.contents.EmptyContent;
import com.intellij.diff.requests.DiffRequest;
import com.intellij.diff.requests.ErrorDiffRequest;
import com.intellij.diff.tools.ErrorDiffTool;
import com.intellij.diff.util.DiffUtil;
import com.intellij.ide.DataManager;
import com.intellij.ide.impl.DataManagerImpl;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DataProvider;
import com.intellij.openapi.actionSystem.ToggleAction;
import com.intellij.openapi.actionSystem.ex.ActionUtil;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Divider;
import com.intellij.openapi.ui.Splitter;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Key;
import com.intellij.ui.EditorNotificationPanel;
import com.intellij.ui.JBColor;
import com.intellij.ui.components.panels.Wrapper;
import com.intellij.util.containers.HashMap;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UIUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.svn.difftool.SvnDiffSettingsHolder.SvnDiffSettings;
import org.jetbrains.idea.svn.difftool.properties.SvnPropertiesDiffRequest;
import org.jetbrains.idea.svn.difftool.properties.SvnPropertiesDiffViewer;
import org.jetbrains.idea.svn.properties.PropertyData;
import org.jetbrains.idea.svn.properties.PropertyValue;

import javax.swing.*;
import java.awt.*;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

public class SvnDiffViewer implements DiffViewer {
  private static final Logger LOG = Logger.getInstance(SvnDiffViewer.class);

  public static final Key<MyPropertyContext> PROPERTY_CONTEXT_KEY = Key.create("MyPropertyContext");
  public static final Key<Boolean> FOCUSED_VIEWER_KEY = Key.create("SvnFocusedViewer");

  @Nullable private final Project myProject;

  @NotNull private final DiffContext myContext;
  @NotNull private final DiffRequest myPropertyRequest;

  @NotNull private final SvnDiffSettings mySettings;

  @NotNull private final JPanel myPanel;
  @NotNull private final Splitter mySplitter;
  @NotNull private final Wrapper myNotificationPanel;

  @NotNull private final DiffViewer myContentViewer;
  @NotNull private final DiffViewer myPropertiesViewer;

  @NotNull private final FocusListener myContentFocusListener = new MyFocusListener(false);
  @NotNull private final FocusListener myPropertiesFocusListener = new MyFocusListener(true);

  private boolean myPropertiesViewerFocused; // False - content viewer, True - properties
  private boolean myDumbContentViewer;

  public SvnDiffViewer(@NotNull DiffContext context, @NotNull DiffRequest propertyRequest, @NotNull DiffViewer wrappingViewer) {
    myProject = context.getProject();
    myContext = context;
    myPropertyRequest = propertyRequest;
    myContentViewer = wrappingViewer;

    myPropertyRequest.onAssigned(true);

    mySettings = initSettings(context);

    mySplitter = new MySplitter("Property Changes");
    mySplitter.setProportion(mySettings.getSplitterProportion());
    mySplitter.setFirstComponent(myContentViewer.getComponent());

    myNotificationPanel = new Wrapper();

    MyPropertyContext propertyContext = initPropertyContext(context);
    myPropertiesViewer = createPropertiesViewer(propertyRequest, propertyContext);

    myPanel = new JPanel(new BorderLayout());
    myPanel.add(mySplitter, BorderLayout.CENTER);
    myPanel.add(myNotificationPanel, BorderLayout.SOUTH);
    DataManager.registerDataProvider(myPanel, new DataProvider() {
      @Override
      public Object getData(@NonNls String dataId) {
        DataProvider propertiesDataProvider = DataManagerImpl.getDataProviderEx(myPropertiesViewer.getComponent());
        DataProvider contentDataProvider = DataManagerImpl.getDataProviderEx(myContentViewer.getComponent());
        DataProvider defaultDP = myPropertiesViewerFocused ? propertiesDataProvider : contentDataProvider;
        DataProvider fallbackDP = myPropertiesViewerFocused ? contentDataProvider : propertiesDataProvider;
        return DiffUtil.getData(defaultDP, fallbackDP, dataId);
      }
    });

    updatePropertiesPanel();
  }

  @NotNull
  private static DiffViewer createPropertiesViewer(@NotNull DiffRequest propertyRequest, @NotNull MyPropertyContext propertyContext) {
    if (propertyRequest instanceof SvnPropertiesDiffRequest) {
      return SvnPropertiesDiffViewer.create(propertyContext, ((SvnPropertiesDiffRequest)propertyRequest), true);
    }
    else {
      return ErrorDiffTool.INSTANCE.createComponent(propertyContext, propertyRequest);
    }
  }

  @NotNull
  @Override
  public ToolbarComponents init() {
    installListeners();

    processContextHints();

    ToolbarComponents properties = myPropertiesViewer.init();
    ToolbarComponents components = new ToolbarComponents();
    components.toolbarActions = createToolbar(properties.toolbarActions);
    return components;
  }

  @Override
  public void dispose() {
    destroyListeners();

    updateContextHints();

    Disposer.dispose(myPropertiesViewer);

    myPropertyRequest.onAssigned(false);
  }

  private void processContextHints() {
    if (myContext.getUserData(FOCUSED_VIEWER_KEY) == Boolean.TRUE) myPropertiesViewerFocused = true;
    myDumbContentViewer = myContentViewer.getPreferredFocusedComponent() == null;
  }

  private void updateContextHints() {
    if (!myDumbContentViewer && !mySettings.isHideProperties()) myContext.putUserData(FOCUSED_VIEWER_KEY, myPropertiesViewerFocused);
    mySettings.setSplitterProportion(mySplitter.getProportion());
  }

  //
  // Diff
  //

  @Nullable
  private JComponent createNotification() {
    if (myPropertyRequest instanceof ErrorDiffRequest) {
      return createNotification(((ErrorDiffRequest)myPropertyRequest).getMessage());
    }

    List<DiffContent> contents = ((SvnPropertiesDiffRequest)myPropertyRequest).getContents();

    Map<String, PropertyValue> before = getProperties(contents.get(0));
    Map<String, PropertyValue> after = getProperties(contents.get(1));

    if (before.isEmpty() && after.isEmpty()) return null;

    if (!before.keySet().equals(after.keySet())) {
      return createNotification("SVN Properties changed");
    }

    for (String key : before.keySet()) {
      if (!Comparing.equal(before.get(key), after.get(key))) return createNotification("SVN Properties changed");
    }

    return null;
  }

  @NotNull
  private static Map<String, PropertyValue> getProperties(@NotNull DiffContent content) {
    if (content instanceof EmptyContent) return Collections.emptyMap();

    List<PropertyData> properties = ((SvnPropertiesDiffRequest.PropertyContent)content).getProperties();

    Map<String, PropertyValue> map = new HashMap<>();

    for (PropertyData data : properties) {
      if (map.containsKey(data.getName())) LOG.warn("Duplicated property: " + data.getName());
      map.put(data.getName(), data.getValue());
    }

    return map;
  }

  @NotNull
  private static JPanel createNotification(@NotNull String text) {
    return new EditorNotificationPanel().text(text);
  }

  //
  // Misc
  //

  private void updatePropertiesPanel() {
    boolean wasFocused = myContext.isFocused();
    if (!mySettings.isHideProperties()) {
      mySplitter.setSecondComponent(myPropertiesViewer.getComponent());
      myNotificationPanel.setContent(null);
    }
    else {
      mySplitter.setSecondComponent(null);
      myNotificationPanel.setContent(createNotification());
    }
    if (wasFocused) myContext.requestFocus();
  }

  @NotNull
  private List<AnAction> createToolbar(@Nullable List<AnAction> propertiesActions) {
    List<AnAction> result = new ArrayList<>();

    if (propertiesActions != null) result.addAll(propertiesActions);

    result.add(new ToggleHidePropertiesAction());

    return result;
  }

  @NotNull
  private static SvnDiffSettings initSettings(@NotNull DiffContext context) {
    SvnDiffSettings settings = context.getUserData(SvnDiffSettings.KEY);
    if (settings == null) {
      settings = SvnDiffSettings.getSettings();
      context.putUserData(SvnDiffSettings.KEY, settings);
    }
    return settings;
  }

  @NotNull
  private MyPropertyContext initPropertyContext(@NotNull DiffContext context) {
    MyPropertyContext propertyContext = context.getUserData(PROPERTY_CONTEXT_KEY);
    if (propertyContext == null) {
      propertyContext = new MyPropertyContext();
      context.putUserData(PROPERTY_CONTEXT_KEY, propertyContext);
    }
    return propertyContext;
  }

  private void installListeners() {
    myContentViewer.getComponent().addFocusListener(myContentFocusListener);
    myPropertiesViewer.getComponent().addFocusListener(myPropertiesFocusListener);
  }

  private void destroyListeners() {
    myContentViewer.getComponent().removeFocusListener(myContentFocusListener);
    myPropertiesViewer.getComponent().removeFocusListener(myPropertiesFocusListener);
  }

  //
  // Getters
  //

  @NotNull
  @Override
  public JComponent getComponent() {
    return myPanel;
  }

  @Nullable
  @Override
  public JComponent getPreferredFocusedComponent() {
    if (myPropertiesViewerFocused) {
      JComponent component = getPropertiesPreferredFocusedComponent();
      if (component != null) return component;
      return myContentViewer.getPreferredFocusedComponent();
    }
    else {
      JComponent component = myContentViewer.getPreferredFocusedComponent();
      if (component != null) return component;
      return getPropertiesPreferredFocusedComponent();
    }
  }

  @Nullable
  private JComponent getPropertiesPreferredFocusedComponent() {
    if (mySettings.isHideProperties()) return null;
    return myPropertiesViewer.getPreferredFocusedComponent();
  }

  //
  // Actions
  //

  private class ToggleHidePropertiesAction extends ToggleAction implements DumbAware {
    public ToggleHidePropertiesAction() {
      ActionUtil.copyFrom(this, "Subversion.TogglePropertiesDiff");
    }

    @Override
    public boolean isSelected(AnActionEvent e) {
      return !mySettings.isHideProperties();
    }

    @Override
    public void setSelected(AnActionEvent e, boolean state) {
      mySettings.setHideProperties(!state);
      updatePropertiesPanel();
    }
  }

  //
  // Helpers
  //

  private class MyPropertyContext extends DiffContext {
    @Nullable
    @Override
    public Project getProject() {
      return myContext.getProject();
    }

    @Override
    public boolean isWindowFocused() {
      return myContext.isWindowFocused();
    }

    @Override
    public boolean isFocused() {
      return DiffUtil.isFocusedComponent(getProject(), myPropertiesViewer.getComponent());
    }

    @Override
    public void requestFocus() {
      DiffUtil.requestFocus(getProject(), myPropertiesViewer.getPreferredFocusedComponent());
    }
  }

  private class MyFocusListener extends FocusAdapter {
    private final boolean myValue;

    public MyFocusListener(boolean value) {
      myValue = value;
    }

    @Override
    public void focusGained(FocusEvent e) {
      myPropertiesViewerFocused = myValue;
    }
  }

  private static class MySplitter extends Splitter {
    @NotNull private final String myLabelText;

    public MySplitter(@NotNull String text) {
      super(true);
      myLabelText = text;
    }

    @Override
    protected Divider createDivider() {
      return new DividerImpl() {
        @Override
        public void setOrientation(boolean isVerticalSplit) {
          if (!isVertical()) LOG.warn("unsupported state: splitter should be vertical");

          removeAll();

          setCursor(Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR));

          JLabel label = new JLabel(myLabelText);
          label.setFont(UIUtil.getOptionPaneMessageFont());
          label.setForeground(UIUtil.getLabelForeground());
          add(label, new GridBagConstraints(0, 0, 1, 1, 0, 0, GridBagConstraints.CENTER, GridBagConstraints.NONE, JBUI.insets(2), 0, 0));
          setDividerWidth(label.getPreferredSize().height + JBUI.scale(4));

          revalidate();
          repaint();
        }

        @Override
        protected void paintComponent(Graphics g) {
          super.paintComponent(g);
          g.setColor(JBColor.border());
          UIUtil.drawLine(g, 0, 0, getWidth(), 0);
        }
      };
    }
  }
}
