/*
 *
 *  *  Copyright 2010-2016 OrientDB LTD (http://orientdb.com)
 *  *
 *  *  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.
 *  *
 *  * For more information: http://orientdb.com
 *
 */
package com.orientechnologies.orient.core.command.traverse;

import com.orientechnologies.orient.core.command.OCommand;
import com.orientechnologies.orient.core.command.OCommandExecutorAbstract;
import com.orientechnologies.orient.core.command.OCommandPredicate;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.exception.OCommandExecutionException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

/**
 * Base class for traversing.
 *
 * @author Luca Garulli (l.garulli--(at)--orientdb.com)
 */
public class OTraverse implements OCommand, Iterable<OIdentifiable>, Iterator<OIdentifiable> {
  private OCommandPredicate predicate;
  private Iterator<? extends OIdentifiable> target;
  private List<Object> fields = new ArrayList<Object>();
  private long resultCount = 0;
  private long limit = 0;
  private OIdentifiable lastTraversed;
  private STRATEGY strategy = STRATEGY.DEPTH_FIRST;
  private OTraverseContext context = new OTraverseContext();
  private int maxDepth = -1;

  public enum STRATEGY {
    DEPTH_FIRST,
    BREADTH_FIRST
  }

  /*
   * Executes a traverse collecting all the result in the returning List<OIdentifiable>. This could be memory expensive because for
   * large results the list could be huge. it's always better to use it as an Iterable and lazy fetch each result on next() call.
   *
   * @see com.orientechnologies.orient.core.command.OCommand#execute()
   */
  public List<OIdentifiable> execute() {
    final List<OIdentifiable> result = new ArrayList<OIdentifiable>();
    while (hasNext()) result.add(next());
    return result;
  }

  public OTraverseAbstractProcess<?> nextProcess() {
    return context.next();
  }

  public boolean hasNext() {
    if (limit > 0 && resultCount >= limit) return false;

    if (lastTraversed == null)
      // GET THE NEXT
      lastTraversed = next();

    if (lastTraversed == null && !context.isEmpty())
      throw new IllegalStateException("Traverse ended abnormally");

    if (!OCommandExecutorAbstract.checkInterruption(context)) return false;

    // BROWSE ALL THE RECORDS
    return lastTraversed != null;
  }

  public OIdentifiable next() {
    if (Thread.interrupted())
      throw new OCommandExecutionException("The traverse execution has been interrupted");

    if (lastTraversed != null) {
      // RETURN LATEST AND RESET IT
      final OIdentifiable result = lastTraversed;
      lastTraversed = null;
      return result;
    }

    if (limit > 0 && resultCount >= limit) return null;

    OIdentifiable result;
    OTraverseAbstractProcess<?> toProcess;
    // RESUME THE LAST PROCESS
    while ((toProcess = nextProcess()) != null) {
      result = toProcess.process();
      if (result != null) {
        resultCount++;
        return result;
      }
    }

    return null;
  }

  public void remove() {
    throw new UnsupportedOperationException("remove()");
  }

  public Iterator<OIdentifiable> iterator() {
    return this;
  }

  public OTraverseContext getContext() {
    return context;
  }

  public OTraverse target(final Iterable<? extends OIdentifiable> iTarget) {
    return target(iTarget.iterator());
  }

  public OTraverse target(final OIdentifiable... iRecords) {
    final List<OIdentifiable> list = new ArrayList<OIdentifiable>();
    Collections.addAll(list, iRecords);
    return target(list.iterator());
  }

  @SuppressWarnings("unchecked")
  public OTraverse target(final Iterator<? extends OIdentifiable> iTarget) {
    target = iTarget;
    context.reset();
    new OTraverseRecordSetProcess(this, (Iterator<OIdentifiable>) target, OTraversePath.empty());
    return this;
  }

  public Iterator<? extends OIdentifiable> getTarget() {
    return target;
  }

  public OTraverse predicate(final OCommandPredicate iPredicate) {
    predicate = iPredicate;
    return this;
  }

  public OCommandPredicate getPredicate() {
    return predicate;
  }

  public OTraverse field(final Object iField) {
    if (!fields.contains(iField)) fields.add(iField);
    return this;
  }

  public OTraverse fields(final Collection<Object> iFields) {
    for (Object f : iFields) field(f);
    return this;
  }

  public OTraverse fields(final String... iFields) {
    for (String f : iFields) field(f);
    return this;
  }

  public List<Object> getFields() {
    return fields;
  }

  public long getLimit() {
    return limit;
  }

  public OTraverse limit(final long iLimit) {
    if (iLimit < -1) throw new IllegalArgumentException("Limit cannot be negative. 0 = infinite");
    this.limit = iLimit;
    return this;
  }

  @Override
  public String toString() {
    return String.format(
        "OTraverse.target(%s).fields(%s).limit(%d).predicate(%s)",
        target, fields, limit, predicate);
  }

  public long getResultCount() {
    return resultCount;
  }

  public OIdentifiable getLastTraversed() {
    return lastTraversed;
  }

  public STRATEGY getStrategy() {
    return strategy;
  }

  public void setStrategy(STRATEGY strategy) {
    this.strategy = strategy;
    context.setStrategy(strategy);
  }

  public int getMaxDepth() {
    return maxDepth;
  }

  public void setMaxDepth(final int maxDepth) {
    this.maxDepth = maxDepth;
  }
}
