/*
 * Copyright 2003-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 jetbrains.mps.ide.ui.dialogs.properties.roots.editors;

import com.intellij.icons.AllIcons.Modules;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.fileChooser.FileChooser;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel;
import com.intellij.openapi.roots.ui.componentsList.layout.VerticalStackLayout;
import com.intellij.openapi.roots.ui.configuration.actions.IconWithTextAction;
import com.intellij.openapi.ui.Splitter;
import com.intellij.openapi.ui.popup.JBPopup;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.ui.popup.PopupStep;
import com.intellij.openapi.ui.popup.util.BaseListPopupStep;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.ui.ScrollPaneFactory;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.ui.components.JBPanel;
import com.intellij.ui.roots.ToolbarPanel;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UIUtil;
import jetbrains.mps.extapi.persistence.FileBasedModelRoot;
import jetbrains.mps.ide.ui.dialogs.properties.PropertiesBundle;
import jetbrains.mps.ide.ui.dialogs.properties.roots.editors.ModelRootEntryContainer.ContentEntryEditorListener;
import jetbrains.mps.ide.vfs.VirtualFileUtils;
import jetbrains.mps.persistence.MementoImpl;
import jetbrains.mps.persistence.PersistenceRegistry;
import jetbrains.mps.project.AbstractModule;
import jetbrains.mps.project.structure.model.ModelRootDescriptor;
import jetbrains.mps.project.structure.modules.ModuleDescriptor;
import jetbrains.mps.smodel.ModelAccessHelper;
import jetbrains.mps.vfs.FileSystemExtPoint;
import jetbrains.mps.vfs.IFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SRepository;
import org.jetbrains.mps.openapi.persistence.Memento;
import org.jetbrains.mps.openapi.persistence.ModelRoot;
import org.jetbrains.mps.openapi.ui.persistence.ModelRootEntry;

import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Point;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import static com.intellij.openapi.vfs.VfsUtilCore.isAncestor;

/**
 * UIComponent which contains all the module roots.
 * It is located in the module properties dialog.
 */
public class ModelRootContentEntriesEditor implements Disposable {
  private static final Color BACKGROUND_COLOR = UIUtil.getListBackground();

  private final ModuleDescriptor myModuleDescriptor;
  private final SRepository myRepository;
  private final ModelRootEntryPersistence myRootEntryPersistence;
  private final List<ModelRootEntryContainer> myModelRootEntries = new ArrayList<>();
  private ModelRootEntryContainer myFocusedModelRootEntryContainer;
  private final MyContentEntryEditorListener myEditorListener = new MyContentEntryEditorListener();

  private JPanel myEditorsListPanel;
  private JBPanel myEditorPanel;
  private JBPanel myMainPanel;
  private IFile myDefaultFolder;

  public ModelRootContentEntriesEditor(ModuleDescriptor moduleDescriptor, SRepository repository) {
    myModuleDescriptor = moduleDescriptor;
    myRepository = repository;
    myRootEntryPersistence = new ModelRootEntryPersistence().initFromEP();
    for (ModelRootDescriptor descriptor : myModuleDescriptor.getModelRootDescriptors()) {
      ModelRootEntry entry = myRootEntryPersistence.getModelRootEntry(descriptor);
      Disposer.register(this, entry);
      ModelRootEntryContainer container = new ModelRootEntryContainer(entry);
      container.addContentEntryEditorListener(myEditorListener);
      myModelRootEntries.add(container);
    }
    initUI();
  }

  private AnAction getContentEntryActions() {
    final List<AddContentEntryAction> list = new ArrayList<>();
    for (String type : myRootEntryPersistence.getModelRootTypes()) {
      list.add(new AddContentEntryAction(type));
    }

    return new IconWithTextAction(
        PropertiesBundle.message("module.common.roots.add.title"),
        PropertiesBundle.message("module.common.roots.add.tip"),
        Modules.AddContentEntry) {
      @Override
      public void actionPerformed(final AnActionEvent e) {
        if (list.size() == 1) {
          myRepository.getModelAccess().runReadAction(() -> list.get(0).actionPerformed(e));
          return;
        }
        final JBPopup popup = JBPopupFactory.getInstance().createListPopup(
            new BaseListPopupStep<AddContentEntryAction>(null, list) {
              @Override
              public Icon getIconFor(AddContentEntryAction aValue) {
                return aValue.getTemplatePresentation().getIcon();
              }

              @Override
              public boolean hasSubstep(AddContentEntryAction selectedValue) {
                return false;
              }

              @Override
              public boolean isMnemonicsNavigationEnabled() {
                return true;
              }

              @Override
              public PopupStep onChosen(final AddContentEntryAction selectedValue, final boolean finalChoice) {
                return doFinalStep(() -> selectedValue.actionPerformed(e));
              }

              @Override
              @NotNull
              public String getTextFor(AddContentEntryAction value) {
                return value.getTemplatePresentation().getText();
              }
            });
        popup.show(new RelativePoint(myEditorsListPanel, new Point(0, 0)));
      }
    };
  }

  public void initUI() {
    myMainPanel = new JBPanel(new BorderLayout());
    myMainPanel.setPreferredSize(new Dimension(300, 300));

    final JBPanel entriesPanel = new JBPanel(new BorderLayout());

    final DefaultActionGroup group = new DefaultActionGroup();
    group.add(getContentEntryActions());

    myEditorsListPanel = new ScrollablePanel(new VerticalStackLayout());
    myEditorsListPanel.setBackground(BACKGROUND_COLOR);
    JScrollPane scrollPane = ScrollPaneFactory.createScrollPane(myEditorsListPanel);
    scrollPane.setPreferredSize(new Dimension(250, 300));
    entriesPanel.add(new ToolbarPanel(scrollPane, group), BorderLayout.CENTER);

    Splitter splitter = new Splitter(false);
    splitter.setHonorComponentsMinimumSize(true);
    myMainPanel.add(splitter, BorderLayout.CENTER);

    final JBPanel editorsPanel = new JBPanel(new GridBagLayout());
    splitter.setFirstComponent(editorsPanel);
    editorsPanel.add(entriesPanel,
        new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, JBUI.emptyInsets(), 0, 0));

    final JBPanel editorPanel = new JBPanel(new BorderLayout());
    editorPanel.setBorder(BorderFactory.createEtchedBorder());
    myEditorPanel = new JBPanel(new BorderLayout());
    editorPanel.add(myEditorPanel, BorderLayout.CENTER);
    splitter.setSecondComponent(editorPanel);

    for (ModelRootEntryContainer entry : myModelRootEntries) {
      myEditorsListPanel.add(entry.getComponent());
    }

    selectEntry(myModelRootEntries.size() > 0 ? myModelRootEntries.get(0) : null);
  }

  private void selectEntry(ModelRootEntryContainer entry) {
    try {
      if (entry != null && entry.equals(myFocusedModelRootEntryContainer)) {
        return;
      }

      if (myFocusedModelRootEntryContainer != null) {
        myFocusedModelRootEntryContainer.setFocused(false);
      }

      if (entry == null) {
        myFocusedModelRootEntryContainer = null;
        myEditorPanel.removeAll();
        return;
      }

      entry.setFocused(true);
      myEditorPanel.removeAll();
      myEditorPanel.add(entry.getEditor().createComponent(), BorderLayout.CENTER);
      myFocusedModelRootEntryContainer = entry;
    } finally {
      myMainPanel.updateUI();
    }
  }

  private void deleteEntry(ModelRootEntryContainer entry) {
    if (myModelRootEntries.contains(entry)) {
      myEditorsListPanel.remove(entry.getComponent());
      int idx = myModelRootEntries.indexOf(entry);
      myModelRootEntries.remove(entry);
      if (myFocusedModelRootEntryContainer.equals(entry)) {
        selectEntry(myModelRootEntries.size() > 0 ?
            myModelRootEntries.get(Math.max(idx - 1, 0))
            : null);
      } else {
        myMainPanel.updateUI();
      }
    }
  }

  public boolean isModified() {
    List<ModelRootDescriptor> newSet = getDescriptors();
    Collection<ModelRootDescriptor> modelRootDescriptors = myModuleDescriptor.getModelRootDescriptors();
    return !(modelRootDescriptors.containsAll(newSet) && newSet.containsAll(modelRootDescriptors));
  }

  public void apply() {
    myModuleDescriptor.getModelRootDescriptors().clear();
    myModuleDescriptor.getModelRootDescriptors().addAll(getDescriptors());
  }

  private List<ModelRootDescriptor> getDescriptors() {
    List<ModelRootDescriptor> descriptors = new LinkedList<>();
    for (ModelRootEntryContainer container : myModelRootEntries) {
      Memento memento = new MementoImpl();
      container.getModelRoot().save(memento);
      descriptors.add(new ModelRootDescriptor(container.getModelRoot().getType(), memento));
    }
    return descriptors;
  }

  public Collection<ModelRoot> getModelRoots() {
    List<ModelRoot> modelRoots = new LinkedList<>();
    for (ModelRootEntryContainer container : myModelRootEntries) {
      modelRoots.add(container.getModelRoot());
    }
    return modelRoots;
  }

  public Collection<ModelRootEntryContainer> getModelRootsEntries() {
    return myModelRootEntries;
  }

  public JComponent getComponent() {
    return myMainPanel;
  }

  @Override
  public void dispose() {
  }

  /** Set default folder for FileBasedModel root content dir if module is not in repository yet */
  public final void setDefaultFolder(IFile defaultFolder) {
    myDefaultFolder = defaultFolder;
  }

  private class AddContentEntryAction extends IconWithTextAction implements DumbAware {
    private String myType;

    AddContentEntryAction(@NotNull String type) {
      super(type);
      myType = type;
    }

    @Override
    public void actionPerformed(AnActionEvent e) {
      ModelRoot modelRoot = PersistenceRegistry.getInstance().getModelRootFactory(myType).create();
      ModelRootEntry entry = myRootEntryPersistence.getModelRootEntry(modelRoot);
      Disposer.register(ModelRootContentEntriesEditor.this, entry);
      if (entry.getModelRoot() instanceof FileBasedModelRoot) {
        if (!checkAndAddFBModelRoot(entry)) {
          return;
        }
      }
      ModelRootEntryContainer container = new ModelRootEntryContainer(entry);
      container.addContentEntryEditorListener(myEditorListener);
      myModelRootEntries.add(container);
      myEditorsListPanel.add(container.getComponent());
      selectEntry(container);
      myEditorsListPanel.revalidate();
      myEditorsListPanel.repaint();
    }

    private boolean checkAndAddFBModelRoot(ModelRootEntry entry) {
      IFile contentRoot = myDefaultFolder != null ? myDefaultFolder : FileSystemExtPoint.getFS().getFile("");
      final SModule module = new ModelAccessHelper(myRepository).runReadAction(() -> myRepository.getModule(myModuleDescriptor.getId()));
      if (module instanceof AbstractModule) {
        contentRoot = ((AbstractModule) module).getModuleSourceDir() == null
            ? ((AbstractModule) module).getDescriptorFile().getParent()
            : ((AbstractModule) module).getModuleSourceDir();
      }

      Set<VirtualFile> candidatesForIntersection = new HashSet<>();
      for (ModelRootEntryContainer existingEntryContainer : myModelRootEntries) {
        if (entry.getClass().equals(existingEntryContainer.getModelRootEntry().getClass())) {
          FileBasedModelRoot existingModelRoot = (FileBasedModelRoot) existingEntryContainer.getModelRootEntry().getModelRoot();
          candidatesForIntersection.add(VirtualFileUtils.getVirtualFile(existingModelRoot.getContentRoot()));
        }
      }
      FileChooserDescriptor fileChooserDescriptor = new FileChooserDescriptor(false, true, true, false, true, false);
      fileChooserDescriptor.setTitle("Choose root folder for new model root");

      VirtualFile chosen = null;
      while (chosen == null) {
        VirtualFile contentRootVFile = VirtualFileUtils.getProjectVirtualFile(contentRoot);
        VirtualFile[] files = FileChooser.chooseFiles(fileChooserDescriptor, null, null, contentRootVFile);
        if (files.length == 0) {
          return false;
        } else if (files.length > 0) {
          assert files.length == 1; // internal contract of the <code>FileChooser</code>
          chosen = files[0];
          for (VirtualFile candidate : candidatesForIntersection) {
            if (doIntersect(chosen, candidate)) {
              JOptionPane.showMessageDialog(myMainPanel,
                  MessageFormat.format("Can''t create new model root, it intersects with the existing model root: '{1}'! \nChoose another folder", candidate));
              chosen = null;
              break;
            }
          }
        }
      }

      contentRoot = VirtualFileUtils.toIFile(chosen);
      assert contentRoot != null; // : #toIFile method contract
      ((FileBasedModelRoot) entry.getModelRoot()).setContentRoot(contentRoot.getPath());
      return true;
    }

    private boolean doIntersect(VirtualFile chosen, VirtualFile candidate) {
      return isAncestor(candidate, chosen, true) || isAncestor(candidate, chosen, false);
    }
  }

  private final class MyContentEntryEditorListener implements ContentEntryEditorListener {
    @Override
    public void focused(ModelRootEntryContainer entry) {
      selectEntry(entry);
    }

    @Override
    public void delete(ModelRootEntryContainer entry) {
      deleteEntry(entry);
    }

    @Override
    public void dataChanged(ModelRootEntryContainer entry) {
    }
  }
}
