/*
 * Copyright 2003-2016 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.genpriorities;

import com.intellij.ui.CheckboxTree;
import com.intellij.ui.CheckboxTree.CheckboxTreeCellRenderer;
import com.intellij.ui.CheckboxTreeBase.CheckPolicy;
import com.intellij.ui.CheckedTreeNode;
import com.intellij.ui.ColoredSideBorder;
import com.intellij.ui.ColoredTreeCellRenderer;
import com.intellij.ui.JBColor;
import com.intellij.ui.LayeredIcon;
import jetbrains.mps.generator.impl.RuleUtil;
import jetbrains.mps.icons.MPSIcons.Nodes;
import jetbrains.mps.icons.MPSIcons.Nodes.Models;
import jetbrains.mps.lang.smodel.generator.smodelAdapter.SModelOperations;
import jetbrains.mps.project.structure.modules.mappingpriorities.MappingConfig_AbstractRef;
import jetbrains.mps.project.structure.modules.mappingpriorities.MappingConfig_ExternalRef;
import jetbrains.mps.project.structure.modules.mappingpriorities.MappingConfig_RefAllGlobal;
import jetbrains.mps.project.structure.modules.mappingpriorities.MappingConfig_RefAllLocal;
import jetbrains.mps.project.structure.modules.mappingpriorities.MappingConfig_RefSet;
import jetbrains.mps.project.structure.modules.mappingpriorities.MappingConfig_SimpleRef;
import jetbrains.mps.smodel.Generator;
import jetbrains.mps.smodel.ModelAccess;
import jetbrains.mps.smodel.SNodePointer;
import jetbrains.mps.util.Computable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeReference;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleReference;
import org.jetbrains.mps.openapi.module.SRepository;
import org.jetbrains.mps.openapi.persistence.PersistenceFacade;

import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JTree;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Set;

public class GeneratorPrioritiesTree {

  private CheckedTreeNodeEx myRootNode;
  private CheckboxTree myCheckboxTree;

  // FIXME isRight = !isLeft, depGenerators used only when isRight - bloody sh!t. Why not initTree is package local, so that caller could configure this Tree(?!) as needed?
  public GeneratorPrioritiesTree(@NotNull final SRepository repo, @NotNull final Generator generator, @NotNull MappingConfig_AbstractRef mapping, boolean isLeft, final Set<SModuleReference> depGenerators) {
    myRootNode = new CheckedTreeNodeEx(null, "Generators", createRootIcon());
    final boolean isRight = !isLeft;

    repo.getModelAccess().runReadAction(new Runnable() {
      @Override
      public void run() {
        initTree(generator);
        if (isRight) {
          for (SModuleReference ref : depGenerators) {
            SModule gen = ref.resolve(repo);
            if (gen instanceof Generator)
              initTree((Generator) gen);
          }
        }
      }
    });

    setCheckedStateForRoot(myRootNode, mapping);

    myCheckboxTree = new CheckboxTree(getCheckboxTreeCellRenderer(), myRootNode, new CheckPolicy(true, true, false, true));
    myCheckboxTree.addFocusListener(new FocusListener() {
      @Override
      public void focusGained(FocusEvent e) { if(myCheckboxTree.getSelectionCount() == 0) myCheckboxTree.setSelectionRow(0); }
      @Override
      public void focusLost(FocusEvent e) {}
    });
    myCheckboxTree.setRootVisible(isRight);
    expandAllRows(myCheckboxTree);
    checkChildren(myCheckboxTree);
    setCheckedUnder(myRootNode);
  }

  private void initTree(Generator generator) {
    CheckedTreeNodeEx generatorNode = new CheckedTreeNodeEx(generator.getModuleReference(), generator.getAlias(), Nodes.Generator);
    myRootNode.add(generatorNode);

    for (SModel templateModel : generator.getOwnTemplateModels()) {
      String modelName = templateModel.getName().getSimpleName();
      CheckedTreeNodeEx templateNode = new CheckedTreeNodeEx(templateModel.getReference(), modelName, Models.TemplatesModel);
      generatorNode.add(templateNode);
      for (SNode mapping : SModelOperations.roots(templateModel, RuleUtil.concept_MappingConfiguration)) {
        CheckedTreeNodeEx mappingNode = new CheckedTreeNodeEx(mapping.getReference(), mapping.getName(), Nodes.MappingConfig);
        templateNode.add(mappingNode);
      }
    }
  }

  private void setCheckedStateForRoot(CheckedTreeNodeEx node, MappingConfig_AbstractRef mapping) {
    if (mapping instanceof MappingConfig_RefAllGlobal) {
      node.setChecked(true);
    } else if (mapping instanceof MappingConfig_ExternalRef) {
      setCheckedStateForRef(node, (MappingConfig_ExternalRef) mapping);
    } else if (mapping instanceof MappingConfig_RefSet) {
      for (MappingConfig_AbstractRef ref : ((MappingConfig_RefSet) mapping).getMappingConfigs()) {
        setCheckedStateForRef(node, (MappingConfig_ExternalRef) ref);
      }
    }
  }

  private void setCheckedStateForRef(CheckedTreeNodeEx node, MappingConfig_ExternalRef mapping) {
    SModuleReference modRef = mapping.getGenerator();

    Enumeration children = node.children();
    while (children.hasMoreElements()) {
      CheckedTreeNodeEx child = (CheckedTreeNodeEx) children.nextElement();

      if (child.getUserObject().equals(modRef)) {
        MappingConfig_AbstractRef innerOperand = mapping.getMappingConfig();

        if (innerOperand instanceof MappingConfig_RefAllLocal) {
          child.setChecked(true);
        } else if (innerOperand instanceof MappingConfig_SimpleRef) {
          setCheckedStateForModel(child, (MappingConfig_SimpleRef) innerOperand);
        } else if (innerOperand instanceof MappingConfig_RefSet) {
          for (MappingConfig_AbstractRef ref : ((MappingConfig_RefSet) innerOperand).getMappingConfigs()) {
            if (ref instanceof MappingConfig_SimpleRef) {
              setCheckedStateForModel(child, (MappingConfig_SimpleRef) ref);
            } else if (ref instanceof MappingConfig_RefSet) {
              setCheckedStateForSet(child, (MappingConfig_RefSet) ref);
            } else if (ref instanceof MappingConfig_ExternalRef) {
              setCheckedStateForRef((CheckedTreeNodeEx) child.getParent(), (MappingConfig_ExternalRef) ref);
            }
          }
        }
      }
    }
  }

  private void setCheckedStateForSet(CheckedTreeNodeEx node, MappingConfig_RefSet mapping) {
    for (MappingConfig_AbstractRef ref : mapping.getMappingConfigs()) {
      if (ref instanceof MappingConfig_SimpleRef) {
        setCheckedStateForModel(node, (MappingConfig_SimpleRef) ref);
      } else if (ref instanceof MappingConfig_RefSet) {
        setCheckedStateForSet(node, (MappingConfig_RefSet) ref);
      }
    }
  }

  private void setCheckedStateForModel(CheckedTreeNodeEx node, MappingConfig_SimpleRef mapping) {
    SModelReference modRef = PersistenceFacade.getInstance().createModelReference(mapping.getModelUID());

    Enumeration children = node.children();
    while (children.hasMoreElements()) {
      CheckedTreeNodeEx child = (CheckedTreeNodeEx) children.nextElement();

      if (child.getUserObject().equals(modRef)) {
        if (mapping.includesAll()) {
          child.setChecked(true);
        } else {
          setCheckedStateForNode(child, mapping);
        }
      }
    }
  }

  private void setCheckedStateForNode(CheckedTreeNodeEx node, MappingConfig_SimpleRef mapping) {
    SNodeReference nodeRef = new SNodePointer(mapping.getModelUID(), mapping.getNodeID());

    Enumeration children = node.children();
    while (children.hasMoreElements()) {
      CheckedTreeNodeEx child = (CheckedTreeNodeEx) children.nextElement();
      if (child.getUserObject().equals(nodeRef)) {
        child.setChecked(true);
      }
    }
  }

  public MappingConfig_AbstractRef getEditResult() {
    setCheckedUnder(myRootNode);
    return ModelAccess.instance().runReadAction(new Computable<MappingConfig_AbstractRef>() {
      public MappingConfig_AbstractRef compute() {
        return getRootMapping();
      }
    });
  }

  private MappingConfig_AbstractRef getRootMapping() {
    if (!(myRootNode.isChecksUnder())) {
      return new MappingConfig_AbstractRef();
    }
    if (myRootNode.isChecked()) {
      return new MappingConfig_RefAllGlobal();
    }
    List<CheckedTreeNodeEx> chChildren = myRootNode.getChildrenWithChecks();
    if (chChildren.size() == 1) {
      return getGeneratorMappingRef(chChildren.get(0));
    } else {
      MappingConfig_RefSet result = new MappingConfig_RefSet();
      for (CheckedTreeNodeEx child : chChildren) {
        result.getMappingConfigs().add(getGeneratorMappingRef(child));
      }
      return result;
    }
  }

  private MappingConfig_AbstractRef getGeneratorMappingRef(CheckedTreeNodeEx generatorNode) {
    MappingConfig_ExternalRef result = new MappingConfig_ExternalRef();
    assert generatorNode.getUserObject() instanceof SModuleReference;
    result.setGenerator((SModuleReference) generatorNode.getUserObject());
    if (generatorNode.isChecked()) {
      result.setMappingConfig(new MappingConfig_RefAllLocal());
    } else {
      List<CheckedTreeNodeEx> chChildren = generatorNode.getChildrenWithChecks();
      if (chChildren.size() == 1) {
        result.setMappingConfig(getModelMappingRef(chChildren.get(0)));
      } else {
        MappingConfig_RefSet modelsResult = new MappingConfig_RefSet();
        for (CheckedTreeNodeEx child : chChildren) {
          modelsResult.getMappingConfigs().add(getModelMappingRef(child));
        }
        result.setMappingConfig(modelsResult);
      }
    }
    return result;
  }

  private MappingConfig_AbstractRef getModelMappingRef(CheckedTreeNodeEx templateModelNode) {
    assert templateModelNode.getUserObject() instanceof SModelReference;
    if (!(templateModelNode.isChecksUnder())) {
      return new MappingConfig_AbstractRef();
    }
    if (templateModelNode.isChecked()) {
      MappingConfig_SimpleRef result = new MappingConfig_SimpleRef();
      result.setModelUID(templateModelNode.getUserObject().toString());
      result.setNodeID("*");
      return result;
    }
    List<CheckedTreeNodeEx> chChildren = templateModelNode.getChildrenWithChecks();
    if (chChildren.size() == 1) {
      return getNodeMappingRef(chChildren.get(0));
    } else {
      MappingConfig_RefSet result = new MappingConfig_RefSet();
      for (CheckedTreeNodeEx child : chChildren) {
        result.getMappingConfigs().add(getNodeMappingRef(child));
      }
      return result;
    }
  }

  private MappingConfig_AbstractRef getNodeMappingRef(CheckedTreeNodeEx mapConfigNode) {
    assert mapConfigNode.getUserObject() instanceof SNodeReference;
    SNodeReference mapConfigRef = (SNodeReference) mapConfigNode.getUserObject();
    MappingConfig_SimpleRef result = new MappingConfig_SimpleRef();
    result.setModelUID(mapConfigRef.getModelReference().toString());
    result.setNodeID(mapConfigRef.getNodeId().toString());
    return result;
  }

  private static boolean setCheckedUnder(CheckedTreeNodeEx root) {
    boolean childChecks = false;
    Enumeration<CheckedTreeNodeEx> children = root.children();
    while (children.hasMoreElements()) {
      CheckedTreeNodeEx child = children.nextElement();
      childChecks = childChecks | setCheckedUnder(child);
    }
    boolean checksUnder = root.isChecked() || childChecks;
    root.setChecksUnder(checksUnder);
    return checksUnder;
  }

  public CheckboxTree getTree() {
    return myCheckboxTree;
  }

  public JComponent getTreePanel() {
    return myCheckboxTree;
  }

  public static CheckboxTreeCellRenderer getCheckboxTreeCellRenderer() {
    return getCheckboxTreeCellRenderer(true);
  }
  public static CheckboxTreeCellRenderer getCheckboxTreeCellRenderer(final boolean withCheckboxes) {
    return new CheckboxTreeCellRenderer() {
      @Override
      public void customizeRenderer(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
        super.customizeRenderer(tree, value, selected, expanded, leaf, row, hasFocus);
        ColoredTreeCellRenderer renderer = getTextRenderer();
        JCheckBox checkBox = getCheckbox();

        if (value instanceof CheckedTreeNodeEx) {
          CheckedTreeNodeEx checkedTreeNode = (CheckedTreeNodeEx) value;

          renderer.append(checkedTreeNode.getPresentation());
          renderer.setIcon(checkedTreeNode.getIcon());

          checkBox.setSelected(checkedTreeNode.isChecked());
          checkBox.setVisible(withCheckboxes);

          if(checkedTreeNode.equals(tree.getModel().getRoot())) {
            if(!GeneratorPrioritiesTree.setCheckedUnder(checkedTreeNode))
              tree.setBorder(new ColoredSideBorder(JBColor.RED, JBColor.RED, JBColor.RED, JBColor.RED, 1));
            else
              tree.setBorder(BorderFactory.createEmptyBorder());
          }
        }
      }
    };
  }

  public static final void expandAllRows(JTree tree) {
    for (int i = 0; i < tree.getRowCount(); i++) {
      tree.expandRow(i);
    }
  }

  private static void checkChildren(CheckboxTree checkboxTree) {
    if(checkboxTree.getModel().getRoot() instanceof CheckedTreeNodeEx) {
      Queue<CheckedTreeNodeEx> treeNodes = new LinkedList<CheckedTreeNodeEx>();
      treeNodes.add((CheckedTreeNodeEx) checkboxTree.getModel().getRoot());
      while (!treeNodes.isEmpty()) {
        CheckedTreeNodeEx treeNode = treeNodes.poll();
        treeNodes.addAll(Collections.list(treeNode.children()));
        if(treeNode.getParent() instanceof CheckedTreeNodeEx && ((CheckedTreeNodeEx)treeNode.getParent()).isChecked()) {
          treeNode.setChecked(true);
        }
      }
    }
  }

  private static Icon createRootIcon() {
    LayeredIcon layeredIcon = new LayeredIcon(3);
    layeredIcon.setIcon(Nodes.Generator, 0, +2, -2);
    layeredIcon.setIcon(Nodes.Generator, 1, 0, 0);
    layeredIcon.setIcon(Nodes.Generator, 2, -2, +2);
    return layeredIcon;
  }

  private static class CheckedTreeNodeEx extends CheckedTreeNode {
    private boolean myChecksUnder;
    private String myText;
    private Icon myIcon;

    public CheckedTreeNodeEx(Object value) {
      super(value);
      setChecked(false);
      myChecksUnder = false;
    }

    public CheckedTreeNodeEx(Object value, String text, Icon icon) {
      this(value);
      myText = text;
      myIcon = icon;
    }

    public boolean isChecksUnder() {
      return myChecksUnder;
    }

    public void setChecksUnder(boolean checksUnder) {
      myChecksUnder = checksUnder;
    }

    public String getPresentation() {
      return myText;
    }

    public Icon getIcon() {
      return myIcon;
    }

    /*package*/ List<CheckedTreeNodeEx> getChildrenWithChecks() {
      List<CheckedTreeNodeEx> result = new ArrayList<CheckedTreeNodeEx>();
      Enumeration children = children();
      while (children.hasMoreElements()) {
        CheckedTreeNodeEx child = (CheckedTreeNodeEx) children.nextElement();
        if (child.isChecksUnder()) {
          result.add(child);
        }
      }
      return result;
    }
  }
}
