/*
 * 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 static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

import com.datastax.driver.core.TypeCodec;
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.Defaults;
import com.datastax.driver.mapping.annotations.PartitionKey;
import com.google.common.reflect.TypeToken;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;

/** Default implementation of {@link MappedProperty}. */
class DefaultMappedProperty<T> implements MappedProperty<T> {

  static <T> DefaultMappedProperty<T> create(
      Class<?> mappedClass,
      String propertyName,
      String mappedName,
      Field field,
      Method getter,
      Method setter,
      Map<Class<? extends Annotation>, Annotation> annotations) {
    @SuppressWarnings("unchecked")
    TypeToken<T> propertyType = (TypeToken<T>) inferType(field, getter);
    boolean partitionKey = annotations.containsKey(PartitionKey.class);
    boolean clusteringColumn = annotations.containsKey(ClusteringColumn.class);
    boolean computed = annotations.containsKey(Computed.class);
    int position = inferPosition(annotations);
    @SuppressWarnings("unchecked")
    Class<? extends TypeCodec<T>> codecClass =
        (Class<? extends TypeCodec<T>>) getCustomCodecClass(annotations);
    return new DefaultMappedProperty<T>(
        mappedClass,
        propertyName,
        mappedName,
        propertyType,
        partitionKey,
        clusteringColumn,
        computed,
        position,
        codecClass,
        field,
        getter,
        setter);
  }

  private final Class<?> mappedClass;
  private final String propertyName;
  private final TypeToken<T> propertyType;
  private final String mappedName;
  private final boolean partitionKey;
  private final boolean clusteringColumn;
  private final boolean computed;
  private final int position;
  private final TypeCodec<T> customCodec;
  private final Field field;
  private final Method getter;
  private final Method setter;

  private DefaultMappedProperty(
      Class<?> mappedClass,
      String propertyName,
      String mappedName,
      TypeToken<T> propertyType,
      boolean partitionKey,
      boolean clusteringColumn,
      boolean computed,
      int position,
      Class<? extends TypeCodec<T>> codecClass,
      Field field,
      Method getter,
      Method setter) {
    checkArgument(propertyName != null && !propertyName.isEmpty());
    checkArgument(mappedName != null && !mappedName.isEmpty());
    checkNotNull(propertyType);
    this.mappedClass = mappedClass;
    this.propertyName = propertyName;
    this.mappedName = mappedName;
    this.propertyType = propertyType;
    this.partitionKey = partitionKey;
    this.clusteringColumn = clusteringColumn;
    this.computed = computed;
    this.position = position;
    this.customCodec =
        codecClass == null || codecClass.equals(Defaults.NoCodec.class)
            ? null
            : ReflectionUtils.newInstance(codecClass);
    this.field = field;
    this.getter = getter;
    this.setter = setter;
  }

  @Override
  public String getPropertyName() {
    return propertyName;
  }

  @Override
  public TypeToken<T> getPropertyType() {
    return propertyType;
  }

  @Override
  public String getMappedName() {
    return mappedName;
  }

  @Override
  public int getPosition() {
    return position;
  }

  @Override
  public TypeCodec<T> getCustomCodec() {
    return customCodec;
  }

  @Override
  public boolean isComputed() {
    return computed;
  }

  @Override
  public boolean isPartitionKey() {
    return partitionKey;
  }

  @Override
  public boolean isClusteringColumn() {
    return clusteringColumn;
  }

  @SuppressWarnings("unchecked")
  @Override
  public T getValue(Object entity) {
    try {
      // try getter first, if available, otherwise direct field access
      if (getter != null && getter.isAccessible()) return (T) getter.invoke(entity);
      else return (T) checkNotNull(field).get(entity);
    } catch (Exception e) {
      throw new IllegalArgumentException(
          "Unable to read property '" + getPropertyName() + "' in " + entity.getClass(), e);
    }
  }

  @Override
  public void setValue(Object entity, T value) {
    try {
      // try setter first, if available, otherwise direct field access
      if (setter != null && setter.isAccessible()) setter.invoke(entity, value);
      else checkNotNull(field).set(entity, value);
    } catch (Exception e) {
      throw new IllegalArgumentException(
          "Unable to write property '" + getPropertyName() + "' in " + entity.getClass(), e);
    }
  }

  @Override
  public String toString() {
    return mappedClass.getSimpleName() + "." + getPropertyName();
  }

  private static TypeToken<?> inferType(Field field, Method getter) {
    if (getter != null) return TypeToken.of(getter.getGenericReturnType());
    else return TypeToken.of(checkNotNull(field).getGenericType());
  }

  private static int inferPosition(Map<Class<? extends Annotation>, Annotation> annotations) {
    if (annotations.containsKey(PartitionKey.class)) {
      return ((PartitionKey) annotations.get(PartitionKey.class)).value();
    }
    if (annotations.containsKey(ClusteringColumn.class)) {
      return ((ClusteringColumn) annotations.get(ClusteringColumn.class)).value();
    }
    return -1;
  }

  private static Class<? extends TypeCodec<?>> getCustomCodecClass(
      Map<Class<? extends Annotation>, Annotation> annotations) {
    Column column = (Column) annotations.get(Column.class);
    if (column != null) return column.codec();
    com.datastax.driver.mapping.annotations.Field udtField =
        (com.datastax.driver.mapping.annotations.Field)
            annotations.get(com.datastax.driver.mapping.annotations.Field.class);
    if (udtField != null) return udtField.codec();
    return null;
  }
}
