/*
 * 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.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.NoType;
import javax.lang.model.type.PrimitiveType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.type.WildcardType;

/**
 * Uses information in a State object to convert client types to their domain
 * equivalents. This types assumes that any incoming type has already been
 * determined to be a transportable type.
 */
class ClientToDomainMapper extends TypeVisitorBase<TypeMirror> {
  public static class UnmappedTypeException extends RuntimeException {
    private final TypeMirror clientType;

    public UnmappedTypeException() {
      super();
      clientType = null;
    }

    public UnmappedTypeException(TypeMirror clientType) {
      super("No domain type resolved for " + clientType.toString());
      this.clientType = clientType;
    }

    public TypeMirror getClientType() {
      return clientType;
    }
  }

  @Override
  public TypeMirror visitDeclared(DeclaredType x, State state) {
    if (x.asElement().getKind().equals(ElementKind.ENUM)) {
      // Enums map to enums
      return x;
    }
    if (state.types.isAssignable(x, state.entityProxyType)
        || state.types.isAssignable(x, state.valueProxyType)) {
      // FooProxy -> FooDomain
      /*
       * TODO(bobv): This if statement should be widened to baseProxy to support
       * heterogenous collections of any proxy type. The BaseProxy interface
       * would also need to be annotated with an @ProxyFor mapping. This can be
       * done once RFIV is removed, since it only allows homogenous collections.
       */
      TypeElement domainType =
          (TypeElement) state.getClientToDomainMap().get(state.types.asElement(x));
      if (domainType == null) {
        return defaultAction(x, state);
      }
      return domainType.asType();
    }
    if (state.types.isAssignable(x, state.entityProxyIdType)) {
      // EntityProxyId<FooProxy> -> FooDomain
      return convertSingleParamType(x, state.entityProxyIdType, 0, state);
    }
    if (state.types.isAssignable(x, state.requestType)) {
      // Request<FooProxy> -> FooDomain
      return convertSingleParamType(x, state.requestType, 0, state);
    }
    if (state.types.isAssignable(x, state.instanceRequestType)) {
      // InstanceRequest<FooProxy, X> -> FooDomain
      return convertSingleParamType(x, state.instanceRequestType, 1, state);
    }
    for (DeclaredType valueType : getValueTypes(state)) {
      if (state.types.isAssignable(x, valueType)) {
        // Value types map straight through
        return x;
      }
    }
    if (state.types.isAssignable(x, state.findType(List.class))
        || state.types.isAssignable(x, state.findType(Set.class))) {
      // Convert Set,List<FooProxy> to Set,List<FooDomain>
      TypeMirror param = convertSingleParamType(x, state.findType(Collection.class), 0, state);
      return state.types.getDeclaredType((TypeElement) state.types.asElement(x), param);
    }
    final DeclaredType mapType = state.findType(Map.class);
    if (state.types.isAssignable(x, mapType)) {
      // Convert Map<FooProxy, BarProxy> to Map<FooDomain, BarDomain>
      TypeMirror keyParam = convertSingleParamType(x, mapType, 0, state);
      TypeMirror valueParam = convertSingleParamType(x, mapType, 1, state);
      return state.types.getDeclaredType((TypeElement) state.types.asElement(x), keyParam, valueParam);
    }
    return defaultAction(x, state);
  }

  @Override
  public TypeMirror visitNoType(NoType x, State state) {
    if (x.getKind().equals(TypeKind.VOID)) {
      // Pass void through
      return x;
    }
    // Here, x would be NONE or PACKAGE, neither of which make sense
    return defaultAction(x, state);
  }

  @Override
  public TypeMirror visitPrimitive(PrimitiveType x, State state) {
    // Primitives pass through
    return x;
  }

  @Override
  public TypeMirror visitTypeVariable(TypeVariable x, State state) {
    // Convert <T extends FooProxy> to FooDomain
    return x.getUpperBound().accept(this, state);
  }

  @Override
  public TypeMirror visitWildcard(WildcardType x, State state) {
    // Convert <? extends FooProxy> to FooDomain
    return state.types.erasure(x).accept(this, state);
  }

  /**
   * Utility method to convert a {@code Foo<BarProxy> -> BarDomain}. The
   * {@code param} parameter specifies the index of the type parameter to
   * extract.
   */
  protected TypeMirror convertSingleParamType(DeclaredType x, DeclaredType convertTo, int param,
      State state) {
    DeclaredType converted = (DeclaredType) State.viewAs(convertTo, x, state);
    if (converted == null) {
      return state.types.getNoType(TypeKind.NONE);
    }
    if (converted.getTypeArguments().isEmpty()) {
      return defaultAction(x, state);
    }
    return converted.getTypeArguments().get(param).accept(this, state);
  }

  @Override
  protected TypeMirror defaultAction(TypeMirror x, State state) {
    throw new UnmappedTypeException(x);
  }
}
