/**
 * Copyright © 2006-2016 Web Cohesion (info@webcohesion.com)
 *
 * 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.webcohesion.enunciate.modules.jackson.model.util;

import com.webcohesion.enunciate.javac.decorations.DecoratedProcessingEnvironment;
import com.webcohesion.enunciate.javac.decorations.type.DecoratedDeclaredType;
import com.webcohesion.enunciate.javac.decorations.type.TypeMirrorUtils;
import com.webcohesion.enunciate.modules.jackson.EnunciateJacksonContext;
import com.webcohesion.enunciate.modules.jackson.model.adapters.AdapterType;

import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVisitor;
import javax.lang.model.util.Types;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * A decorated map type.
 *
 * @author Ryan Heaton
 */
public class MapType extends DecoratedDeclaredType {

  static final String PROPERTY_MAP_TYPES = "com.webcohesion.enunciate.modules.jackson.model.util.MapType#PROPERTY_MAP_TYPES";

  private TypeMirror keyType;
  private TypeMirror valueType;
  private DeclaredType originalType;

  private MapType(DeclaredType mapType, DecoratedProcessingEnvironment env) {
    super(mapType, env);
    this.originalType = mapType;
  }

  /**
   * Finds the map type for the specified type mirror, if it exists.
   *
   * @param typeMirror The type mirror.
   * @param context The context
   * @return The map type or null.
   */
  public static MapType findMapType(TypeMirror typeMirror, EnunciateJacksonContext context) {
    if (!(typeMirror instanceof DeclaredType)) {
      return null;
    }

    DeclaredType declaredType = (DeclaredType) typeMirror;
    TypeElement element = (TypeElement) declaredType.asElement();
    if (element == null) {
      return null;
    }
    else {
      String typeSignature = declaredType.toString();
      @SuppressWarnings ( "unchecked" )
      Map<String, MapType> mapTypes = (Map<String, MapType>) context.getContext().getProperty(PROPERTY_MAP_TYPES);
      if (mapTypes == null) {
        mapTypes = new HashMap<String, MapType>();
        context.getContext().setProperty(PROPERTY_MAP_TYPES, mapTypes);
      }

      MapType mapType = mapTypes.get(typeSignature);
      if (mapType != null) {
        return mapType;
      }
      else {
        DeclaredType declaredMapType = findMapTypeDeclaration(declaredType, context);
        if (declaredMapType == null) {
          return null;
        }

        MapType newMapType = new MapType(declaredType, context.getContext().getProcessingEnvironment());
        mapTypes.put(typeSignature, newMapType);

        TypeMirror keyType = null;
        TypeMirror valueType = null;

        List<? extends TypeMirror> typeArgs = declaredMapType.getTypeArguments();
        if ((typeArgs != null) && (typeArgs.size() == 2)) {
          Iterator<? extends TypeMirror> argIt = typeArgs.iterator();
          keyType = argIt.next();
          valueType = argIt.next();
        }

        if ((keyType == null) || (valueType == null)) {
          TypeMirror objectType = TypeMirrorUtils.objectType(context.getContext().getProcessingEnvironment());
          keyType = objectType;
          valueType = objectType;
        }

        TypeMirror mapKeyType = findMapType(keyType, context);
        newMapType.keyType = mapKeyType == null ? keyType : mapKeyType;

        TypeMirror mapValueType = findMapType(valueType, context);
        newMapType.valueType = mapValueType == null ? valueType : mapValueType;

        return newMapType;
      }
    }
  }

  public static DeclaredType findMapTypeDeclaration(TypeMirror typeMirror, EnunciateJacksonContext context) {
    if (!(typeMirror instanceof DeclaredType)) {
      return null;
    }

    DeclaredType declaredType = (DeclaredType) typeMirror;
    TypeElement element = (TypeElement) declaredType.asElement();
    String fqn = element.getQualifiedName().toString();
    if (Map.class.getName().equals(fqn)) {
      return declaredType;
    }

    AdapterType adapterType = JacksonUtil.findAdapterType(element, context);
    if (adapterType != null) {
      return findMapTypeDeclaration(adapterType.getAdaptingType(), context);
    }

    DeclaredType mapType = null;
    Types typeUtils = context.getContext().getProcessingEnvironment().getTypeUtils();
    List<? extends TypeMirror> supers = typeUtils.directSupertypes(declaredType);
    for (TypeMirror superInterface : supers) {
      mapType = findMapTypeDeclaration(superInterface, context);
      if (mapType != null) {
        break;
      }
    }

    return mapType;
  }

  /**
   * The key type associated with this map type.
   *
   * @return The key type associated with this map type.
   */
  public TypeMirror getKeyType() {
    return keyType;
  }

  /**
   * The key type associated with this map type.
   *
   * @return The key type associated with this map type.
   */
  public TypeMirror getValueType() {
    return valueType;
  }

  /**
   * The original map type.
   *
   * @return The original map type.
   */
  public DeclaredType getOriginalType() {
    return originalType;
  }

  public boolean isMap() {
    return true;
  }

  @Override
  public TypeKind getKind() {
    return this.originalType.getKind();
  }

  @Override
  public <R, P> R accept(TypeVisitor<R, P> v, P p) {
    return this.originalType.accept(v, p);
  }
}
