/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.dao;

import static com.datastax.oss.driver.internal.mapper.processor.dao.DefaultDaoReturnTypeKind.BOOLEAN;
import static com.datastax.oss.driver.internal.mapper.processor.dao.DefaultDaoReturnTypeKind.BOUND_STATEMENT;
import static com.datastax.oss.driver.internal.mapper.processor.dao.DefaultDaoReturnTypeKind.CUSTOM;
import static com.datastax.oss.driver.internal.mapper.processor.dao.DefaultDaoReturnTypeKind.FUTURE_OF_ASYNC_RESULT_SET;
import static com.datastax.oss.driver.internal.mapper.processor.dao.DefaultDaoReturnTypeKind.FUTURE_OF_BOOLEAN;
import static com.datastax.oss.driver.internal.mapper.processor.dao.DefaultDaoReturnTypeKind.FUTURE_OF_VOID;
import static com.datastax.oss.driver.internal.mapper.processor.dao.DefaultDaoReturnTypeKind.REACTIVE_RESULT_SET;
import static com.datastax.oss.driver.internal.mapper.processor.dao.DefaultDaoReturnTypeKind.RESULT_SET;
import static com.datastax.oss.driver.internal.mapper.processor.dao.DefaultDaoReturnTypeKind.VOID;

import com.datastax.oss.driver.api.core.cql.BoundStatement;
import com.datastax.oss.driver.api.core.cql.BoundStatementBuilder;
import com.datastax.oss.driver.api.core.cql.SimpleStatement;
import com.datastax.oss.driver.api.mapper.annotations.Delete;
import com.datastax.oss.driver.internal.mapper.processor.ProcessorContext;
import com.datastax.oss.driver.internal.mapper.processor.entity.EntityDefinition;
import com.datastax.oss.driver.internal.mapper.processor.entity.PropertyDefinition;
import com.datastax.oss.driver.internal.mapper.processor.util.generation.GeneratedCodePatterns;
import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableSet;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeName;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;

public class DaoDeleteMethodGenerator extends DaoMethodGenerator {

  public DaoDeleteMethodGenerator(
      ExecutableElement methodElement,
      Map<Name, TypeElement> typeParameters,
      TypeElement processedType,
      DaoImplementationSharedCode enclosingClass,
      ProcessorContext context) {
    super(methodElement, typeParameters, processedType, enclosingClass, context);
  }

  protected Set<DaoReturnTypeKind> getSupportedReturnTypes() {
    return ImmutableSet.of(
        VOID,
        FUTURE_OF_VOID,
        BOOLEAN,
        FUTURE_OF_BOOLEAN,
        RESULT_SET,
        BOUND_STATEMENT,
        FUTURE_OF_ASYNC_RESULT_SET,
        REACTIVE_RESULT_SET,
        CUSTOM);
  }

  @Override
  public boolean requiresReactive() {
    // Validate the return type:
    DaoReturnType returnType =
        parseAndValidateReturnType(getSupportedReturnTypes(), Delete.class.getSimpleName());
    if (returnType == null) {
      return false;
    }
    return returnType.requiresReactive();
  }

  @Override
  public Optional<MethodSpec> generate() {

    Delete annotation = methodElement.getAnnotation(Delete.class);
    assert annotation != null;
    if (annotation.ifExists() && !annotation.customIfClause().isEmpty()) {
      context
          .getMessager()
          .error(
              methodElement,
              "Invalid annotation parameters: %s cannot have both ifExists and customIfClause",
              Delete.class.getSimpleName());
      return Optional.empty();
    }

    // Validate the arguments: either an entity instance, or the PK components (in the latter case,
    // the entity class has to be provided via the annotation).
    // In either case, a Function<BoundStatementBuilder, BoundStatementBuilder> can be added in last
    // position.
    List<? extends VariableElement> parameters = methodElement.getParameters();
    VariableElement boundStatementFunction = findBoundStatementFunction(methodElement);
    if (boundStatementFunction != null) {
      parameters = parameters.subList(0, parameters.size() - 1);
    }

    TypeElement entityElement;
    EntityDefinition entityDefinition;
    boolean hasEntityParameter;
    if (parameters.isEmpty()) {
      context
          .getMessager()
          .error(
              methodElement,
              "Wrong number of parameters: %s methods with no custom clause "
                  + "must take either an entity instance, or the primary key components",
              Delete.class.getSimpleName());
      return Optional.empty();
    }
    String customWhereClause = annotation.customWhereClause();
    String customIfClause = annotation.customIfClause();
    VariableElement firstParameter = parameters.get(0);
    entityElement = EntityUtils.asEntityElement(firstParameter, typeParameters);
    hasEntityParameter = (entityElement != null);

    // the number of primary key parameters provided, if -1 this implies a custom
    // where clause where number of parameters that are primary key are irrelevant.
    final int primaryKeyParameterCount;
    if (hasEntityParameter) {
      if (!customWhereClause.isEmpty()) {
        context
            .getMessager()
            .error(
                methodElement,
                "Invalid parameter list: %s methods that have a custom where clause "
                    + "must not take an Entity (%s) as a parameter",
                Delete.class.getSimpleName(),
                entityElement.getSimpleName());
      }
      entityDefinition = context.getEntityFactory().getDefinition(entityElement);
      primaryKeyParameterCount = entityDefinition.getPrimaryKey().size();
    } else {
      entityElement = getEntityClassFromAnnotation(Delete.class);
      if (entityElement == null) {
        context
            .getMessager()
            .error(
                methodElement,
                "Missing entity class: %s methods that do not operate on an entity "
                    + "instance must have an 'entityClass' argument",
                Delete.class.getSimpleName());
        return Optional.empty();
      } else {
        entityDefinition = context.getEntityFactory().getDefinition(entityElement);
      }

      if (customWhereClause.isEmpty()) {
        /* if a custom if clause is provided, the whole primary key must also be provided.
         * we only do this check if there is no custom where clause as the order of
         * keys may differ in that case.*/
        List<? extends VariableElement> primaryKeyParameters = parameters;
        if (!customIfClause.isEmpty()) {
          if (primaryKeyParameters.size() < entityDefinition.getPrimaryKey().size()) {
            List<TypeName> primaryKeyTypes =
                entityDefinition.getPrimaryKey().stream()
                    .map(d -> d.getType().asTypeName())
                    .collect(Collectors.toList());
            context
                .getMessager()
                .error(
                    methodElement,
                    "Invalid parameter list: %s methods that have a custom if clause"
                        + "must specify the entire primary key (expected primary keys of %s: %s)",
                    Delete.class.getSimpleName(),
                    entityElement.getSimpleName(),
                    primaryKeyTypes);
            return Optional.empty();
          } else {
            // restrict parameters to primary key length.
            primaryKeyParameters =
                primaryKeyParameters.subList(0, entityDefinition.getPrimaryKey().size());
          }
        }

        primaryKeyParameterCount = primaryKeyParameters.size();
        if (!EntityUtils.areParametersValid(
            entityElement,
            entityDefinition,
            primaryKeyParameters,
            Delete.class,
            context,
            methodElement,
            processedType,
            "do not operate on an entity instance and lack a custom where clause")) {
          return Optional.empty();
        }
      } else {
        primaryKeyParameterCount = -1;
      }
    }

    // Validate the return type:
    DaoReturnType returnType =
        parseAndValidateReturnType(getSupportedReturnTypes(), Delete.class.getSimpleName());
    if (returnType == null) {
      return Optional.empty();
    }

    // Generate the method
    String helperFieldName = enclosingClass.addEntityHelperField(ClassName.get(entityElement));
    String statementName =
        enclosingClass.addPreparedStatement(
            methodElement,
            (methodBuilder, requestName) ->
                generatePrepareRequest(
                    methodBuilder, requestName, helperFieldName, primaryKeyParameterCount));

    CodeBlock.Builder createStatementBlock = CodeBlock.builder();

    createStatementBlock.addStatement(
        "$T boundStatementBuilder = $L.boundStatementBuilder()",
        BoundStatementBuilder.class,
        statementName);
    populateBuilderWithStatementAttributes(createStatementBlock, methodElement);
    populateBuilderWithFunction(createStatementBlock, boundStatementFunction);

    int nextParameterIndex = 0;
    if (hasEntityParameter) {
      warnIfCqlNamePresent(Collections.singletonList(firstParameter));
      // Bind entity's PK properties
      for (PropertyDefinition property : entityDefinition.getPrimaryKey()) {
        GeneratedCodePatterns.setValue(
            property.getCqlName(),
            property.getType(),
            CodeBlock.of("$L.$L()", firstParameter.getSimpleName(), property.getGetterName()),
            "boundStatementBuilder",
            createStatementBlock,
            enclosingClass);
      }
      nextParameterIndex = 1;
    } else if (customWhereClause.isEmpty()) {
      // The PK components are passed as arguments to the method (we've already checked that the
      // types match).
      List<CodeBlock> primaryKeyNames =
          entityDefinition.getPrimaryKey().stream()
              .map(PropertyDefinition::getCqlName)
              .collect(Collectors.toList())
              .subList(0, primaryKeyParameterCount);
      List<? extends VariableElement> bindMarkers = parameters.subList(0, primaryKeyParameterCount);
      warnIfCqlNamePresent(bindMarkers);
      GeneratedCodePatterns.bindParameters(
          bindMarkers, primaryKeyNames, createStatementBlock, enclosingClass, context, false);
      nextParameterIndex = primaryKeyNames.size();
    }

    // Bind any remaining parameters, assuming they are values for a custom WHERE or IF clause
    if (nextParameterIndex < parameters.size()) {
      if (customIfClause.isEmpty() && customWhereClause.isEmpty()) {
        context
            .getMessager()
            .error(
                methodElement,
                "Wrong number of parameters: %s methods can only have additional "
                    + "parameters if they specify a custom WHERE or IF clause",
                Delete.class.getSimpleName());
      }
      List<? extends VariableElement> bindMarkers =
          parameters.subList(nextParameterIndex, parameters.size());
      if (validateCqlNamesPresent(bindMarkers)) {
        GeneratedCodePatterns.bindParameters(
            bindMarkers, createStatementBlock, enclosingClass, context, false);
      } else {
        return Optional.empty();
      }
    }

    createStatementBlock.addStatement(
        "$T boundStatement = boundStatementBuilder.build()", BoundStatement.class);

    return crudMethod(createStatementBlock, returnType, helperFieldName);
  }

  private void generatePrepareRequest(
      MethodSpec.Builder methodBuilder,
      String requestName,
      String helperFieldName,
      int parameterSize) {

    Delete delete = methodElement.getAnnotation(Delete.class);
    boolean ifExists = delete.ifExists();
    String customWhereClause = delete.customWhereClause();
    String customIfClause = delete.customIfClause();

    methodBuilder.addCode("$[$T $L = $L", SimpleStatement.class, requestName, helperFieldName);

    if (!customWhereClause.isEmpty()) {
      methodBuilder.addCode(".deleteStart().whereRaw($S)", customWhereClause);
    } else {
      methodBuilder.addCode(".deleteByPrimaryKeyParts($L)", parameterSize);
    }

    if (ifExists) {
      methodBuilder.addCode(".ifExists()");
    } else if (!customIfClause.isEmpty()) {
      methodBuilder.addCode(".ifRaw($S)", customIfClause);
    }

    methodBuilder.addCode(".build();$]\n");
  }
}
