/*
 * 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 com.google.gwt.dev.util.Name.BinaryName;
import com.google.web.bindery.requestfactory.shared.JsonRpcService;
import com.google.web.bindery.requestfactory.shared.Service;
import com.google.web.bindery.requestfactory.shared.ServiceName;

import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.MirroredTypeException;
import javax.lang.model.type.TypeMirror;

/**
 * Scans a RequestContext declaration. This visitor will call out to the State
 * object to validate the types that it encounters.
 */
class RequestContextScanner extends ScannerBase<Void> {

  private TypeElement checkedElement;

  @Override
  public Void visitExecutable(ExecutableElement x, State state) {
    if (shouldIgnore(x, state)) {
      return null;
    }
    // resolve type parameters, if any
    ExecutableType xType = viewIn(checkedElement, x, state);
    TypeMirror returnType = xType.getReturnType();
    if (state.types.isAssignable(returnType, state.requestType)) {
      // Extract Request<Foo> type
      DeclaredType asRequest = (DeclaredType) State.viewAs(state.requestType, returnType, state);
      if (asRequest.getTypeArguments().isEmpty()) {
        state.poison(x, Messages.rawType());
      } else {
        TypeMirror requestReturn = asRequest.getTypeArguments().get(0);
        if (!state.isTransportableType(requestReturn)) {
          state.poison(x, Messages.untransportableType(requestReturn));
        }
      }
    } else if (state.types.isAssignable(returnType, state.instanceRequestType)) {
      // Extract InstanceRequest<FooProxy, String>
      DeclaredType asInstanceRequest =
          (DeclaredType) State.viewAs(state.instanceRequestType, returnType, state);
      if (asInstanceRequest.getTypeArguments().isEmpty()) {
        state.poison(x, Messages.rawType());
      } else {
        TypeMirror instanceType = asInstanceRequest.getTypeArguments().get(0);
        state.maybeScanProxy((TypeElement) state.types.asElement(instanceType));
        TypeMirror requestReturn = asInstanceRequest.getTypeArguments().get(1);
        if (!state.isTransportableType(requestReturn)) {
          state.poison(x, Messages.untransportableType(requestReturn));
        }
      }
    } else if (!isSetter(x, state)) {
      state.poison(x, Messages.contextRequiredReturnTypes(state.requestType.asElement()
          .getSimpleName(), state.instanceRequestType.asElement().getSimpleName()));
    }

    // check parameters (we do not defer to visitVariable, as we need the
    // resolved generics)
    int i = 0;
    for (TypeMirror parameterType : xType.getParameterTypes()) {
      if (!state.isTransportableType(parameterType)) {
        // see comments in ProxyScanner#visitExecutable
        state.poison(x.getParameters().get(i), Messages.untransportableType(parameterType));
      }
      i++;
    }
    return null;
  }

  @Override
  public Void visitType(TypeElement x, State state) {
    checkedElement = x;
    Service service = x.getAnnotation(Service.class);
    ServiceName serviceName = x.getAnnotation(ServiceName.class);
    JsonRpcService jsonRpcService = x.getAnnotation(JsonRpcService.class);
    if (service != null) {
      poisonIfAnnotationPresent(state, x, serviceName, jsonRpcService);

      // See javadoc on Element.getAnnotation() for why it works this way
      try {
        service.value();
        throw new RuntimeException("Should not reach here");
      } catch (MirroredTypeException expected) {
        TypeMirror type = expected.getTypeMirror();
        state.addMapping(x, (TypeElement) state.types.asElement(type));
      }
    }
    if (serviceName != null) {
      poisonIfAnnotationPresent(state, x, jsonRpcService);
      TypeElement domain =
          state.elements.getTypeElement(BinaryName.toSourceName(serviceName.value()));
      if (domain == null) {
        state.warn(x, Messages.contextMissingDomainType(serviceName.value()));
      }
      state.addMapping(x, domain);
    }

    scanAllInheritedMethods(x, state);
    state.checkExtraTypes(x);
    return null;
  }

  @Override
  public Void visitTypeParameter(TypeParameterElement x, State state) {
    for (TypeMirror bound : x.getBounds()) {
      if (!state.isTransportableType(bound)) {
        state.poison(x, Messages.untransportableType(bound));
      }
    }
    return super.visitTypeParameter(x, state);
  }
}