/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.avro.data;

import java.io.IOException;
import java.io.StringReader;
import java.util.Iterator;

import org.apache.avro.util.internal.JacksonUtils;
import org.codehaus.jackson.JsonFactory;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.node.JsonNodeFactory;
import org.codehaus.jackson.node.LongNode;
import org.codehaus.jackson.node.DoubleNode;
import org.codehaus.jackson.node.TextNode;
import org.codehaus.jackson.node.BooleanNode;
import org.codehaus.jackson.node.NullNode;
import org.codehaus.jackson.node.ArrayNode;
import org.codehaus.jackson.node.ObjectNode;

import org.apache.avro.Schema;
import org.apache.avro.AvroRuntimeException;
import org.apache.avro.io.DatumReader;
import org.apache.avro.io.DatumWriter;
import org.apache.avro.io.Encoder;
import org.apache.avro.io.Decoder;
import org.apache.avro.io.DecoderFactory;
import org.apache.avro.io.ResolvingDecoder;

/** Utilities for reading and writing arbitrary Json data in Avro format. */
public class Json {
  private Json() {}                               // singleton: no public ctor

  static final JsonFactory FACTORY = new JsonFactory();
  static final ObjectMapper MAPPER = new ObjectMapper(FACTORY);

  /** The schema for Json data. */
  public static final Schema SCHEMA;
  static {
    try {
      SCHEMA = Schema.parse
        (Json.class.getResourceAsStream("/org/apache/avro/data/Json.avsc"));
    } catch (IOException e) {
      throw new AvroRuntimeException(e);
    }
  }

  /**
   * {@link DatumWriter} for arbitrary Json data.
   * @deprecated use {@link ObjectWriter}
   */
  @Deprecated
  public static class Writer implements DatumWriter<JsonNode> {

    @Override public void setSchema(Schema schema) {
      if (!SCHEMA.equals(schema))
        throw new RuntimeException("Not the Json schema: "+schema);
    }

    @Override
    public void write(JsonNode datum, Encoder out) throws IOException {
      Json.write(datum, out);
    }
  }

  /**
   * {@link DatumReader} for arbitrary Json data.
   * @deprecated use {@link ObjectReader}
   */
  @Deprecated
  public static class Reader implements DatumReader<JsonNode> {
    private Schema written;
    private ResolvingDecoder resolver;

    @Override public void setSchema(Schema schema) {
      this.written = SCHEMA.equals(written) ? null : schema;
    }

    @Override
    public JsonNode read(JsonNode reuse, Decoder in) throws IOException {
      if (written == null)                        // same schema
        return Json.read(in);

      // use a resolver to adapt alternate version of Json schema
      if (resolver == null)
        resolver = DecoderFactory.get().resolvingDecoder(written, SCHEMA, null);
      resolver.configure(in);
      JsonNode result = Json.read(resolver);
      resolver.drain();
      return result;
    }
  }

  /** {@link DatumWriter} for arbitrary Json data using the object model described
   *  in {@link org.apache.avro.JsonProperties}. */
  public static class ObjectWriter implements DatumWriter<Object> {

    @Override public void setSchema(Schema schema) {
      if (!SCHEMA.equals(schema))
        throw new RuntimeException("Not the Json schema: "+schema);
    }

    @Override
    public void write(Object datum, Encoder out) throws IOException {
      Json.writeObject(datum, out);
    }
  }

  /** {@link DatumReader} for arbitrary Json data using the object model described
   *  in {@link org.apache.avro.JsonProperties}. */
  public static class ObjectReader implements DatumReader<Object> {
    private Schema written;
    private ResolvingDecoder resolver;

    @Override public void setSchema(Schema schema) {
      this.written = SCHEMA.equals(written) ? null : schema;
    }

    @Override
    public Object read(Object reuse, Decoder in) throws IOException {
      if (written == null)                        // same schema
        return Json.readObject(in);

      // use a resolver to adapt alternate version of Json schema
      if (resolver == null)
        resolver = DecoderFactory.get().resolvingDecoder(written, SCHEMA, null);
      resolver.configure(in);
      Object result = Json.readObject(resolver);
      resolver.drain();
      return result;
    }
  }

  /**
   * Parses a JSON string and converts it to the object model described in
   * {@link org.apache.avro.JsonProperties}.
   */
  public static Object parseJson(String s) {
    try {
      return JacksonUtils.toObject(MAPPER.readTree(FACTORY.createJsonParser(
          new StringReader(s))));
    } catch (JsonParseException e) {
      throw new RuntimeException(e);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Converts an instance of the object model described in
   * {@link org.apache.avro.JsonProperties} to a JSON string.
   */
  public static String toString(Object datum) {
    return JacksonUtils.toJsonNode(datum).toString();
  }

  /** Note: this enum must be kept aligned with the union in Json.avsc. */
  private enum JsonType { LONG, DOUBLE, STRING, BOOLEAN, NULL, ARRAY, OBJECT }

  /**
   * Write Json data as Avro data.
   * @deprecated internal method
   */
  @Deprecated
  public static void write(JsonNode node, Encoder out) throws IOException {
    switch(node.asToken()) {
    case VALUE_NUMBER_INT:
      out.writeIndex(JsonType.LONG.ordinal());
      out.writeLong(node.getLongValue());
      break;
    case VALUE_NUMBER_FLOAT:
      out.writeIndex(JsonType.DOUBLE.ordinal());
      out.writeDouble(node.getDoubleValue());
      break;
    case VALUE_STRING:
      out.writeIndex(JsonType.STRING.ordinal());
      out.writeString(node.getTextValue());
      break;
    case VALUE_TRUE:
      out.writeIndex(JsonType.BOOLEAN.ordinal());
      out.writeBoolean(true);
      break;
    case VALUE_FALSE:
      out.writeIndex(JsonType.BOOLEAN.ordinal());
      out.writeBoolean(false);
      break;
    case VALUE_NULL:
      out.writeIndex(JsonType.NULL.ordinal());
      out.writeNull();
      break;
    case START_ARRAY:
      out.writeIndex(JsonType.ARRAY.ordinal());
      out.writeArrayStart();
      out.setItemCount(node.size());
      for (JsonNode element : node) {
        out.startItem();
        write(element, out);
      }
      out.writeArrayEnd();
      break;
    case START_OBJECT:
      out.writeIndex(JsonType.OBJECT.ordinal());
      out.writeMapStart();
      out.setItemCount(node.size());
      Iterator<String> i = node.getFieldNames();
      while (i.hasNext()) {
        out.startItem();
        String name = i.next();
        out.writeString(name);
        write(node.get(name), out);
      }
      out.writeMapEnd();
      break;
    default:
      throw new AvroRuntimeException(node.asToken()+" unexpected: "+node);
    }
  }

  /**
   * Read Json data from Avro data.
   * @deprecated internal method
   */
  @Deprecated
  public static JsonNode read(Decoder in) throws IOException {
    switch (JsonType.values()[in.readIndex()]) {
    case LONG:
      return new LongNode(in.readLong());
    case DOUBLE:
      return new DoubleNode(in.readDouble());
    case STRING:
      return new TextNode(in.readString());
    case BOOLEAN:
      return in.readBoolean() ? BooleanNode.TRUE : BooleanNode.FALSE;
    case NULL:
      in.readNull();
      return NullNode.getInstance();
    case ARRAY:
      ArrayNode array = JsonNodeFactory.instance.arrayNode();
      for (long l = in.readArrayStart(); l > 0; l = in.arrayNext())
        for (long i = 0; i < l; i++)
          array.add(read(in));
      return array;
    case OBJECT:
      ObjectNode object = JsonNodeFactory.instance.objectNode();
      for (long l = in.readMapStart(); l > 0; l = in.mapNext())
        for (long i = 0; i < l; i++)
          object.put(in.readString(), read(in));
      return object;
    default:
      throw new AvroRuntimeException("Unexpected Json node type");
    }
  }

  private static void writeObject(Object datum, Encoder out) throws IOException {
    write(JacksonUtils.toJsonNode(datum), out);
  }

  private static Object readObject(Decoder in) throws IOException {
    return JacksonUtils.toObject(read(in));
  }

}
