/*
 * Copyright DataStax, Inc.
 *
 * 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 com.datastax.oss.driver.examples.json.jsr;

import com.datastax.oss.driver.api.core.ProtocolVersion;
import com.datastax.oss.driver.api.core.type.DataType;
import com.datastax.oss.driver.api.core.type.DataTypes;
import com.datastax.oss.driver.api.core.type.codec.TypeCodec;
import com.datastax.oss.driver.api.core.type.reflect.GenericType;
import com.datastax.oss.driver.internal.core.util.Strings;
import com.datastax.oss.protocol.internal.util.Bytes;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.nio.ByteBuffer;
import java.util.Map;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonException;
import javax.json.JsonObject;
import javax.json.JsonReader;
import javax.json.JsonReaderFactory;
import javax.json.JsonStructure;
import javax.json.JsonWriter;
import javax.json.JsonWriterFactory;

/**
 * A JSON codec that uses the <a href="https://jcp.org/en/jsr/detail?id=353">Java API for JSON
 * processing</a> to perform serialization and deserialization of JSON structures.
 *
 * <p>More specifically, this codec maps an arbitrary {@link JsonStructure} to a CQL {@code varchar}
 * column.
 *
 * <p>This codec handles the Java type {@link JsonStructure}. It is therefore required that values
 * are set and retrieved using that exact Java type; users should manually downcast to either {@link
 * JsonObject} or {@link JsonArray}, as in the example below:
 *
 * <pre>{@code
 * // setting values
 * JsonObject myObject = ...
 * PreparedStatement ps = ...
 * // set values using JsonStructure as target Java type
 * BoundStatement bs = ps.bind().set(1, myObject, JsonStructure.class);
 *
 * // retrieving values
 * Row row = session.execute(bs).one();
 * // use JsonStructure as target Java type to retrieve values
 * JsonStructure json = row.get(0, JsonStructure.class);
 * if (json instanceof JsonObject) {
 *     myObject = (JsonObject) json;
 *     ...
 * }
 * }</pre>
 *
 * <p>Note that at runtime, this codec requires the presence of both JSR-353 API and a
 * JSR-353-compatible runtime library, such as <a
 * href="https://jsonp.java.net/download.html">JSR-353's reference implementation</a>. If you use
 * Maven, this can be done by declaring the following dependencies in your project:
 *
 * <pre>{@code
 * <dependency>
 *   <groupId>javax.json</groupId>
 *   <artifactId>javax.json-api</artifactId>
 *   <version>1.0</version>
 * </dependency>
 *
 * <dependency>
 *   <groupId>org.glassfish</groupId>
 *   <artifactId>javax.json</artifactId>
 *   <version>1.1.4</version>
 * </dependency>
 * }</pre>
 */
public class Jsr353JsonCodec implements TypeCodec<JsonStructure> {

  private final JsonReaderFactory readerFactory;

  private final JsonWriterFactory writerFactory;

  /** Creates a new instance using a default configuration. */
  public Jsr353JsonCodec() {
    this(null);
  }

  /**
   * Creates a new instance using the provided configuration.
   *
   * @param config A map of provider-specific configuration properties. May be empty or {@code
   *     null}.
   */
  public Jsr353JsonCodec(@Nullable Map<String, ?> config) {
    readerFactory = Json.createReaderFactory(config);
    writerFactory = Json.createWriterFactory(config);
  }

  @NonNull
  @Override
  public GenericType<JsonStructure> getJavaType() {
    return GenericType.of(JsonStructure.class);
  }

  @NonNull
  @Override
  public DataType getCqlType() {
    return DataTypes.TEXT;
  }

  @Nullable
  @Override
  public ByteBuffer encode(
      @Nullable JsonStructure value, @NonNull ProtocolVersion protocolVersion) {
    if (value == null) {
      return null;
    }
    try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
      JsonWriter writer = writerFactory.createWriter(baos);
      writer.write(value);
      return ByteBuffer.wrap(baos.toByteArray());
    } catch (JsonException | IOException e) {
      throw new IllegalArgumentException("Failed to encode value as JSON", e);
    }
  }

  @Nullable
  @Override
  public JsonStructure decode(
      @Nullable ByteBuffer bytes, @NonNull ProtocolVersion protocolVersion) {
    if (bytes == null) {
      return null;
    }
    try (ByteArrayInputStream bais = new ByteArrayInputStream(Bytes.getArray(bytes))) {
      JsonReader reader = readerFactory.createReader(bais);
      return reader.read();
    } catch (JsonException | IOException e) {
      throw new IllegalArgumentException("Failed to decode JSON value", e);
    }
  }

  @NonNull
  @Override
  public String format(@Nullable JsonStructure value) {
    if (value == null) {
      return "NULL";
    }
    String json;
    try (StringWriter sw = new StringWriter()) {
      JsonWriter writer = writerFactory.createWriter(sw);
      writer.write(value);
      json = sw.toString();
    } catch (JsonException | IOException e) {
      throw new IllegalArgumentException("Failed to format value as JSON", e);
    }
    return Strings.quote(json);
  }

  @Nullable
  @Override
  public JsonStructure parse(String value) {
    if (value == null || value.isEmpty() || value.equalsIgnoreCase("NULL")) {
      return null;
    }
    if (!Strings.isQuoted(value)) {
      throw new IllegalArgumentException("JSON strings must be enclosed by single quotes");
    }
    String json = Strings.unquote(value);
    try (StringReader sr = new StringReader(json)) {
      JsonReader reader = readerFactory.createReader(sr);
      return reader.read();
    } catch (JsonException e) {
      throw new IllegalArgumentException("Failed to parse value as JSON", e);
    }
  }
}
