/*
 * Copyright 2003-2013 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.nodeEditor.cells;

import jetbrains.mps.openapi.editor.cells.CellTraversalUtil;
import jetbrains.mps.openapi.editor.cells.EditorCell;
import jetbrains.mps.openapi.editor.cells.EditorCell_Collection;
import jetbrains.mps.util.AndCondition;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.util.Condition;

/**
 * Semen Alperovich
 * 03 15, 2013
 */
public class CellFinderUtil {
  // no one can instantiate this class.
  private CellFinderUtil() {
  }

  public enum Finder {
    FIRST_ERROR,
    LAST_ERROR,
    FIRST_SELECTABLE_LEAF,
    LAST_SELECTABLE_LEAF,
    FIRST_EDITABLE,
    LAST_EDITABLE
  }

  // You can find child by this common method or by specialized methods below
  public static EditorCell findChild(@NotNull EditorCell cell, @NotNull Finder finder, boolean includeThis) {
    switch (finder) {
      case FIRST_ERROR:
        return findFirstError(cell, includeThis);
      case LAST_ERROR:
        return findLastError(cell, includeThis);
      case FIRST_EDITABLE:
        return findFirstEditable(cell, includeThis);
      case LAST_EDITABLE:
        return findLastEditable(cell, includeThis);
      case FIRST_SELECTABLE_LEAF:
        return findFirstSelectableLeaf(cell, includeThis);
      case LAST_SELECTABLE_LEAF:
        return findLastSelectableLeaf(cell, includeThis);
      default:
        return null;
    }
  }

  public static EditorCell findChild(@NotNull EditorCell cell, @NotNull Finder finder) {
    return findChild(cell, finder, false);
  }

  public static EditorCell findChildByManyFinders(@NotNull EditorCell cell, boolean includeThis, @NotNull Finder... finders) {
    for (Finder finder : finders) {
      EditorCell result = findChild(cell, finder, includeThis);
      if (result != null) {
        return result;
      }
    }
    return null;
  }

  public static EditorCell findChildByManyFinders(@NotNull EditorCell cell, @NotNull Finder... finders) {
    return findChildByManyFinders(cell, false, finders);
  }

  public static EditorCell_Collection findParent(@NotNull EditorCell cell, @NotNull Condition<EditorCell_Collection> condition) {
    if (cell instanceof EditorCell_Collection && condition.met(
        ((EditorCell_Collection) cell))) {
      return ((EditorCell_Collection) cell);
    }

    for (EditorCell_Collection parent = cell.getParent(); parent != null; parent = parent.getParent()) {
      if (condition.met(parent)) {
        return parent;
      }
    }
    return null;
  }

  public static EditorCell findChildByCondition(@NotNull EditorCell cell, @NotNull Condition<EditorCell> condition, boolean forward, boolean includeThis) {
    for (EditorCell current : CellTraversalUtil.iterateTree(cell, cell, forward).skipStart(!includeThis)) {
      if (condition.met(current)) {
        return current;
      }
    }
    return null;
  }

  public static <C extends EditorCell> C findChildByConditionAndClass(@NotNull EditorCell cell, @NotNull final Condition<EditorCell> condition,
      final Class<C> clazz, boolean forward, boolean includeThis) {
    return ((C) findChildByCondition(cell, new AndCondition<EditorCell>(new ByClassCondition(clazz), condition), forward, includeThis));
  }

  public static <C extends EditorCell> C findChildByConditionAndClass(@NotNull EditorCell cell, @NotNull final Condition<EditorCell> condition,
      final Class<C> clazz, boolean forward) {
    return findChildByConditionAndClass(cell, condition, clazz, forward, false);
  }

  public static EditorCell findChildByCondition(@NotNull EditorCell cell, @NotNull Condition<EditorCell> condition, boolean forward) {
    return findChildByCondition(cell, condition, forward, false);
  }

  public static <C extends EditorCell> C findChildByClass(@NotNull EditorCell cell, @NotNull final Class<C> clazz, boolean forward, boolean includeThis) {
    return ((C) findChildByCondition(cell, new ByClassCondition(clazz), forward, includeThis));
  }

  public static <C extends EditorCell> C findChildByClass(@NotNull EditorCell cell, @NotNull final Class<C> clazz, boolean forward) {
    return findChildByClass(cell, clazz, forward, false);
  }


  public static EditorCell findChildById(@NotNull EditorCell cell, @NotNull final SNode node, @NotNull final String cellId, boolean includeThis) {
    return findChildByCondition(cell, new ByIdCondition(node, cellId), true, includeThis);
  }

  public static EditorCell findChildById(@NotNull EditorCell cell, @NotNull final SNode node, final String cellId) {
    return findChildById(cell, node, cellId, false);
  }

  private static EditorCell_Label findEditable(@NotNull EditorCell cell, boolean forward, boolean includeThis) {
    return findChildByConditionAndClass(cell, jetbrains.mps.openapi.editor.cells.CellConditions.EDITABLE, EditorCell_Label.class, forward, includeThis);
  }

  public static EditorCell_Label findFirstEditable(@NotNull EditorCell cell, boolean includeThis) {
    return findEditable(cell, true, includeThis);
  }

  public static EditorCell_Label findFirstEditable(@NotNull EditorCell cell) {
    return findFirstEditable(cell, false);
  }

  public static EditorCell_Label findLastEditable(@NotNull EditorCell cell, boolean includeThis) {
    return findEditable(cell, false, includeThis);
  }

  public static EditorCell_Label findLastEditable(@NotNull EditorCell cell) {
    return findLastEditable(cell, false);
  }

  private static EditorCell_Label findError(@NotNull EditorCell cell, boolean forward, boolean includeThis) {
    return findChildByConditionAndClass(cell, CellConditions.ERROR_CONDITION, EditorCell_Label.class, forward, includeThis);
  }

  public static EditorCell_Label findFirstError(@NotNull EditorCell cell, boolean includeThis) {
    return findError(cell, true, includeThis);
  }

  public static EditorCell_Label findFirstError(@NotNull EditorCell cell) {
    return findFirstError(cell, false);
  }

  public static EditorCell_Label findLastError(@NotNull EditorCell cell, boolean includeThis) {
    return findError(cell, false, includeThis);
  }

  public static EditorCell_Label findLastError(@NotNull EditorCell cell) {
    return findLastError(cell, false);
  }

  private static EditorCell findSelectableLeaf(@NotNull EditorCell cell, boolean forward, boolean includeThis) {
    return findChildByCondition(cell, jetbrains.mps.openapi.editor.cells.CellConditions.SELECTABLE_lEAF, forward, includeThis);
  }

  public static EditorCell findFirstSelectableLeaf(@NotNull EditorCell cell, boolean includeThis) {
    return findSelectableLeaf(cell, true, includeThis);
  }

  public static EditorCell findFirstSelectableLeaf(@NotNull EditorCell cell) {
    return findFirstSelectableLeaf(cell, false);
  }

  public static EditorCell findLastSelectableLeaf(@NotNull EditorCell cell, boolean includeThis) {
    return findSelectableLeaf(cell, false, includeThis);
  }

  public static EditorCell findLastSelectableLeaf(@NotNull EditorCell cell) {
    return findLastSelectableLeaf(cell, false);
  }


  private static class ByClassCondition implements Condition<EditorCell> {
    private Class myClass;

    public ByClassCondition(Class clazz) {
      myClass = clazz;
    }

    @Override
    public boolean met(EditorCell cell) {
      return myClass.isInstance(cell);
    }
  }

  private static class ByIdCondition implements Condition<EditorCell> {
    private SNode myNode;
    private String myCellId;

    public ByIdCondition(SNode node, String id) {
      myNode = node;
      myCellId = id;
    }


    @Override
    public boolean met(EditorCell object) {
      return object.getSNode() == myNode && myCellId.equals(object.getCellId());
    }

  }
}
