/*
 * 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.util;

import com.intellij.diff.DiffContext;
import com.intellij.diff.impl.DiffSettingsHolder.DiffSettings;
import com.intellij.diff.requests.DiffRequest;
import com.intellij.diff.tools.holders.BinaryEditorHolder;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.fileEditor.FileEditorState;
import com.intellij.openapi.fileEditor.FileEditorStateLevel;
import com.intellij.openapi.fileEditor.TransferableFileEditorState;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Key;
import com.intellij.ui.ToggleActionButton;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class TransferableFileEditorStateSupport {
  @NotNull private static final Key<Map<String, Map<String, String>>> TRANSFERABLE_FILE_EDITOR_STATE =
    Key.create("Diff.TransferableFileEditorState");

  private static final Condition<BinaryEditorHolder> IS_SUPPORTED = holder -> {
    return getEditorState(holder.getEditor()) != null;
  };

  @NotNull private final DiffSettings mySettings;
  @NotNull private final List<BinaryEditorHolder> myHolders;
  private final boolean mySupported;

  public TransferableFileEditorStateSupport(@NotNull DiffSettings settings,
                                            @NotNull List<BinaryEditorHolder> holders,
                                            @NotNull Disposable disposable) {
    mySettings = settings;
    myHolders = holders;
    mySupported = ContainerUtil.or(myHolders, IS_SUPPORTED);
    new MySynchronizer(ContainerUtil.filter(myHolders, IS_SUPPORTED)).install(disposable);
  }

  public boolean isSupported() {
    return mySupported;
  }

  public boolean isEnabled() {
    return mySettings.isSyncBinaryEditorSettings();
  }

  public void setEnabled(boolean enabled) {
    mySettings.setSyncBinaryEditorSettings(enabled);
  }

  public void processContextHints(@NotNull DiffRequest request, @NotNull DiffContext context) {
    if (!isEnabled()) return;

    for (BinaryEditorHolder holder : myHolders) {
      FileEditor editor = holder.getEditor();
      TransferableFileEditorState state = getEditorState(holder.getEditor());
      if (state != null) {
        readContextData(context, editor, state);
      }
    }
  }

  public void updateContextHints(@NotNull DiffRequest request, @NotNull DiffContext context) {
    if (!isEnabled()) return;

    Set<String> updated = ContainerUtil.newHashSet();

    for (BinaryEditorHolder holder : myHolders) {
      TransferableFileEditorState state = getEditorState(holder.getEditor());
      if (state != null) {
        boolean processed = !updated.add(state.getEditorId());
        if (!processed) writeContextData(context, state);
      }
    }
  }

  @NotNull
  public AnAction createToggleAction() {
    return new ToggleSynchronousEditorStatesAction(this);
  }

  private static void readContextData(@NotNull DiffContext context,
                                      @NotNull FileEditor editor,
                                      @NotNull TransferableFileEditorState state) {
    Map<String, Map<String, String>> map = context.getUserData(TRANSFERABLE_FILE_EDITOR_STATE);
    Map<String, String> options = map != null ? map.get(state.getEditorId()) : null;
    if (options == null) return;

    state.setTransferableOptions(options);
    editor.setState(state);
  }

  private static void writeContextData(@NotNull DiffContext context, @NotNull TransferableFileEditorState state) {
    Map<String, Map<String, String>> map = context.getUserData(TRANSFERABLE_FILE_EDITOR_STATE);
    if (map == null) {
      map = ContainerUtil.newHashMap();
      context.putUserData(TRANSFERABLE_FILE_EDITOR_STATE, map);
    }

    map.put(state.getEditorId(), state.getTransferableOptions());
  }

  @Nullable
  private static TransferableFileEditorState getEditorState(@NotNull FileEditor editor) {
    FileEditorState state = editor.getState(FileEditorStateLevel.FULL);
    return state instanceof TransferableFileEditorState ? (TransferableFileEditorState)state : null;
  }

  private class MySynchronizer implements PropertyChangeListener {
    @NotNull private final List<? extends FileEditor> myEditors;

    private boolean myDuringUpdate = false;

    public MySynchronizer(@NotNull List<BinaryEditorHolder> editors) {
      myEditors = ContainerUtil.map(editors, holder -> holder.getEditor());
    }

    public void install(@NotNull Disposable disposable) {
      if (myEditors.size() < 2) return;

      for (FileEditor editor : myEditors) {
        editor.addPropertyChangeListener(this);
      }

      Disposer.register(disposable, new Disposable() {
        @Override
        public void dispose() {
          for (FileEditor editor : myEditors) {
            editor.removePropertyChangeListener(MySynchronizer.this);
          }
        }
      });
    }

    @Override
    public void propertyChange(PropertyChangeEvent evt) {
      if (myDuringUpdate || !isEnabled()) return;
      if (!(evt.getSource() instanceof FileEditor)) return;

      TransferableFileEditorState sourceState = getEditorState(((FileEditor)evt.getSource()));
      if (sourceState == null) return;

      Map<String, String> options = sourceState.getTransferableOptions();
      String id = sourceState.getEditorId();

      for (FileEditor editor : myEditors) {
        if (evt.getSource() != editor) {
          updateEditor(editor, id, options);
        }
      }
    }

    private void updateEditor(@NotNull FileEditor editor, @NotNull String id, @NotNull Map<String, String> options) {
      try {
        myDuringUpdate = true;
        TransferableFileEditorState state = getEditorState(editor);
        if (state != null && state.getEditorId().equals(id)) {
          state.setTransferableOptions(options);
          editor.setState(state);
        }
      }
      finally {
        myDuringUpdate = false;
      }
    }
  }

  private class ToggleSynchronousEditorStatesAction extends ToggleActionButton implements DumbAware {
    @NotNull private final TransferableFileEditorStateSupport mySupport;

    public ToggleSynchronousEditorStatesAction(@NotNull TransferableFileEditorStateSupport support) {
      super("Synchronize Editors Settings", AllIcons.Actions.SyncPanels);
      mySupport = support;
    }

    @Override
    public boolean isVisible() {
      return mySupport.isSupported();
    }

    @Override
    public boolean isSelected(AnActionEvent e) {
      return mySupport.isEnabled();
    }

    @Override
    public void setSelected(AnActionEvent e, boolean state) {
      mySupport.setEnabled(state);
    }
  }
}
