/*
   Copyright 2015 Immutables Authors and Contributors

   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.
 */
package org.immutables.mongo.types;

import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;
import org.bson.BsonBinary;
import org.bson.BsonRegularExpression;
import org.bson.types.Decimal128;
import org.bson.types.ObjectId;
import org.immutables.metainf.Metainf;
import org.immutables.mongo.bson4gson.BsonReader;
import org.immutables.mongo.bson4gson.BsonWriter;

import javax.annotation.Nullable;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.regex.Pattern;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

/**
 * Built-in BSON type adapters. Also contains reusable delegate-adapters to easily implement own
 * wrapper if needed. Supports {@link Id}, {@link TimeInstant}, {@link Binary}, {@link Pattern}
 * types which correspond to {@code ObjectID}, {@code UTCDate}, {@code Binary}, {@code Regex} native
 * BSON types, not available in JSON.
 * <p>
 * This class also expose simple type adapters to BSON special types, so you can easily write
 * adapters for your own wrapper types.
 * @see #binaryAdapter()
 * @see #objectIdAdapter()
 * @see #objectIdAdapter()
 */
@Metainf.Service
public final class TypeAdapters implements TypeAdapterFactory {
  private static final TypeToken<Id> ID_TYPE_TOKEN = TypeToken.get(Id.class);
  private static final TypeToken<TimeInstant> TIME_INSTANT_TYPE_TOKEN = TypeToken.get(TimeInstant.class);
  private static final TypeToken<Binary> BINARY_TYPE_TOKEN = TypeToken.get(Binary.class);
  private static final TypeToken<Pattern> PATTERN_TYPE_TOKEN = TypeToken.get(Pattern.class);
  private static final TypeToken<Decimal128> DECIMAL128_TYPE_TOKEN = TypeToken.get(Decimal128.class);

  // safe unchecked, typecheck performed by type token equality
  @SuppressWarnings("unchecked")
  @Override
  @Nullable
  public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
    if (ID_TYPE_TOKEN.equals(type)) {
      return (TypeAdapter<T>) WRAPPED_ID_ADAPTER;
    }
    if (TIME_INSTANT_TYPE_TOKEN.equals(type)) {
      return (TypeAdapter<T>) WRAPPED_TIME_INSTANT_ADAPTER;
    }
    if (BINARY_TYPE_TOKEN.equals(type)) {
      return (TypeAdapter<T>) WRAPPED_BINARY_ADAPTER;
    }
    if (PATTERN_TYPE_TOKEN.equals(type)) {
      return (TypeAdapter<T>) PATTERN_ADAPTER;
    }
    if (DECIMAL128_TYPE_TOKEN.equals(type)) {
      return (TypeAdapter<T>) DECIMAL128_ADAPTER;
    }
    return null;
  }

  /**
   * Use this adapter to easily delagate marshaling of custom time instant wrapper.
   * @return {@code Long <==> UTCDate} adapter
   */
  public static TypeAdapter<Long> timeInstantAdapter() {
    return TIME_INSTANT_ADAPTER;
  }

  /**
   * Use this adapter to easily delagate marshaling of custom ObjectID wrapper.
   * @return {@code byte[] <==> ObjectID} adapter
   */
  public static TypeAdapter<byte[]> objectIdAdapter() {
    return OBJECT_ID_ADAPTER;
  }

  /**
   * Use this adapter to easily delagate marshaling of custom Binary wrapper.
   * @return {@code byte[] <==> Binary} adapter
   */
  public static TypeAdapter<byte[]> binaryAdapter() {
    return BINARY_ADAPTER;
  }

  /**
   * Use this adapter (not registered by factory, default Gson adapter is used otherwise) to
   * delagate marshaling of {@link BigDecimal} to {@link Decimal128} type in mongo.
   * @return {@code BigDecimal <==> Decimal128} adapter
   */
  public static TypeAdapter<BigDecimal> decimalAdapter() {
    return DECIMAL_ADAPTER;
  }

  private static final TypeAdapter<TimeInstant> WRAPPED_TIME_INSTANT_ADAPTER = new TypeAdapter<TimeInstant>() {
    @Override
    public void write(JsonWriter out, TimeInstant value) throws IOException {
      if (out instanceof BsonWriter) {
        TIME_INSTANT_ADAPTER.write(out, value.value());
      } else {
        out.value(value.toString());
      }
    }

    @Override
    public TimeInstant read(JsonReader in) throws IOException {
      return TimeInstant.of(TIME_INSTANT_ADAPTER.read(in));
    }

    @Override
    public String toString() {
      return "TypeAdapters.(TimeInstant)";
    }
  };

  private static final TypeAdapter<Id> WRAPPED_ID_ADAPTER = new TypeAdapter<Id>() {
    @Override
    public void write(JsonWriter out, Id value) throws IOException {
      if (out instanceof BsonWriter) {
        OBJECT_ID_ADAPTER.write(out, value.value());
      } else {
        out.value(value.toString());
      }
    }

    @Override
    public Id read(JsonReader in) throws IOException {
      return Id.from(OBJECT_ID_ADAPTER.read(in));
    }

    @Override
    public String toString() {
      return "TypeAdapters.(Id)";
    }
  };

  private static final TypeAdapter<Binary> WRAPPED_BINARY_ADAPTER = new TypeAdapter<Binary>() {
    @Override
    public void write(JsonWriter out, Binary value) throws IOException {
      if (out instanceof BsonWriter) {
        BINARY_ADAPTER.write(out, value.value());
      } else {
        out.value(value.toString());
      }
    }

    @Override
    public Binary read(JsonReader in) throws IOException {
      return Binary.create(BINARY_ADAPTER.read(in));
    }

    @Override
    public String toString() {
      return "TypeAdapters.(Binary)";
    }
  };

  private static final TypeAdapter<Pattern> PATTERN_ADAPTER = new TypeAdapter<Pattern>() {
    @Override
    public void write(JsonWriter out, Pattern value) throws IOException {
      if (value == null) {
        out.nullValue();
      } else if (out instanceof BsonWriter) {
        ((BsonWriter) out).unwrap()
                .writeRegularExpression(new BsonRegularExpression(value.pattern()));
      } else {
        out.value(value.toString());
      }
    }

    @Override
    public Pattern read(JsonReader in) throws IOException {
      if (in.peek() == JsonToken.NULL) {
        in.nextNull();
        return null;
      }
      if (in instanceof BsonReader) {
        final String pattern = ((BsonReader) in).unwrap()
                .readRegularExpression().getPattern();
        return Pattern.compile(pattern);
      }
      return Pattern.compile(in.nextString());
    }

    @Override
    public String toString() {
      return "TypeAdapters.(Pattern)";
    }
  };

  private static final TypeAdapter<Decimal128> DECIMAL128_ADAPTER = new TypeAdapter<Decimal128>() {
    @Override
    public void write(JsonWriter out, Decimal128 value) throws IOException {
      if (value == null) {
        out.nullValue();
      } else if (out instanceof BsonWriter) {
        ((BsonWriter) out).unwrap().writeDecimal128(value);
      } else {
        out.value(value.toString());
      }
    }

    @Override
    public Decimal128 read(JsonReader in) throws IOException {
      if (in.peek() == JsonToken.NULL) {
        in.nextNull();
        return null;
      }
      if (in instanceof BsonReader) {
        return ((BsonReader) in).unwrap().readDecimal128();
      }
      return Decimal128.parse(in.nextString());
    }

    @Override
    public String toString() {
      return "TypeAdapters.(Decimal128)";
    }
  };

  private static final TypeAdapter<BigDecimal> DECIMAL_ADAPTER = new TypeAdapter<BigDecimal>() {
    @Override
    public void write(JsonWriter out, BigDecimal value) throws IOException {
      checkArgument(out instanceof BsonWriter, "Should be BsonWriter, not some other JsonWriter");
      checkNotNull(value, "Value could not be null, delegate to #nullSafe() adapter if needed");
      ((BsonWriter) out).value(value);
    }

    @Override
    public BigDecimal read(JsonReader in) throws IOException {
      checkArgument(in instanceof BsonReader, "Should be BsonReader, not some other JsonReader");
      return ((BsonReader) in).unwrap().readDecimal128().bigDecimalValue();
    }

    @Override
    public String toString() {
      return "TypeAdapters.decimalAdapter()";
    }
  };

  private static final TypeAdapter<Long> TIME_INSTANT_ADAPTER = new TypeAdapter<Long>() {
    @Override
    public void write(JsonWriter out, Long value) throws IOException {
      checkArgument(out instanceof BsonWriter, "Should be BsonWriter, not some other JsonWriter");
      checkNotNull(value, "Value could not be null, delegate to #nullSafe() adapter if needed");
      ((BsonWriter) out).unwrap()
              .writeDateTime(value);
    }

    @Override
    public Long read(JsonReader in) throws IOException {
      checkArgument(in instanceof BsonReader, "Should be BsonReader, not some other JsonReader");
      return ((BsonReader) in).unwrap().readDateTime();
    }

    @Override
    public String toString() {
      return "TypeAdapters.timeInstantAdapter()";
    }
  };

  private static final TypeAdapter<byte[]> OBJECT_ID_ADAPTER = new TypeAdapter<byte[]>() {
    @Override
    public void write(JsonWriter out, byte[] value) throws IOException {
      checkArgument(out instanceof BsonWriter, "Should be BsonWriter, not some other JsonWriter");
      checkNotNull(value, "Value could not be null, delegate to #nullSafe() adapter if needed");
      ((BsonWriter) out).unwrap().writeObjectId(new ObjectId(value));
    }

    @Override
    public byte[] read(JsonReader in) throws IOException {
      checkArgument(in instanceof BsonReader, "Should be BsonReader, not some other JsonReader");
      return ((BsonReader) in).unwrap().readObjectId().toByteArray();
    }

    @Override
    public String toString() {
      return "TypeAdapters.objectIdAdapter()";
    }
  };

  private static final TypeAdapter<byte[]> BINARY_ADAPTER = new TypeAdapter<byte[]>() {
    @Override
    public void write(JsonWriter out, byte[] value) throws IOException {
      checkArgument(out instanceof BsonWriter, "Should be BsonWriter, not some other JsonWriter");
      checkNotNull(value, "Value could not be null, delegate to #nullSafe() adapter if needed");
      ((BsonWriter) out).unwrap().writeBinaryData(new BsonBinary(value));
    }

    @Override
    public byte[] read(JsonReader in) throws IOException {
      checkArgument(in instanceof BsonReader, "Should be BsonReader, not some other JsonReader");
      return ((BsonReader) in).unwrap().readBinaryData().getData();
    }

    @Override
    public String toString() {
      return "TypeAdapters.binaryAdapter()";
    }
  };
}