/*
 * 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.driver.mapping;

import com.datastax.driver.mapping.annotations.ClusteringColumn;
import com.datastax.driver.mapping.annotations.Column;
import com.datastax.driver.mapping.annotations.Computed;
import com.datastax.driver.mapping.annotations.PartitionKey;
import com.datastax.driver.mapping.annotations.Table;
import java.lang.annotation.Annotation;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

/** Various checks on mapping annotations. */
class AnnotationChecks {

  // The package containing the mapping annotations
  private static final Package MAPPING_PACKAGE = Table.class.getPackage();

  /**
   * Checks that a class is decorated with the given annotation, and return the annotation instance.
   * Also validates that no other mapping annotation is present.
   */
  static <T extends Annotation> T getTypeAnnotation(Class<T> annotation, Class<?> annotatedClass) {
    T instance = annotatedClass.getAnnotation(annotation);
    if (instance == null)
      throw new IllegalArgumentException(
          String.format(
              "@%s annotation was not found on %s", annotation.getSimpleName(), annotatedClass));

    // Check that no other mapping annotations are present
    validateAnnotations(annotatedClass, annotation);

    return instance;
  }

  private static void validateAnnotations(Class<?> clazz, Class<? extends Annotation> allowed) {
    Collection<Annotation> classAnnotations = new HashSet<Annotation>();
    Collections.addAll(classAnnotations, clazz.getAnnotations());
    Class<? extends Annotation> invalid =
        validateAnnotations(classAnnotations, Collections.singleton(allowed));
    if (invalid != null)
      throw new IllegalArgumentException(
          String.format(
              "Cannot have both @%s and @%s on %s",
              allowed.getSimpleName(), invalid.getSimpleName(), clazz));
  }

  /**
   * Checks that a field is only annotated with the given mapping annotations, and that its "frozen"
   * annotations are valid.
   */
  static void validateAnnotations(
      String propertyName,
      Map<Class<? extends Annotation>, Annotation> annotations,
      Collection<? extends Class<? extends Annotation>> allowed) {
    Class<? extends Annotation> invalid = validateAnnotations(annotations.values(), allowed);
    if (invalid != null) {
      throw new IllegalArgumentException(
          String.format(
              "Annotation @%s is not allowed on property '%s'",
              invalid.getSimpleName(), propertyName));
    }
    checkValidPrimaryKey(propertyName, annotations);
    checkValidComputed(propertyName, annotations);
  }

  // Returns the offending annotation if there is one
  private static Class<? extends Annotation> validateAnnotations(
      Collection<Annotation> annotations,
      Collection<? extends Class<? extends Annotation>> allowed) {
    for (Annotation annotation : annotations) {
      Class<? extends Annotation> actual = annotation.annotationType();
      if (actual.getPackage().equals(MAPPING_PACKAGE) && !allowed.contains(actual)) return actual;
    }
    return null;
  }

  private static void checkValidPrimaryKey(
      String propertyName, Map<Class<? extends Annotation>, Annotation> annotations) {
    if (annotations.containsKey(PartitionKey.class)
        && annotations.containsKey(ClusteringColumn.class))
      throw new IllegalArgumentException(
          String.format(
              "Property '%s' cannot be annotated with both @PartitionKey and @ClusteringColumn",
              propertyName));
  }

  private static void checkValidComputed(
      String propertyName, Map<Class<? extends Annotation>, Annotation> annotations) {
    if (annotations.containsKey(Computed.class)) {
      Computed computed = (Computed) annotations.get(Computed.class);
      if (computed.value().isEmpty()) {
        throw new IllegalArgumentException(
            String.format(
                "Property '%s': attribute 'value' of annotation @Computed is mandatory for computed properties",
                propertyName));
      }
      if (annotations.containsKey(Column.class)) {
        throw new IllegalArgumentException(
            String.format(
                "Property '%s' cannot be annotated with both @Column and @Computed", propertyName));
      }
    }
  }

  static void validateOrder(List<AliasedMappedProperty> properties, String annotation) {
    for (int i = 0; i < properties.size(); i++) {
      AliasedMappedProperty property = properties.get(i);
      int pos = property.mappedProperty.getPosition();
      if (pos != i)
        throw new IllegalArgumentException(
            String.format(
                "Invalid ordering value %d for annotation %s of property '%s', was expecting %d",
                pos, annotation, property.mappedProperty.getPropertyName(), i));
    }
  }
}
