/*
 * Copyright DataStax, 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.datastax.oss.driver.internal.mapper.processor.util.generation;

import com.datastax.oss.driver.api.core.data.GettableByName;
import com.datastax.oss.driver.api.core.data.SettableByName;
import com.datastax.oss.driver.api.core.data.UdtValue;
import com.datastax.oss.driver.api.mapper.annotations.Entity;
import com.datastax.oss.driver.internal.mapper.processor.ProcessorContext;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;

/**
 * Wraps the declared type of an entity property (or DAO method parameter) that will be injected
 * into a {@link SettableByName}, or extracted from a {@link GettableByName}.
 *
 * <p>The goal is to detect if the type contains other mapped entities, that must be translated into
 * UDT values.
 */
public abstract class PropertyType {

  private static final ClassName UDT_VALUE_CLASS_NAME = ClassName.get(UdtValue.class);
  public static final ClassName LIST_CLASS_NAME = ClassName.get(List.class);
  public static final ClassName SET_CLASS_NAME = ClassName.get(Set.class);
  public static final ClassName MAP_CLASS_NAME = ClassName.get(Map.class);

  public static PropertyType parse(TypeMirror typeMirror, ProcessorContext context) {
    if (typeMirror.getKind() == TypeKind.DECLARED) {
      DeclaredType declaredType = (DeclaredType) typeMirror;
      if (declaredType.asElement().getAnnotation(Entity.class) != null) {
        return new SingleEntity(declaredType);
      } else if (context.getClassUtils().isList(declaredType)) {
        PropertyType elementType = parse(declaredType.getTypeArguments().get(0), context);
        return (elementType instanceof Simple)
            ? new Simple(typeMirror)
            : new EntityList(typeMirror, elementType);
      } else if (context.getClassUtils().isSet(declaredType)) {
        PropertyType elementType = parse(declaredType.getTypeArguments().get(0), context);
        return (elementType instanceof Simple)
            ? new Simple(typeMirror)
            : new EntitySet(typeMirror, elementType);
      } else if (context.getClassUtils().isMap(declaredType)) {
        PropertyType keyType = parse(declaredType.getTypeArguments().get(0), context);
        PropertyType valueType = parse(declaredType.getTypeArguments().get(1), context);
        return (keyType instanceof Simple && valueType instanceof Simple)
            ? new Simple(typeMirror)
            : new EntityMap(typeMirror, keyType, valueType);
      }
    }
    return new Simple(typeMirror);
  }

  private final TypeMirror typeMirror;

  protected PropertyType(TypeMirror typeMirror) {
    this.typeMirror = typeMirror;
  }

  public TypeMirror asTypeMirror() {
    return typeMirror;
  }

  public abstract TypeName asTypeName();

  /**
   * Returns the name of the type we will convert to before saving to the database; that is,
   * replacing every entity class by {@code UdtValue}.
   */
  public abstract TypeName asRawTypeName();

  /**
   * A type that does not contain any mapped entity.
   *
   * <p>Note that it can still be a collection, for example {@code Map<String, List<Integer>>}.
   */
  public static class Simple extends PropertyType {
    public final TypeName typeName;

    public Simple(TypeMirror typeMirror) {
      super(typeMirror);
      this.typeName = ClassName.get(typeMirror);
    }

    @Override
    public TypeName asTypeName() {
      return typeName;
    }

    @Override
    public TypeName asRawTypeName() {
      return typeName;
    }
  }

  /** A mapped entity. */
  public static class SingleEntity extends PropertyType {
    public final ClassName entityName;

    public SingleEntity(DeclaredType declaredType) {
      super(declaredType);
      this.entityName = (ClassName) TypeName.get(declaredType);
    }

    @Override
    public TypeName asTypeName() {
      return entityName;
    }

    @Override
    public TypeName asRawTypeName() {
      return UDT_VALUE_CLASS_NAME;
    }
  }

  /** A list of another non-simple type. */
  public static class EntityList extends PropertyType {
    public final PropertyType elementType;

    public EntityList(TypeMirror typeMirror, PropertyType elementType) {
      super(typeMirror);
      this.elementType = elementType;
    }

    @Override
    public TypeName asTypeName() {
      return ParameterizedTypeName.get(LIST_CLASS_NAME, elementType.asTypeName());
    }

    @Override
    public TypeName asRawTypeName() {
      return ParameterizedTypeName.get(LIST_CLASS_NAME, elementType.asRawTypeName());
    }
  }

  /** A set of another non-simple type. */
  public static class EntitySet extends PropertyType {
    public final PropertyType elementType;

    public EntitySet(TypeMirror typeMirror, PropertyType elementType) {
      super(typeMirror);
      this.elementType = elementType;
    }

    @Override
    public TypeName asTypeName() {
      return ParameterizedTypeName.get(SET_CLASS_NAME, elementType.asTypeName());
    }

    @Override
    public TypeName asRawTypeName() {
      return ParameterizedTypeName.get(SET_CLASS_NAME, elementType.asRawTypeName());
    }
  }

  /** A map where either the key type, the value type, or both, are non-simple types. */
  public static class EntityMap extends PropertyType {
    public final PropertyType keyType;
    public final PropertyType valueType;

    public EntityMap(TypeMirror typeMirror, PropertyType keyType, PropertyType valueType) {
      super(typeMirror);
      this.keyType = keyType;
      this.valueType = valueType;
    }

    @Override
    public TypeName asTypeName() {
      return ParameterizedTypeName.get(
          MAP_CLASS_NAME, keyType.asTypeName(), valueType.asTypeName());
    }

    @Override
    public TypeName asRawTypeName() {
      return ParameterizedTypeName.get(
          MAP_CLASS_NAME, keyType.asRawTypeName(), valueType.asRawTypeName());
    }
  }
}
