/* See LICENSE for licensing and NOTICE for copyright. */
package org.ldaptive.beans.reflect;

import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.ldaptive.transcode.BooleanValueTranscoder;
import org.ldaptive.transcode.ByteArrayValueTranscoder;
import org.ldaptive.transcode.CertificateValueTranscoder;
import org.ldaptive.transcode.CharArrayValueTranscoder;
import org.ldaptive.transcode.DoubleValueTranscoder;
import org.ldaptive.transcode.FloatValueTranscoder;
import org.ldaptive.transcode.GeneralizedTimeValueTranscoder;
import org.ldaptive.transcode.IntegerValueTranscoder;
import org.ldaptive.transcode.LongValueTranscoder;
import org.ldaptive.transcode.ObjectValueTranscoder;
import org.ldaptive.transcode.ShortValueTranscoder;
import org.ldaptive.transcode.StringValueTranscoder;
import org.ldaptive.transcode.UUIDValueTranscoder;
import org.ldaptive.transcode.ValueTranscoder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Default implementation of a reflection transcoder. Determines the correct underlying reflection transcoder by
 * inspecting the class type characteristics.
 *
 * @author  Middleware Services
 */
public class DefaultReflectionTranscoder implements ReflectionTranscoder
{

  /** Logger for this class. */
  protected final Logger logger = LoggerFactory.getLogger(getClass());

  /** Custom transcoder to override the default transcoder. */
  private final SingleValueReflectionTranscoder<?> customTranscoder;

  /** Transcoder for this type. */
  private final ReflectionTranscoder valueTranscoder;

  /** Set of transcoders support single values. */
  private final Set<SingleValueReflectionTranscoder<?>> singleValueTranscoders;


  /**
   * Creates a new default reflection transcoder.
   *
   * @param  type  of object to transcode
   */
  public DefaultReflectionTranscoder(final Type type)
  {
    this(type, null);
  }


  /**
   * Creates a new default reflection transcoder.
   *
   * @param  type  of object to transcode
   * @param  transcoder  custom transcoder for this type
   */
  public DefaultReflectionTranscoder(final Type type, final ValueTranscoder<?> transcoder)
  {
    if (transcoder != null) {
      customTranscoder = SingleValueReflectionTranscoder.newInstance(transcoder);
    } else {
      customTranscoder = null;
    }
    singleValueTranscoders = getDefaultSingleValueTranscoders();
    if (type instanceof Class) {
      final Class<?> c = (Class<?>) type;
      if (c.isArray()) {
        if (byte[].class == c || char[].class == c) {
          valueTranscoder = getSingleValueReflectionTranscoder(c);
        } else {
          valueTranscoder = new ArrayReflectionTranscoder(getSingleValueReflectionTranscoder(c.getComponentType()));
        }
      } else if (Collection.class.isAssignableFrom(c)) {
        valueTranscoder = getCollectionEncoder(c, Object.class);
      } else {
        valueTranscoder = getSingleValueReflectionTranscoder(c);
      }
    } else if (type instanceof ParameterizedType) {
      final ParameterizedType pt = (ParameterizedType) type;
      final Type rawType = pt.getRawType();
      final Type[] typeArgs = pt.getActualTypeArguments();
      if (typeArgs.length != 1) {
        throw new IllegalArgumentException("Unsupported type arguments: " + Arrays.toString(typeArgs));
      }

      final Class<?> rawClass = ReflectionUtils.classFromType(rawType);
      if (typeArgs[0] instanceof GenericArrayType) {
        final GenericArrayType gat = (GenericArrayType) typeArgs[0];
        if (Collection.class.isAssignableFrom(rawClass)) {
          valueTranscoder = getCollectionEncoder(rawClass, gat);
        } else {
          throw new IllegalArgumentException("Unsupported type: " + rawClass);
        }
      } else if (typeArgs[0] instanceof Class) {
        if (Collection.class.isAssignableFrom(rawClass)) {
          valueTranscoder = getCollectionEncoder(rawClass, typeArgs[0]);
        } else {
          throw new IllegalArgumentException("Unsupported type: " + rawClass);
        }
      } else {
        throw new IllegalArgumentException("Unsupported type: " + rawClass);
      }
    } else {
      throw new IllegalArgumentException("Unsupported type: " + type);
    }
  }


  /**
   * Initializes the set of default single value transcoders.
   *
   * @return  single value transcoders
   */
  protected Set<SingleValueReflectionTranscoder<?>> getDefaultSingleValueTranscoders()
  {
    final Set<SingleValueReflectionTranscoder<?>> transcoders = new HashSet<>();
    transcoders.add(new SingleValueReflectionTranscoder<>(new ObjectValueTranscoder()));
    transcoders.add(new SingleValueReflectionTranscoder<>(new BooleanValueTranscoder()));
    transcoders.add(new SingleValueReflectionTranscoder<>(new BooleanValueTranscoder(true)));
    transcoders.add(new SingleValueReflectionTranscoder<>(new DoubleValueTranscoder()));
    transcoders.add(new SingleValueReflectionTranscoder<>(new DoubleValueTranscoder(true)));
    transcoders.add(new SingleValueReflectionTranscoder<>(new FloatValueTranscoder()));
    transcoders.add(new SingleValueReflectionTranscoder<>(new FloatValueTranscoder(true)));
    transcoders.add(new SingleValueReflectionTranscoder<>(new IntegerValueTranscoder()));
    transcoders.add(new SingleValueReflectionTranscoder<>(new IntegerValueTranscoder(true)));
    transcoders.add(new SingleValueReflectionTranscoder<>(new LongValueTranscoder()));
    transcoders.add(new SingleValueReflectionTranscoder<>(new LongValueTranscoder(true)));
    transcoders.add(new SingleValueReflectionTranscoder<>(new ShortValueTranscoder()));
    transcoders.add(new SingleValueReflectionTranscoder<>(new ShortValueTranscoder(true)));
    transcoders.add(new SingleValueReflectionTranscoder<>(new StringValueTranscoder()));
    transcoders.add(new SingleValueReflectionTranscoder<>(new ByteArrayValueTranscoder()));
    transcoders.add(new SingleValueReflectionTranscoder<>(new CharArrayValueTranscoder()));
    transcoders.add(new SingleValueReflectionTranscoder<>(new CertificateValueTranscoder()));
    transcoders.add(new SingleValueReflectionTranscoder<>(new GeneralizedTimeValueTranscoder()));
    transcoders.add(new SingleValueReflectionTranscoder<>(new UUIDValueTranscoder()));
    return transcoders;
  }


  /**
   * Returns the appropriate single value encoder for the supplied type.
   *
   * @param  type  to provide a single value encoder for
   *
   * @return  single value reflection transcoder
   */
  protected SingleValueReflectionTranscoder getSingleValueReflectionTranscoder(final Class<?> type)
  {
    if (customTranscoder != null) {
      return customTranscoder;
    }
    for (SingleValueReflectionTranscoder transcoder : singleValueTranscoders) {
      @SuppressWarnings("unchecked")
      final boolean supports = transcoder.supports(type);
      if (supports) {
        return transcoder;
      }
    }
    throw new IllegalArgumentException("Unsupported type: " + type);
  }


  /**
   * Returns the appropriate collection encoder for the supplied type.
   *
   * @param  type  to provide a collection encoder for
   * @param  genericType  of the collection
   *
   * @return  reflection transcoder for a collection
   */
  protected ReflectionTranscoder getCollectionEncoder(final Class<?> type, final Type genericType)
  {
    final Class<?> genericClass;
    boolean isGenericArray = false;
    if (genericType instanceof GenericArrayType) {
      final Class<?> c = ReflectionUtils.classFromType(((GenericArrayType) genericType).getGenericComponentType());
      if (Byte.TYPE == c) {
        genericClass = byte[].class;
      } else if (Character.TYPE == c) {
        genericClass = char[].class;
      } else {
        genericClass = c;
        isGenericArray = true;
      }
    } else {
      genericClass = ReflectionUtils.classFromType(genericType);
    }

    final ReflectionTranscoder encoder;
    if (type == Collection.class || List.class.isAssignableFrom(type)) {
      if (isGenericArray) {
        encoder = new ListReflectionTranscoder(
          type,
          new ArrayReflectionTranscoder(getSingleValueReflectionTranscoder(genericClass)));
      } else {
        encoder = new ListReflectionTranscoder(type, getSingleValueReflectionTranscoder(genericClass));
      }
    } else if (Set.class.isAssignableFrom(type)) {
      if (isGenericArray) {
        encoder = new SetReflectionTranscoder(
          type,
          new ArrayReflectionTranscoder(getSingleValueReflectionTranscoder(genericClass)));
      } else {
        encoder = new SetReflectionTranscoder(type, getSingleValueReflectionTranscoder(genericClass));
      }
    } else {
      throw new IllegalArgumentException("Unsupported type: " + type + " with generic type: " + genericType);
    }
    return encoder;
  }


  @Override
  public Object decodeStringValues(final Collection<String> values)
  {
    return valueTranscoder.decodeStringValues(values);
  }


  @Override
  public Object decodeBinaryValues(final Collection<byte[]> values)
  {
    return valueTranscoder.decodeBinaryValues(values);
  }


  @Override
  public Collection<String> encodeStringValues(final Object values)
  {
    return valueTranscoder.encodeStringValues(values);
  }


  @Override
  public Collection<byte[]> encodeBinaryValues(final Object values)
  {
    return valueTranscoder.encodeBinaryValues(values);
  }


  @Override
  public Class<?> getType()
  {
    return valueTranscoder.getType();
  }


  @Override
  public boolean supports(final Class<?> type)
  {
    return valueTranscoder.supports(type);
  }


  @Override
  public String toString()
  {
    return new StringBuilder("[").append(
      getClass().getName()).append("@").append(hashCode()).append("::")
      .append("customTranscoder=").append(customTranscoder).append(", ")
      .append("valueTranscoder=").append(valueTranscoder).append("]").toString();
  }
}
