/*
 * Copyright 2010-2012 Luca Garulli (l.garulli--at--orientechnologies.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.
 */
package com.orientechnologies.orient.core.command.traverse;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import com.orientechnologies.orient.core.command.OCommand;
import com.orientechnologies.orient.core.command.OCommandPredicate;
import com.orientechnologies.orient.core.db.record.OIdentifiable;
import com.orientechnologies.orient.core.exception.OCommandExecutionException;

/**
 * Base class for traversing.
 * 
 * @author Luca Garulli
 */
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();

  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 (!context.checkTimeout())
      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);
  }
}
