
package com.outerspacecat.json;

import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.outerspacecat.util.Base64;
import com.outerspacecat.util.ImmutableLong;
import com.outerspacecat.util.Path;
import com.outerspacecat.util.Utils;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

/**
 * Defines methods for retrieving values from a {@link Map} and verifying their
 * expected types. Useful for reducing boilerplate in implementations of
 * {@link UserTypeMarshaler}.
 * 
 * @author Caleb Richardson
 */
public final class UserTypeFields
{
  private UserTypeFields ()
  {
  }

  /**
   * Retrieves a value with type {@code t} mapped by {@code name}.
   * 
   * @param name the key for the value. Must be non {@code null}.
   * @param t the type or a supertype of the value. Must be non {@code null}.
   * @param fields the map to extract the value from. Must be non {@code null}.
   * @return a value with the specified type. Never {@code null}.
   * @throws MarshalerException if no value with the specified type is mapped by
   *         {@code name}
   * @param <T> the type of the value to retrieve
   */
  @SuppressWarnings ( "unchecked" )
  public static <T> T read ( final String name, final Class<T> t,
      final Map<String, ?> fields )
    throws MarshalerException
  {
    Preconditions.checkNotNull(name, "name required");
    Preconditions.checkNotNull(t, "t required");
    Preconditions.checkNotNull(fields, "fields required");

    Object value = fields.get(name);
    if ( value != null && t.isAssignableFrom(value.getClass()) )
      return (T) value;

    throw new MarshalerException("\"" + name + "\" is not a valid " +
        t.getName());
  }

  /**
   * Retrieves a {@code boolean} mapped by {@code name}.
   * 
   * @param name the key for the {@code boolean}. Must be non {@code null}.
   * @param fields the map to extract the {@code boolean} from. Must be non
   *        {@code null}.
   * @return a {@code boolean}. Never {@code null}.
   * @throws MarshalerException if no {@code boolean} is mapped by {@code name}
   */
  public static boolean readBoolean ( final String name,
      final Map<String, ?> fields )
    throws MarshalerException
  {
    return read(name, Boolean.class, fields);
  }

  /**
   * Retrieves an {@code int} mapped by {@code name}.
   * 
   * @param name the key for the {@code int}. Must be non {@code null}.
   * @param fields the map to extract the {@code int} from. Must be non
   *        {@code null}.
   * @return an {@code int}. Never {@code null}.
   * @throws MarshalerException if no {@code int} is mapped by {@code name}
   */
  public static int readInt ( final String name, final Map<String, ?> fields )
    throws MarshalerException
  {
    return read(name, Integer.class, fields);
  }

  /**
   * Retrieves a {@link String} mapped by {@code name}.
   * 
   * @param name the key for the {@link String}. Must be non {@code null}.
   * @param fields the map to extract the {@link String} from. Must be non
   *        {@code null}.
   * @return a {@link String}. Never {@code null}.
   * @throws MarshalerException if no {@link String} is mapped by {@code name}
   */
  public static String readString ( final String name,
      final Map<String, ?> fields )
    throws MarshalerException
  {
    return read(name, String.class, fields);
  }

  /**
   * Retrieves a {@link ImmutableLong} mapped by {@code name}.
   * 
   * @param name the key for the {@link ImmutableLong}. Must be non {@code null}
   *        .
   * @param fields the map to extract the {@link ImmutableLong} from. Must be
   *        non {@code null}.
   * @return a {@code long}. Never {@code null}.
   * @throws MarshalerException if no {@link ImmutableLong} is mapped by
   *         {@code name}
   */
  public static long readImmutableLong ( final String name,
      final Map<String, ?> fields )
    throws MarshalerException
  {
    Preconditions.checkNotNull(name, "name required");
    Preconditions.checkNotNull(fields, "fields required");

    Object value = fields.get(name);
    if ( value instanceof String ) {
      try {
        return new ImmutableLong((String) value).getLongValue();
      } catch ( IllegalArgumentException e ) {
      }
    }

    throw new MarshalerException("\"" + name +
        "\" is not a valid ImmutableLong");
  }

  /**
   * Retrieves a {@link UUID} mapped by {@code name}.
   * 
   * @param name the key for the {@link UUID}. Must be non {@code null}.
   * @param fields the map to extract the {@link UUID} from. Must be non
   *        {@code null}.
   * @return a {@link UUID}. Never {@code null}.
   * @throws MarshalerException if no {@link UUID} is mapped by {@code name}
   */
  public static UUID readUuid ( final String name, final Map<String, ?> fields )
    throws MarshalerException
  {
    Preconditions.checkNotNull(name, "name required");
    Preconditions.checkNotNull(fields, "fields required");

    Object value = fields.get(name);
    if ( value instanceof String ) {
      try {
        return UUID.fromString((String) value);
      } catch ( IllegalArgumentException e ) {
      }
    }

    throw new MarshalerException("\"" + name + "\" is not a valid UUID");
  }

  /**
   * Retrieves a {@link Path} mapped by {@code name}.
   * 
   * @param name the key for the {@link Path}. Must be non {@code null}.
   * @param fields the map to extract the {@link Path} from. Must be non
   *        {@code null}.
   * @return a {@link Path}. Never {@code null}.
   * @throws MarshalerException if no {@link Path} is mapped by {@code name}
   */
  public static Path readPath ( final String name, final Map<String, ?> fields )
    throws MarshalerException
  {
    Preconditions.checkNotNull(name, "name required");
    Preconditions.checkNotNull(fields, "fields required");

    Object value = fields.get(name);
    if ( value instanceof String ) {
      try {
        return new Path((String) value);
      } catch ( IllegalArgumentException e ) {
      }
    }

    throw new MarshalerException("\"" + name + "\" is not a valid Path");
  }

  /**
   * Retrieves a {@link Set} that contains objects of type {@code t} and is
   * mapped by {@code name}.
   * 
   * @param name the key for the {@link Set}. Must be non {@code null}.
   * @param t the type or a supertype of the objects in the {@link Set}. Must be
   *        non {@code null}.
   * @param fields the map to extract the {@link Set} from. Must be non
   *        {@code null}.
   * @return a {@link Set}. Never {@code null}.
   * @throws MarshalerException if no {@link Set} that contains objects of type
   *         {@code t} is mapped by {@code name}
   * @param <T> the type of objects in the {@link Set}
   */
  @SuppressWarnings ( "unchecked" )
  public static <T> Set<T> readSet ( final String name, final Class<T> t,
      final Map<String, ?> fields )
    throws MarshalerException
  {
    Preconditions.checkNotNull(name, "name required");
    Preconditions.checkNotNull(t, "t required");
    Preconditions.checkNotNull(fields, "fields required");

    Object value = fields.get(name);
    if ( value instanceof List ) {
      Set<T> s = Sets.newHashSet();

      for ( Object e : ((List<?>) value) ) {
        if ( e == null || !t.isAssignableFrom(e.getClass()) )
          throw new MarshalerException("\"" + name +
              "\" is not a valid java.util.Set of " + t.getName());

        s.add((T) e);
      }

      return s;
    }

    throw new MarshalerException("\"" + name + "\" is not a valid Set of " +
        t.getName());
  }

  /**
   * Retrieves a {@link List} that contains objects of type {@code t} and is
   * mapped by {@code name}.
   * 
   * @param name the key for the {@link List}. Must be non {@code null}.
   * @param t the type or a supertype of the objects in the {@link List}. Must
   *        be non {@code null}.
   * @param fields the map to extract the {@link List} from. Must be non
   *        {@code null}.
   * @return a {@link List}. Never {@code null}.
   * @throws MarshalerException if no {@link List} that contains objects of type
   *         {@code t} is mapped by {@code name}
   * @param <T> the type of objects in the {@link List}
   */
  @SuppressWarnings ( "unchecked" )
  public static <T> List<T> readList ( final String name, final Class<T> t,
      final Map<String, ?> fields )
    throws MarshalerException
  {
    Preconditions.checkNotNull(name, "name required");
    Preconditions.checkNotNull(t, "t required");
    Preconditions.checkNotNull(fields, "fields required");

    Object value = fields.get(name);
    if ( value instanceof List ) {
      List<T> l = Lists.newArrayList();

      for ( Object e : ((List<?>) value) ) {
        if ( e == null || !t.isAssignableFrom(e.getClass()) )
          throw new MarshalerException("\"" + name +
              "\" is not a valid List of " + t);

        l.add((T) e);
      }

      return l;
    }

    throw new MarshalerException("\"" + name + "\" is not a valid List of " +
        t.getName());
  }

  /**
   * Retrieves a {@link Map} that has keys of type {@code k} and values of type
   * {@code v} and is mapped by {@code name}.
   * 
   * @param name the key for the {@link Map}. Must be non {@code null}.
   * @param k the type or a supertype of the keys in the {@link Map}. Must be
   *        non {@code null}.
   * @param v the type or a supertype of the values in the {@link Map}. Must be
   *        non {@code null}.
   * @param fields the map to extract the {@link Map} from. Must be non
   *        {@code null}.
   * @return a {@link Map}. Never {@code null}.
   * @throws MarshalerException if no {@link Map} that has keys of type
   *         {@code k} and values of type {@code v} is mapped by {@code name}
   * @param <K> the type of keys in the {@link Map}
   * @param <V> the type of values in the {@link Map}
   */
  @SuppressWarnings ( "unchecked" )
  public static <K, V> Map<K, V> readMap ( final String name, final Class<K> k,
      final Class<V> v, final Map<String, ?> fields )
    throws MarshalerException
  {
    Preconditions.checkNotNull(name, "name required");
    Preconditions.checkNotNull(k, "k required");
    Preconditions.checkNotNull(v, "v required");
    Preconditions.checkNotNull(fields, "fields required");

    Object value = fields.get(name);
    if ( value instanceof Map ) {
      Map<K, V> m = Maps.newHashMap();

      for ( Map.Entry<?, ?> e : ((Map<?, ?>) value).entrySet() ) {
        if ( e.getKey() == null || e.getValue() == null ||
            !k.isAssignableFrom(e.getKey().getClass()) ||
            !v.isAssignableFrom(e.getValue().getClass()) )
          throw new MarshalerException("\"" + name +
              "\" is not a valid Map of " + k + " => " + v);

        m.put((K) e.getKey(), (V) e.getValue());
      }

      return m;
    }

    throw new MarshalerException("\"" + name +
        "\" is not a valid java.util.Map of " + k.getName() + " => " +
        v.getName());
  }

  /**
   * Retrieves a hex-encoded byte array mapped by {@code name}.
   * 
   * @param name the key for the byte array. Must be non {@code null}.
   * @param fields the map to extract the byte array from. Must be non
   *        {@code null}.
   * @return a byte array. Never {@code null}.
   * @throws MarshalerException if no hex-encoded byte array is mapped by
   *         {@code name}
   */
  public static byte[] readHexBytes ( final String name,
      final Map<String, ?> fields )
    throws MarshalerException
  {
    Preconditions.checkNotNull(name, "name required");
    Preconditions.checkNotNull(fields, "fields required");

    Object value = fields.get(name);
    if ( value instanceof String ) {
      try {
        return Utils.fromHex(((String) value).toCharArray());
      } catch ( IllegalArgumentException e ) {
      }
    }

    throw new MarshalerException("\"" + name +
        "\" is not a valid hex encoded byte array");
  }

  /**
   * Retrieves a base64-encoded byte array mapped by {@code name}.
   * 
   * @param name the key for the byte array. Must be non {@code null}.
   * @param fields the map to extract the byte array from. Must be non
   *        {@code null}.
   * @return a byte array. Never {@code null}.
   * @throws MarshalerException if no base-64 encoded byte array is mapped by
   *         {@code name}
   */
  public static byte[] readBase64Bytes ( final String name,
      final Map<String, ?> fields )
    throws MarshalerException
  {
    Preconditions.checkNotNull(name, "name required");
    Preconditions.checkNotNull(fields, "fields required");

    Object value = fields.get(name);
    if ( value instanceof String ) {
      try {
        return Base64.decode(((String) value).toCharArray(), false);
      } catch ( IOException e ) {
      }
    }

    throw new MarshalerException("\"" + name +
        "\" is not a valid base64 encoded byte array");
  }
}
