/*
 * Copyright 2013-present Facebook, 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 com.facebook.buck.rules;

import com.facebook.buck.io.ProjectFilesystem;
import com.facebook.buck.rules.coercer.CoerceFailedException;
import com.facebook.buck.rules.coercer.TypeCoercer;
import com.facebook.buck.rules.coercer.TypeCoercerFactory;
import com.facebook.buck.util.Types;
import com.google.common.base.CaseFormat;
import com.google.common.base.Function;
import com.google.common.base.Optional;

import java.lang.reflect.Field;
import java.nio.file.Path;
import java.util.Map;

import javax.annotation.Nullable;

public class ParamInfo<T> implements Comparable<ParamInfo<T>> {

  private final TypeCoercer<?> typeCoercer;

  private final boolean isOptional;
  private final String name;
  private final String pythonName;
  private final boolean isDep;
  private final boolean isInput;
  private final Field field;

  public ParamInfo(TypeCoercerFactory typeCoercerFactory, Field field) {
    this.field = field;
    this.name = field.getName();
    Hint hint = field.getAnnotation(Hint.class);
    this.pythonName = determinePythonName(this.name, hint);
    this.isDep = hint != null ? hint.isDep() : Hint.DEFAULT_IS_DEP;
    this.isInput = hint != null ? hint.isInput() : Hint.DEFAULT_IS_INPUT;

    isOptional = Optional.class.isAssignableFrom(field.getType());
    this.typeCoercer = typeCoercerFactory.typeCoercerForType(Types.getFirstNonOptionalType(field));
  }

  public String getName() {
    return name;
  }

  public boolean isOptional() {
    return isOptional;
  }

  public String getPythonName() {
    return pythonName;
  }

  public boolean isDep() {
    return isDep;
  }

  public boolean isInput() {
    return isInput;
  }

  /**
   * Returns the type that input values will be coerced to.
   * Return the type parameter of Optional if wrapped in Optional.
   */
  public Class<?> getResultClass() {
    return typeCoercer.getOutputClass();
  }

  public void traverse(Traversal traversal, T dto) {
    traverseHelper(typeCoercer, traversal, dto);
  }

  @SuppressWarnings("unchecked")
  private <U> void traverseHelper(TypeCoercer<U> typeCoercer, Traversal traversal, T dto) {
    U object;
    try {
      if (isOptional) {
        Optional<U> optional = (Optional<U>) field.get(dto);
        object = optional.orNull();
      } else {
        object = (U) field.get(dto);
      }
    } catch (ReflectiveOperationException e) {
      throw new RuntimeException(e);
    }

    if (object != null) {
      typeCoercer.traverse(object, traversal);
    }
  }

  public boolean hasElementTypes(final Class<?>... types) {
    return typeCoercer.hasElementClass(types);
  }

  public void setFromParams(
      Function<Optional<String>, Path> cellRoots,
      ProjectFilesystem filesystem,
      BuildRuleFactoryParams params,
      Object arg,
      Map<String, ?> instance
      ) throws ParamInfoException {
    set(
        cellRoots,
        filesystem,
        params.target.getBasePath(),
        arg,
        instance.get(name));
  }

  /**
   * Sets a single property of the {@code dto}, coercing types as necessary.
   * @param filesystem {@link com.facebook.buck.io.ProjectFilesystem} used to ensure
   *        {@link java.nio.file.Path}s exist.
   * @param pathRelativeToProjectRoot The path relative to the project root that this DTO is for.
   * @param dto The constructor DTO on which the value should be set.
   * @param value The value, which may be coerced depending on the type on {@code dto}.
   */
  public void set(
      Function<Optional<String>, Path> cellRoots,
      ProjectFilesystem filesystem,
      Path pathRelativeToProjectRoot,
      Object dto,
      @Nullable Object value) throws ParamInfoException {
    Object result;

    if (value == null) {
      if (isOptional) {
        result = typeCoercer.getOptionalValue();
      } else if (Number.class.isAssignableFrom(typeCoercer.getOutputClass())) {
        result = 0;
      } else if (Boolean.class.isAssignableFrom(typeCoercer.getOutputClass())) {
        result = false;
      } else {
        throw new ParamInfoException(name, "field cannot be null");
      }
    } else {
      try {
        result = typeCoercer.coerce(
            cellRoots,
            filesystem,
            pathRelativeToProjectRoot,
            value);
      } catch (CoerceFailedException e) {
        throw new ParamInfoException(name, e.getMessage(), e);
      }
      if (isOptional) {
        result = Optional.of(result);
      }
    }

    try {
      field.set(dto, result);
    } catch (ReflectiveOperationException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Only valid when comparing {@link ParamInfo} instances from the same description.
   */
  @Override
  public int compareTo(ParamInfo<T> that) {
    if (this == that) {
      return 0;
    }

    return this.name.compareTo(that.name);
  }

  @Override
  public int hashCode() {
    return name.hashCode();
  }

  @Override
  public boolean equals(Object obj) {
    if (!(obj instanceof ParamInfo)) {
      return false;
    }

    ParamInfo<?> that = (ParamInfo<?>) obj;
    return name.equals(that.getName());
  }

  private String determinePythonName(String javaName, @Nullable Hint hint) {
    if (hint != null && !Hint.DEFAULT_NAME.equals(hint.name())) {
      return hint.name();
    }
    return CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, javaName);
  }

  public interface Traversal extends TypeCoercer.Traversal {}
}


@@ -1131,16 +1131,158 @@
 lable;%0A%0A
+/**%0A * Represents a single field that can be represented in buck build files.%0A *%0A * @param %3CT%3E the class that this ParamInfo operates on.%0A */%0A
 public c
@@ -2621,32 +2621,457 @@
 utClass();%0A  %7D%0A%0A
+  /**%0A   * Traverse the value of the field on %7B@code dto%7D that is represented by this instance.%0A   *%0A   * If this field has a top level Optional type, traversal begins at the Optional value, or not at%0A   * all if the field is empty.%0A   *%0A   * @param traversal traversal to apply on the values.%0A   * @param dto the object whose field will be traversed.%0A   *%0A   * @see TypeCoercer#traverse(Object, TypeCoercer.Traversal)%0A   */%0A
   public void tr

Add some docs to ParamInfo