/*
 * Copyright 2011 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 com.google.web.bindery.requestfactory.apt;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.annotation.Annotation;
import java.util.List;

import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.ElementScanner6;

/**
 * Contains utility methods for traversing RequestFactory declarations.
 */
class ScannerBase<R> extends ElementScanner6<R, State> {

  /**
   * Poisons the given type if one or more of the annotation values are
   * non-null.
   */
  protected static void poisonIfAnnotationPresent(State state, TypeElement x,
      Annotation... annotations) {
    for (Annotation a : annotations) {
      if (a != null) {
        state.poison(x, Messages.redundantAnnotation(a.annotationType().getSimpleName()));
      }
    }
  }

  protected static ExecutableType viewIn(TypeElement lookIn, ExecutableElement methodElement,
      State state) {
    // Do not use state.types.getDeclaredType, as we really want a
    // "prototypical" type, and not a raw type.
    // This is important when a proxy maps to a generic domain type:
    // state.types.getDeclaredType without typeArgs would return the raw type,
    // and asMemberOf would then return an ExecutableType using raw types too.
    // For instance, if a class Foo<T> contains a method whose return type is
    // List<String> (note it doesn't even make use of the T type parameter),
    // then if we were to use raw types, the returned type of the ExecutableType
    // would be the raw type java.util.List, and not List<String>. Using
    // asType(), we'd get the expected List<String> though; and for a List<T>,
    // we'd get a List<Object> (or whichever upper bound for the T type
    // parameter).
    try {
      return (ExecutableType) state.types.asMemberOf((DeclaredType) lookIn.asType(), methodElement);
    } catch (IllegalArgumentException e) {
      return (ExecutableType) methodElement.asType();
    }
  }

  @Override
  public final R scan(Element x, State state) {
    try {
      return super.scan(x, state);
    } catch (HaltException e) {
      throw e;
    } catch (Throwable e) {
      StringWriter sw = new StringWriter();
      e.printStackTrace(new PrintWriter(sw));
      state.poison(x, sw.toString());
      throw new HaltException();
    }
  }

  /**
   * No parameters, name stars with "get" or is a boolean / Boolean isFoo hasFoo
   * method.
   */
  protected boolean isGetter(ExecutableElement x, State state) {
    String name = x.getSimpleName().toString();
    TypeMirror returnType = x.getReturnType();
    if (!x.getParameters().isEmpty()) {
      return false;
    }
    if (name.startsWith("get")) {
      return true;
    }
    if (name.startsWith("is") || name.startsWith("has")) {
      TypeMirror javaLangBoolean =
          state.types.boxedClass(state.types.getPrimitiveType(TypeKind.BOOLEAN)).asType();
      if (returnType.getKind().equals(TypeKind.BOOLEAN)
          || state.types.isSameType(returnType, javaLangBoolean)) {
        return true;
      }
    }
    return false;
  }

  /**
   * Name starts with set, has one parameter, returns either null or something
   * assignable from the element's enclosing type.
   */
  protected boolean isSetter(ExecutableElement x, State state) {
    String name = x.getSimpleName().toString();
    TypeMirror returnType = x.getReturnType();

    if (x.getParameters().size() != 1) {
      return false;
    }
    if (!name.startsWith("set")) {
      return false;
    }
    if (returnType.getKind().equals(TypeKind.VOID)) {
      return true;
    }
    if (x.getEnclosingElement() != null
        && state.types.isAssignable(x.getEnclosingElement().asType(), returnType)) {
      return true;
    }
    return false;
  }

  protected R scanAllInheritedMethods(TypeElement x, State state) {
    R toReturn = DEFAULT_VALUE;
    List<ExecutableElement> methods = ElementFilter.methodsIn(state.elements.getAllMembers(x));
    for (ExecutableElement method : methods) {
      toReturn = scan(method, state);
    }
    return toReturn;
  }

  /**
   * Ignore all static initializers and methods defined in the base
   * RequestFactory interfaces
   */
  protected boolean shouldIgnore(ExecutableElement x, State state) {
    TypeMirror enclosingType = x.getEnclosingElement().asType();
    return x.getKind().equals(ElementKind.STATIC_INIT)
        || state.types.isSameType(state.objectType, enclosingType)
        || state.types.isSameType(state.requestFactoryType, enclosingType)
        || state.types.isSameType(state.requestContextType, enclosingType)
        || state.types.isSameType(state.entityProxyType, enclosingType)
        || state.types.isSameType(state.valueProxyType, enclosingType);
  }
}