/*
 * Copyright (C) 2008 Google Inc.
 *
 * 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 org.mcteam.factions.gson;

import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Type;

/**
 * Provides ability to apply a visitor to an object and all of its fields
 * recursively.
 * 
 * @author Inderjeet Singh
 * @author Joel Leitch
 */
final class ObjectNavigator {

  public interface Visitor {
    public void start(ObjectTypePair node);

    public void end(ObjectTypePair node);

    /**
     * This is called before the object navigator starts visiting the current
     * object
     */
    void startVisitingObject(Object node);

    /**
     * This is called to visit the current object if it is an array
     */
    void visitArray(Object array, Type componentType);

    /**
     * This is called to visit an object field of the current object
     */
    void visitObjectField(FieldAttributes f, Type typeOfF, Object obj);

    /**
     * This is called to visit an array field of the current object
     */
    void visitArrayField(FieldAttributes f, Type typeOfF, Object obj);

    /**
     * This is called to visit an object using a custom handler
     * 
     * @return true if a custom handler exists, false otherwise
     */
    public boolean visitUsingCustomHandler(ObjectTypePair objTypePair);

    /**
     * This is called to visit a field of the current object using a custom
     * handler
     */
    public boolean visitFieldUsingCustomHandler(FieldAttributes f, Type actualTypeOfField,
        Object parent);

    /**
     * Retrieve the current target
     */
    Object getTarget();

    void visitPrimitive(Object primitive);
  }

  private final ExclusionStrategy exclusionStrategy;
  private final ObjectTypePair objTypePair;

  /**
   * @param objTypePair
   *          The object,type (fully genericized) being navigated
   * @param exclusionStrategy
   *          the concrete strategy object to be used to filter out fields of an
   *          object.
   */
  ObjectNavigator(ObjectTypePair objTypePair, ExclusionStrategy exclusionStrategy) {
    Preconditions.checkNotNull(exclusionStrategy);

    this.objTypePair = objTypePair;
    this.exclusionStrategy = exclusionStrategy;
  }

  /**
   * Navigate all the fields of the specified object. If a field is null, it
   * does not get visited.
   */
  public void accept(Visitor visitor) {
    TypeInfo objTypeInfo = new TypeInfo(objTypePair.type);
    if (exclusionStrategy.shouldSkipClass(objTypeInfo.getRawClass())) {
      return;
    }
    boolean visitedWithCustomHandler = visitor.visitUsingCustomHandler(objTypePair);
    if (!visitedWithCustomHandler) {
      Object obj = objTypePair.getObject();
      Object objectToVisit = (obj == null) ? visitor.getTarget() : obj;
      if (objectToVisit == null) {
        return;
      }
      objTypePair.setObject(objectToVisit);
      visitor.start(objTypePair);
      try {
        if (objTypeInfo.isArray()) {
          visitor.visitArray(objectToVisit, objTypePair.type);
        } else if (objTypeInfo.getActualType() == Object.class
            && isPrimitiveOrString(objectToVisit)) {
          // TODO(Joel): this is only used for deserialization of "primitives"
          // we should rethink this!!!
          visitor.visitPrimitive(objectToVisit);
          objectToVisit = visitor.getTarget();
        } else {
          visitor.startVisitingObject(objectToVisit);
          ObjectTypePair currObjTypePair = objTypePair.toMoreSpecificType();
          Class<?> topLevelClass = new TypeInfo(currObjTypePair.type).getRawClass();
          for (Class<?> curr = topLevelClass; curr != null && !curr.equals(Object.class); curr =
              curr.getSuperclass()) {
            if (!curr.isSynthetic()) {
              navigateClassFields(objectToVisit, curr, visitor);
            }
          }
        }
      } finally {
        visitor.end(objTypePair);
      }
    }
  }

  private boolean isPrimitiveOrString(Object objectToVisit) {
    Class<?> realClazz = objectToVisit.getClass();
    return realClazz == Object.class || realClazz == String.class
        || Primitives.unwrap(realClazz).isPrimitive();
  }

  private void navigateClassFields(Object obj, Class<?> clazz, Visitor visitor) {
    Field[] fields = clazz.getDeclaredFields();
    AccessibleObject.setAccessible(fields, true);
    for (Field f : fields) {
      FieldAttributes fieldAttributes = new FieldAttributes(clazz, f);
      if (exclusionStrategy.shouldSkipField(fieldAttributes)
          || exclusionStrategy.shouldSkipClass(fieldAttributes.getDeclaredClass())) {
        continue; // skip
      }
      TypeInfo fieldTypeInfo = TypeInfoFactory.getTypeInfoForField(f, objTypePair.type);
      Type declaredTypeOfField = fieldTypeInfo.getActualType();
      boolean visitedWithCustomHandler =
        visitor.visitFieldUsingCustomHandler(fieldAttributes, declaredTypeOfField, obj);
      if (!visitedWithCustomHandler) {
        if (fieldTypeInfo.isArray()) {
          visitor.visitArrayField(fieldAttributes, declaredTypeOfField, obj);
        } else {
          visitor.visitObjectField(fieldAttributes, declaredTypeOfField, obj);
        }
      }
    }
  }
}
