/*
 * Copyright 2000-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 com.intellij.dupLocator;

import com.intellij.dupLocator.util.NodeFilter;
import com.intellij.psi.PsiElement;
import com.intellij.dupLocator.iterators.ArrayBackedNodeIterator;
import com.intellij.dupLocator.iterators.FilteringNodeIterator;
import com.intellij.dupLocator.iterators.NodeIterator;
import com.intellij.dupLocator.iterators.SiblingNodeIterator;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * @author Eugene.Kudelevsky
 */
public abstract class AbstractMatchingVisitor {

  public abstract boolean matchSequentially(NodeIterator nodes, NodeIterator nodes2);

  public abstract boolean match(PsiElement element1, PsiElement element2);

  protected abstract boolean doMatchInAnyOrder(NodeIterator elements, NodeIterator elements2);

  public boolean matchSequentially(@NotNull PsiElement[] elements1, @NotNull PsiElement[] element2) {
    return matchSequentially(new FilteringNodeIterator(new ArrayBackedNodeIterator(elements1), getNodeFilter()),
                             new FilteringNodeIterator(new ArrayBackedNodeIterator(element2), getNodeFilter()));
  }

  @NotNull
  protected abstract NodeFilter getNodeFilter();

  public boolean matchOptionally(@Nullable PsiElement element1, @Nullable PsiElement element2) {
    return element1 == null && isLeftLooseMatching() ||
           element2 == null && isRightLooseMatching() ||
           match(element1, element2);
  }

  public boolean matchSons(final PsiElement el1, final PsiElement el2) {
    if (el1 == null || el2 == null) return el1 == el2;
    return matchSequentially(el1.getFirstChild(), el2.getFirstChild());
  }

  public boolean matchSonsOptionally(final PsiElement element, final PsiElement element2) {
    if (element == null && isLeftLooseMatching()) {
      return true;
    }
    if (element2 == null && isRightLooseMatching()) {
      return true;
    }
    if (element == null || element2 == null) {
      return element == element2;
    }
    return matchSequentiallyOptionally(element.getFirstChild(), element2.getFirstChild());
  }

  public final boolean matchSonsInAnyOrder(PsiElement element1, PsiElement element2) {
    if (element1 == null && isLeftLooseMatching()) {
      return true;
    }
    if (element2 == null && isRightLooseMatching()) {
      return true;
    }
    if (element1 == null || element2 == null) {
      return element1 == element2;
    }
    PsiElement e = element1.getFirstChild();
    PsiElement e2 = element2.getFirstChild();
    return (e == null && isLeftLooseMatching()) ||
           (e2 == null && isRightLooseMatching()) ||
           matchInAnyOrder(new FilteringNodeIterator(new SiblingNodeIterator(e), getNodeFilter()),
                           new FilteringNodeIterator(new SiblingNodeIterator(e2), getNodeFilter()));
  }

  public boolean matchOptionally(@NotNull PsiElement[] elements1, @NotNull PsiElement[] elements2) {
    return (elements1.length == 0 && isLeftLooseMatching()) ||
           (elements2.length == 0 && isRightLooseMatching()) ||
           matchSequentially(elements1, elements2);
  }

  public final boolean matchInAnyOrder(final PsiElement[] elements, final PsiElement[] elements2) {
    if (elements == elements2) return true;

    return matchInAnyOrder(
      new ArrayBackedNodeIterator(elements),
      new ArrayBackedNodeIterator(elements2)
    );
  }

  public boolean isLeftLooseMatching() {
    return true;
  }

  public boolean isRightLooseMatching() {
    return true;
  }

  public boolean matchSequentially(PsiElement el1, PsiElement el2) {
    //if (el1==null || el2==null) return el1 == el2;
    return matchSequentially(new FilteringNodeIterator(new SiblingNodeIterator(el1), getNodeFilter()),
                             new FilteringNodeIterator(new SiblingNodeIterator(el2), getNodeFilter()));
  }

  public boolean matchSequentiallyOptionally(PsiElement el1, PsiElement el2) {
    return (el1 == null && isLeftLooseMatching()) ||
           (el2 == null && isRightLooseMatching()) ||
           matchSequentially(new FilteringNodeIterator(new SiblingNodeIterator(el1), getNodeFilter()),
                             new FilteringNodeIterator(new SiblingNodeIterator(el2), getNodeFilter()));
  }

  public final boolean matchInAnyOrder(final NodeIterator elements, final NodeIterator elements2) {
    if ((!elements.hasNext() && isLeftLooseMatching()) ||
        (!elements2.hasNext() && isRightLooseMatching()) ||
        (!elements.hasNext() && !elements2.hasNext())
      ) {
      return true;
    }

    return doMatchInAnyOrder(elements, elements2);
  }
}
