/*
 * Copyright 2000-2009 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.pom.tree.events.impl;

import com.intellij.lang.ASTNode;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.pom.PomModelAspect;
import com.intellij.pom.event.PomChangeSet;
import com.intellij.pom.tree.events.ChangeInfo;
import com.intellij.pom.tree.events.TreeChange;
import com.intellij.pom.tree.events.TreeChangeEvent;
import com.intellij.psi.impl.source.tree.CompositeElement;
import com.intellij.psi.impl.source.tree.FileElement;
import com.intellij.psi.impl.source.tree.TreeElement;
import com.intellij.util.CharTable;
import gnu.trove.THashMap;
import gnu.trove.TObjectIntHashMap;
import org.jetbrains.annotations.NotNull;

import java.util.*;

/**
 * @author ik
 */
public class TreeChangeEventImpl implements TreeChangeEvent{
  private static final Logger LOG = Logger.getInstance("#com.intellij.pom.tree.events.impl.TreeChangeEventImpl");
  private final Map<ASTNode, TreeChange> myChangedElements = new THashMap<>();
  private List<ASTNode> myChangedInOrder;
  private final List<Set<ASTNode>> myOfEqualDepth = new ArrayList<>(10);
  private final PomModelAspect myAspect;
  private final FileElement myFileElement;

  public TreeChangeEventImpl(@NotNull PomModelAspect aspect, @NotNull FileElement treeElement) {
    myAspect = aspect;
    myFileElement = treeElement;
  }

  @Override
  @NotNull
  public FileElement getRootElement() {
    return myFileElement;
  }

  @Override
  @NotNull
  public ASTNode[] getChangedElements() {
    if (myChangedInOrder == null) {
      myChangedInOrder = new ArrayList<>(myChangedElements.keySet());

      myChangedInOrder.sort(new Comparator<ASTNode>() {
        final Map<ASTNode, int[]> routeMap = new THashMap<>(myChangedElements.size());
        final TObjectIntHashMap<ASTNode> nodeIndex = new TObjectIntHashMap<>(myChangedElements.size());

        @Override
        public int compare(ASTNode o1, ASTNode o2) {
          int[] route = routeMap.get(o1);
          if (route == null) routeMap.put(o1, route = getRoute(o1, nodeIndex));
          int[] route2 = routeMap.get(o2);
          if (route2 == null) routeMap.put(o2, route2 = getRoute(o2, nodeIndex));
          return compareRoutes(route, route2);
        }
      });
    }
    return myChangedInOrder.toArray(new ASTNode[myChangedInOrder.size()]);
  }

  @Override
  public TreeChange getChangesByElement(@NotNull ASTNode element) {
    LOG.assertTrue(isAncestor(element, myFileElement), element);
    return myChangedElements.get(element);
  }

  private static boolean isAncestor(ASTNode thisElement, FileElement fileElement) {
    TreeElement element;
    for (element = (TreeElement)thisElement; element.getTreeParent() != null; element = element.getTreeParent()) {
    }
    return element == fileElement;
  }

  @Override
  public void addElementaryChange(@NotNull ASTNode element, @NotNull ChangeInfo change) {
    LOG.assertTrue(isAncestor(element, myFileElement), element);
    final ASTNode parent = element.getTreeParent();
    if (parent == null) return;
    ASTNode currentParent = parent;
    ASTNode prevParent = element;
    int depth = 0;
    while (currentParent != null) {
      if (myChangedElements.containsKey(currentParent)) {
        final TreeChange changesByElement = getChangesByElement(currentParent);
        final boolean currentParentHasChange = changesByElement.getChangeByChild(prevParent) != null;
        if (currentParentHasChange && prevParent != element) return;
        if (prevParent != element) {
          final ChangeInfo newChange = ChangeInfoImpl.create(ChangeInfo.CONTENTS_CHANGED, prevParent);
          if (change.getChangeType() != ChangeInfo.REMOVED) {
            ((ChangeInfoImpl)newChange).processElementaryChange(change, element);
          }
          change = newChange;
        }
        processElementaryChange(currentParent, prevParent, change, -1);
        return;
      }
      depth++;
      prevParent = currentParent;
      currentParent = currentParent.getTreeParent();
    }
    compactChanges(parent, depth - 1);
    processElementaryChange(parent, element, change, depth - 1);
  }

  private static int getDepth(ASTNode element) {
    int depth = 0;
    while((element = element.getTreeParent()) != null) depth++;
    return depth;
  }

  @Override
  public void clear() {
    myChangedInOrder = null;
    myChangedElements.clear();
    myOfEqualDepth.clear();
  }

  private void processElementaryChange(ASTNode parent, ASTNode element, ChangeInfo change, int depth) {
    TreeChange treeChange = myChangedElements.get(parent);
    if (treeChange == null) {
      treeChange = new TreeChangeImpl(parent);
      myChangedElements.put(parent, treeChange);

      final int index = depth >= 0 ? depth : getDepth(parent);
      addToEqualsDepthList(index, parent);
    }
    treeChange.addChange(element, change);
    if (change.getChangeType() == ChangeInfo.REMOVED) {
      element.putUserData(CharTable.CHAR_TABLE_KEY, myFileElement.getCharTable());
    }
    if (treeChange.isEmpty()) removeAssociatedChanges(parent, depth);
  }

  private void addToEqualsDepthList(final int depth, final ASTNode parent) {
    Set<ASTNode> treeElements = depth < myOfEqualDepth.size() ? myOfEqualDepth.get(depth) : null;
    if(treeElements == null){
      treeElements = new HashSet<>();
      while (depth > myOfEqualDepth.size()) {
        myOfEqualDepth.add(new HashSet<>());
      }
      myOfEqualDepth.add(depth, treeElements);
    }
    treeElements.add(parent);
  }

  private void compactChanges(ASTNode parent, int depth) {
    int currentDepth = myOfEqualDepth.size();
    while(--currentDepth > depth){
      final Set<ASTNode> treeElements = myOfEqualDepth.get(currentDepth);
      if(treeElements == null) continue;
      final Iterator<ASTNode> iterator = treeElements.iterator();
      while (iterator.hasNext()) {
        boolean isUnderCompacted = false;
        final TreeElement treeElement = (TreeElement)iterator.next();
        ASTNode currentParent = treeElement;
        while(currentParent != null){
          if(currentParent == parent){
            isUnderCompacted = true;
            break;
          }
          currentParent = currentParent.getTreeParent();
        }

        if(isUnderCompacted){
          final ChangeInfoImpl compactedChange = ChangeInfoImpl.create(ChangeInfo.CONTENTS_CHANGED, treeElement);
          compactedChange.compactChange(getChangesByElement(treeElement));

          iterator.remove();
          removeAssociatedChanges(treeElement, currentDepth);
          final CompositeElement treeParent = treeElement.getTreeParent();
          final TreeChange changesByElement = getChangesByElement(treeParent);
          if (changesByElement != null) {
            final ChangeInfoImpl changeByChild = (ChangeInfoImpl)changesByElement.getChangeByChild(treeElement);
            if (changeByChild != null) {
              changeByChild.setOldLength(compactedChange.getOldLength());
            }
            else {
              changesByElement.addChange(treeElement, compactedChange);
            }
          }
          else {
            processElementaryChange(treeParent, treeElement, compactedChange, currentDepth - 1);
          }
        }
      }
    }
  }

  private void removeAssociatedChanges(ASTNode treeElement, int depth) {
    if(myChangedElements.remove(treeElement) != null) {
      if (depth < 0) depth = getDepth(treeElement);
      if (depth < myOfEqualDepth.size()) {
        myOfEqualDepth.get(depth < 0 ? getDepth(treeElement) : depth).remove(treeElement);
      }
    }
  }

  private static int[] getRoute(ASTNode node, TObjectIntHashMap<ASTNode> index){
    final List<ASTNode> parents = new ArrayList<>(20);
    while(node != null){
      ASTNode nodeTreeParent = node.getTreeParent();
      if (nodeTreeParent == null) break;
      if (!index.contains(node)) {
        ASTNode current = nodeTreeParent.getFirstChildNode();
        int rootIndex = 0;

        while(current != null){
          index.put(current, rootIndex);
          current = current.getTreeNext();
          rootIndex++;
        }
      }
      parents.add(node);
      node = nodeTreeParent;
    }
    final int[] root = new int[parents.size()];
    for(int i = 0; i < root.length; i++){
      final ASTNode parent = parents.get(root.length - i - 1);
      root[i] = index.get(parent);
    }
    return root;
  }

  private static int compareRoutes(int[] root1, int[] root2){
    final int depth = Math.min(root1.length, root2.length);
    for(int i = 0; i < depth; i++){
      if(root1[i] == root2[i]) continue;
      if(root1[i] > root2[i]){
        return 1;
      }
      else if(root2[i] > root1[i]){
        return -1;
      }
    }
    if(root1.length == root2.length) return 0;
    if(root1.length < root2.length) return 1;
    return -1;
  }

  @Override
  @NotNull
  public PomModelAspect getAspect() {
    return myAspect;
  }

  @Override
  public void merge(@NotNull PomChangeSet blocked) {
    if(!(blocked instanceof TreeChangeEventImpl)) return;
    final TreeChangeEventImpl blockedTreeChange = (TreeChangeEventImpl)blocked;
    final Map<ASTNode, TreeChange> changedElements = new HashMap<>(blockedTreeChange.myChangedElements);
    {// merging conflicting changes
      final Iterator<Map.Entry<ASTNode, TreeChange>> iterator = changedElements.entrySet().iterator();

      while (iterator.hasNext()) {
        final Map.Entry<ASTNode, TreeChange> entry = iterator.next();
        final ASTNode changed = entry.getKey();
        LOG.assertTrue(isAncestor(changed, myFileElement), changed);

        final TreeChange treeChange = myChangedElements.get(changed);
        if(treeChange != null){
          iterator.remove();
          treeChange.add(entry.getValue());
        }
      }
    }
    int depth = 0;

    {
      final Iterator<Map.Entry<ASTNode, TreeChange>> iterator = changedElements.entrySet().iterator();

      while (iterator.hasNext()) {
        final Map.Entry<ASTNode, TreeChange> entry = iterator.next();
        final ASTNode changed = entry.getKey();
        TreeElement prevParent = (TreeElement)changed;
        CompositeElement currentParent = (CompositeElement)changed.getTreeParent();
        while(currentParent != null){
          if(myChangedElements.containsKey(currentParent)){
            final ChangeInfoImpl newChange = ChangeInfoImpl.create(ChangeInfo.CONTENTS_CHANGED, prevParent);
            final int newLength = ((TreeElement)changed).getNotCachedLength();
            final int oldLength = entry.getValue().getOldLength();
            newChange.setOldLength(prevParent.getNotCachedLength() - newLength + oldLength);
            processElementaryChange(currentParent, prevParent, newChange, -1);
            iterator.remove();
            break;
          }
          depth++;
          prevParent = currentParent;
          currentParent = currentParent.getTreeParent();
        }
      }
    }

    {
      for (final Map.Entry<ASTNode, TreeChange> entry : changedElements.entrySet()) {
        final ASTNode changed = entry.getKey();
        myChangedElements.put(changed, entry.getValue());
        addToEqualsDepthList(depth, changed);
        compactChanges(changed, depth);
      }
    }
  }

  public String toString(){
    final StringBuilder buffer = new StringBuilder();
    for (final Map.Entry<ASTNode, TreeChange> entry : myChangedElements.entrySet()) {
      buffer.append(entry.getKey().getElementType().toString());
      buffer.append(": ");
      buffer.append(entry.getValue().toString());
      buffer.append("\n");
    }
    return buffer.toString();
  }
}
