/**
 * Copyright 2013 OpenSocial Foundation
 * Copyright 2013 International Business Machines Corporation
 *
 * Licensed 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.
 *
 * Utility library for working with Activity Streams Actions
 * Requires underscorejs.
 *
 * @author James M Snell ([email protected])
 */
package com.ibm.common.activitystreams.internal;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.Iterables.getFirst;
import static com.google.common.collect.Iterables.size;

import static com.ibm.common.activitystreams.Makers.actions;
import static com.ibm.common.activitystreams.Makers.linkValues;
import static com.ibm.common.activitystreams.internal.ASObjectAdapter.primConverter;

import java.lang.reflect.Type;
import java.util.Date;
import java.util.Map;

import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.joda.time.Interval;
import org.joda.time.Period;
import org.joda.time.format.ISODateTimeFormat;

import com.google.common.base.Optional;
import com.google.common.base.Throwables;
import com.google.common.collect.BoundType;
import com.google.common.collect.ImmutableTable;
import com.google.common.collect.Range;
import com.google.common.collect.Table;
import com.google.common.net.MediaType;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonElement;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.internal.LazilyParsedNumber;
import com.ibm.common.activitystreams.ASObject;
import com.ibm.common.activitystreams.ActionsValue;
import com.ibm.common.activitystreams.LinkValue;
import com.ibm.common.activitystreams.util.AbstractDictionaryObjectAdapter;

/**
 * @author james
 * @version $Revision: 1.0 $
 */
final class Adapters {

  private Adapters() {}

  /**
   * Method forEnum.
   * @param _enumClass Class<E>
   * @return EnumAdapter<E>
   */
  static <E extends Enum<E>>EnumAdapter<E> forEnum(Class<E> _enumClass) {
    return new EnumAdapter<E>(_enumClass);
  }
  
  /**
   * Method forEnum.
   * @param _enumClass Class<E>
   * @param or E
   * @return EnumAdapter<E>
   */
  static <E extends Enum<E>>EnumAdapter<E> forEnum(Class<E> _enumClass, E or) {
    return new EnumAdapter<E>(_enumClass,or);
  }
  
  static final Adapter<com.ibm.common.activitystreams.NLV> NLV = 
    new NaturalLanguageValueAdapter();
  
  static final Adapter<ActionsValue> ACTIONS =
    new AbstractDictionaryObjectAdapter
      <LinkValue,
       ActionsValue,
       ActionsValue.Builder>(LinkValue.class) {
    public JsonElement serialize(
      ActionsValue actions, 
      Type type,
      JsonSerializationContext context) {
        JsonObject obj = new JsonObject();
        for (String verb : actions) {
          Iterable<LinkValue> links = 
            actions.get(verb);
          obj.add(
            verb, 
            context.serialize(
              size(links) == 1 ? // if there's only one, serialize just 1
                getFirst(links,null) : // otherwise, serialize the list
                linkValues().add(links).get(),
              LinkValue.class));
        }
        return obj;
    }
    @Override
    protected ActionsValue.Builder builder() {
      return actions();
    }
  };
  
  static final Adapter<Iterable<?>> ITERABLE = 
    new Adapter<Iterable<?>>() {

      public JsonElement serialize(
        Iterable<?> i, 
        Type type,
        JsonSerializationContext context) {
        JsonArray ary = new JsonArray();
        for (Object obj : i)
          ary.add(context.serialize(obj, obj.getClass()));
        return ary;
      }

      public Iterable<?> deserialize(JsonElement arg0, Type arg1,
          JsonDeserializationContext arg2) throws JsonParseException {
        return null; // handled elsewhere
      }
    
  };
  
  static final Adapter<Date> DATE = 
    new SimpleAdapter<Date>() {
      protected String serialize(Date t) {
        return ISODateTimeFormat.dateTime().print(new DateTime(t));
      }
      public Date apply(String v) {
        return DateTime.parse(v).toDate();
      }
    };
  
  static final Adapter<DateTime> DATETIME =
    new SimpleAdapter<DateTime>() {
      protected String serialize(DateTime t) {
        return ISODateTimeFormat.dateTime().print(t);
      }
      public DateTime apply(String v) {
        return DateTime.parse(v);
      }
    };
  
  static final Adapter<Duration> DURATION =  
    new SimpleAdapter<Duration>() {
      public Duration apply(String v) {
        return Duration.parse(v);
      }
    };

  static final Adapter<Period> PERIOD =
    new SimpleAdapter<Period>() {
      public Period apply(String v) {
        return Period.parse(v);
      }
  };

  static final Adapter<Interval> INTERVAL =
    new SimpleAdapter<Interval>() {
      public Interval apply(String v) {
        return Interval.parse(v);
      }
    };
  
  static final Adapter<MediaType> MIMETYPE =
    new SimpleAdapter<MediaType>() {
      public MediaType apply(String v) {
        return MediaType.parse(v);
      }
    };

  static final MultimapAdapter MULTIMAP = 
    new MultimapAdapter();
  

  static final Adapter<Range<?>> RANGE = 
    new Adapter<Range<?>>() {

    public JsonElement serialize(
      Range<?> src, 
      Type typeOfSrc,
      JsonSerializationContext context) {
      JsonObject el = new JsonObject();
      el.add("lower", makeBound(src.lowerBoundType(),src.lowerEndpoint(),context));
      el.add("upper", makeBound(src.upperBoundType(),src.upperEndpoint(),context));
      return el;
    }
    
    private JsonElement makeBound(
      BoundType type, 
      Object val, 
      JsonSerializationContext context) {
      JsonObject obj = new JsonObject();
      obj.add("type", context.serialize(type.name().toLowerCase()));
      obj.add("endpoint", context.serialize(val));
      return obj;
    }

    @SuppressWarnings("rawtypes")
    public Range<?> deserialize(
      JsonElement json, 
      Type typeOfT,
      JsonDeserializationContext context) 
        throws JsonParseException {
      checkArgument(json.isJsonObject());
      try {
        JsonObject obj = json.getAsJsonObject();
        JsonObject upper = obj.getAsJsonObject("upper");
        JsonObject lower = obj.getAsJsonObject("lower");
        BoundType ubt = bt(upper.getAsJsonPrimitive("type"));
        BoundType lbt = bt(lower.getAsJsonPrimitive("type"));
        Object ub = des(upper.get("endpoint"),context);
        Object lb = des(lower.get("endpoint"),context);
        return Range.range((Comparable)lb, lbt, (Comparable)ub, ubt);
      } catch (Throwable t) {
        throw Throwables.propagate(t);
      }
    }
    
    private Object des(JsonElement val, JsonDeserializationContext context) {
      if (val.isJsonArray())
        return MultimapAdapter.arraydes(val.getAsJsonArray(), context);
      else if (val.isJsonObject())
        return context.deserialize(val, ASObject.class);
      else if (val.isJsonPrimitive()) {
        Object v = primConverter.convert(val.getAsJsonPrimitive());
        if (v instanceof LazilyParsedNumber) 
          v = new LazilyParsedNumberComparable((LazilyParsedNumber) v);
        return v;
      }
      else
        return null;
    }
    
    private BoundType bt(JsonPrimitive p) {
      try {
        return BoundType.valueOf(p.toString());
      } catch (Throwable t) {
        return BoundType.CLOSED;
      }
    }
  };
  
  static final Adapter<Optional<?>> OPTIONAL =
    new Adapter<Optional<?>>() {
      public JsonElement serialize(
        Optional<?> src, 
        Type typeOfSrc,
        JsonSerializationContext context) {
          return context.serialize(src.orNull());
      }
      public Optional<?> deserialize(
        JsonElement json, 
        Type typeOfT,
        JsonDeserializationContext context)
          throws JsonParseException {
        return null;
      }
  };
  
  static final Adapter<Table<?,?,?>> TABLE = 
    new Adapter<Table<?,?,?>>() {

    public JsonElement serialize(
      Table<?, ?, ?> src, 
      Type typeOfSrc,
      JsonSerializationContext context) {
      
      JsonObject obj = new JsonObject();
      for (Table.Cell<?, ?, ?> cell : src.cellSet()) {
        String r = cell.getRowKey().toString();
        String c = cell.getColumnKey().toString();
        JsonObject rowobj = null;
        if (!obj.has(r)) {
          rowobj = new JsonObject();
          obj.add(r, rowobj);
        } else {
          rowobj = obj.getAsJsonObject(r);
        }
        Object val = cell.getValue();
        if (val != null)
          rowobj.add(c, context.serialize(val,val.getClass()));
      }
      
      return obj;
    }

    public Table<?, ?, ?> deserialize(
      JsonElement json, 
      Type typeOfT,
      JsonDeserializationContext context) 
        throws JsonParseException {
      ImmutableTable.Builder<String,String,Object> table = 
        ImmutableTable.builder();
      checkArgument(json.isJsonObject());
      JsonObject obj = json.getAsJsonObject();
      for (Map.Entry<String,JsonElement> rowentry : obj.entrySet()) {
        String row = rowentry.getKey();
        JsonElement cell = rowentry.getValue();
        checkArgument(cell.isJsonObject());
        for (Map.Entry<String,JsonElement> cellentry : cell.getAsJsonObject().entrySet()) {
          String ckey = cellentry.getKey();
          JsonElement val = cellentry.getValue();
          Object desval = null;
          if (val.isJsonArray())
            desval = MultimapAdapter.arraydes(val.getAsJsonArray(),context);
          else if (val.isJsonObject())
            desval = context.deserialize(val, ASObject.class);
          else if (val.isJsonPrimitive())
            desval = primConverter.convert(val.getAsJsonPrimitive());
          if (desval != null)
            table.put(row,ckey,desval);
        }
      }
      return table.build();
    }

  };
}