// Copyright (c) 2013, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

part of dart.convert;

/**
 * Error thrown by JSON serialization if an object cannot be serialized.
 *
 * The [unsupportedObject] field holds that object that failed to be serialized.
 *
 * If an object isn't directly serializable, the serializer calls the 'toJson'
 * method on the object. If that call fails, the error will be stored in the
 * [cause] field. If the call returns an object that isn't directly
 * serializable, the [cause] is be null.
 */
class JsonUnsupportedObjectError extends Error {
  /** The object that could not be serialized. */
  final unsupportedObject;
  /** The exception thrown when trying to convert the object. */
  final cause;

  JsonUnsupportedObjectError(this.unsupportedObject, { this.cause });

  String toString() {
    if (cause != null) {
      return "Converting object to an encodable object failed.";
    } else {
      return "Converting object did not return an encodable object.";
    }
  }
}


/**
 * Reports that an object could not be stringified due to cyclic references.
 *
 * An object that references itself cannot be serialized by [stringify].
 * When the cycle is detected, a [JsonCyclicError] is thrown.
 */
class JsonCyclicError extends JsonUnsupportedObjectError {
  /** The first object that was detected as part of a cycle. */
  JsonCyclicError(Object object): super(object);
  String toString() => "Cyclic error in JSON stringify";
}


/**
 * An instance of the default implementation of the [JsonCodec].
 *
 * This instance provides a convenient access to the most common JSON
 * use cases.
 *
 * Examples:
 *
 *     var encoded = JSON.encode([1, 2, { "a": null }]);
 *     var decoded = JSON.decode('["foo", { "bar": 499 }]');
 */
const JsonCodec JSON = const JsonCodec();

typedef _Reviver(var key, var value);
typedef _ToEncodable(var o);


/**
 * A [JsonCodec] encodes JSON objects to strings and decodes strings to
 * JSON objects.
 */
class JsonCodec extends Codec<Object, String> {
  final _Reviver _reviver;
  final _ToEncodable _toEncodable;

  /**
   * Creates a `JsonCodec` with the given reviver and encoding function.
   *
   * The [reviver] function is called during decoding. It is invoked
   * once for each object or list property that has been parsed.
   * The `key` argument is either the
   * integer list index for a list property, the string map key for object
   * properties, or `null` for the final result.
   *
   * If [reviver] is omitted, it defaults to returning the value argument.
   *
   * The [toEncodable] function is used during encoding. It is invoked for
   * values that are not directly encodable to a JSON1toE
   * string (a value that is not a number, boolean, string, null, list or a map
   * with string keys). The function must return an object that is directly
   * encodable. The elements of a returned list and values of a returned map
   * do not need be directly encodable, and if they aren't, `toEncodable` will
   * be used on them as well.
   * Please notice that it is possible to cause an infinite recursive
   * regress in this way, by effectively creating an infinite data structure
   * through repeated call to `toEncodable`.
   *
   * If [toEncodable] is omitted, it defaults to a function that returns the
   * result of calling `.toJson()` on the unencodable object.
   */
  const JsonCodec({reviver(var key, var value), toEncodable(var object)})
      : _reviver = reviver,
        _toEncodable = toEncodable;

  /**
   * Creates a `JsonCodec` with the given reviver.
   *
   * The [reviver] function is called once for each object or list property
   * that has been parsed during decoding. The `key` argument is either the
   * integer list index for a list property, the string map key for object
   * properties, or `null` for the final result.
   */
  JsonCodec.withReviver(reviver(var key, var value)) : this(reviver: reviver);

  /**
   * Parses the string and returns the resulting Json object.
   *
   * The optional [reviver] function is called once for each object or list
   * property that has been parsed during decoding. The `key` argument is either
   * the integer list index for a list property, the string map key for object
   * properties, or `null` for the final result.
   *
   * The default [reviver] (when not provided) is the identity function.
   */
  dynamic decode(String source, {reviver(var key, var value)}) {
    if (reviver == null) reviver = _reviver;
    if (reviver == null) return decoder.convert(source);
    return new JsonDecoder(reviver).convert(source);
  }

  /**
   * Converts [value] to a JSON string.
   *
   * If value contains objects that are not directly encodable to a JSON
   * string (a value that is not a number, boolean, string, null, list or a map
   * with string keys), the [toEncodable] function is used to convert it to an
   * object that must be directly encodable.
   *
   * If [toEncodable] is omitted, it defaults to a function that returns the
   * result of calling `.toJson()` on the unencodable object.
   */
  String encode(Object value, {toEncodable(var object)}) {
    if (toEncodable == null) toEncodable = _toEncodable;
    if (toEncodable == null) return encoder.convert(value);
    return new JsonEncoder(toEncodable).convert(value);
  }

  JsonEncoder get encoder {
    if (_toEncodable == null) return const JsonEncoder();
    return new JsonEncoder(_toEncodable);
  }

  JsonDecoder get decoder {
    if (_reviver == null) return const JsonDecoder();
    return new JsonDecoder(_reviver);
  }
}

/**
 * This class converts JSON objects to strings.
 */
class JsonEncoder extends Converter<Object, String> {
  /**
   * The string used for indention.
   *
   * When generating multi-line output, this string is inserted once at the
   * beginning of each indented line for each level of indentation.
   *
   * If `null`, the output is encoded as a single line.
   */
  final String indent;

  final _toEncodableFunction;

  /**
   * Creates a JSON encoder.
   *
   * The JSON encoder handles numbers, strings, booleans, null, lists and
   * maps directly.
   *
   * Any other object is attempted converted by [toEncodable] to an
   * object that is of one of the convertible types.
   *
   * If [toEncodable] is omitted, it defaults to calling `.toJson()` on
   * the object.
   */
  const JsonEncoder([Object toEncodable(Object nonSerializable)])
      : this.indent = null,
        this._toEncodableFunction = toEncodable;

  /**
   * Creates a JSON encoder that creates multi-line JSON.
   *
   * The encoding of elements of lists and maps are indented and put on separate
   * lines. The [indent] string is prepended to these elements, once for each
   * level of indentation.
   *
   * If [indent] is `null`, the output is encoded as a single line.
   *
   * The JSON encoder handles numbers, strings, booleans, null, lists and
   * maps directly.
   *
   * Any other object is attempted converted by [toEncodable] to an
   * object that is of one of the convertible types.
   *
   * If [toEncodable] is omitted, it defaults to calling `.toJson()` on
   * the object.
   */
  const JsonEncoder.withIndent(this.indent,
      [Object toEncodable(Object nonSerializable)])
      : this._toEncodableFunction = toEncodable;

  /**
   * Converts [object] to a JSON [String].
   *
   * Directly serializable values are [num], [String], [bool], and [Null], as
   * well as some [List] and [Map] values.
   * For [List], the elements must all be serializable.
   * For [Map], the keys must be [String] and the values must be serializable.
   *
   * If a value is any other type is attempted serialized, the conversion
   * function provided in the constructor is invoked with the object as argument
   * and the result, which must be a directly serializable value,
   * is serialized instead of the original value.
   *
   * If the conversion throws, or returns a value that is not directly
   * serializable, a [JsonUnsupportedObjectError] exception is thrown.
   * If the call throws, the error is caught and stored in the
   * [JsonUnsupportedObjectError]'s [:cause:] field.
   *
   * If a [List] or [Map] contains a reference to itself, directly or through
   * other lists or maps, it cannot be serialized and a [JsonCyclicError] is
   * thrown.
   *
   * [object] should not change during serialization.
   *
   * If an object is serialized more than once, [convert] may cache the text
   * for it. In other words, if the content of an object changes after it is
   * first serialized, the new values may not be reflected in the result.
   */
  String convert(Object object) =>
      _JsonStringifier.stringify(object, _toEncodableFunction, indent);

  /**
   * Starts a chunked conversion.
   *
   * The converter works more efficiently if the given [sink] is a
   * [StringConversionSink].
   *
   * Returns a chunked-conversion sink that accepts at most one object. It is
   * an error to invoke `add` more than once on the returned sink.
   */
  ChunkedConversionSink<Object> startChunkedConversion(Sink<String> sink) {
    if (sink is! StringConversionSink) {
      sink = new StringConversionSink.from(sink);
    }
    return new _JsonEncoderSink(sink, _toEncodableFunction, indent);
  }

  // Override the base-classes bind, to provide a better type.
  Stream<String> bind(Stream<Object> stream) => super.bind(stream);
}

/**
 * Implements the chunked conversion from object to its JSON representation.
 *
 * The sink only accepts one value, but will produce output in a chunked way.
 */
class _JsonEncoderSink extends ChunkedConversionSink<Object> {
  final String _indent;
  final Function _toEncodableFunction;
  final StringConversionSink _sink;
  bool _isDone = false;

  _JsonEncoderSink(this._sink, this._toEncodableFunction, this._indent);

  /**
   * Encodes the given object [o].
   *
   * It is an error to invoke this method more than once on any instance. While
   * this makes the input effectly non-chunked the output will be generated in
   * a chunked way.
   */
  void add(Object o) {
    if (_isDone) {
      throw new StateError("Only one call to add allowed");
    }
    _isDone = true;
    ClosableStringSink stringSink = _sink.asStringSink();
    _JsonStringifier.printOn(o, stringSink, _toEncodableFunction, _indent);
    stringSink.close();
  }

  void close() { /* do nothing */ }
}

/**
 * This class parses JSON strings and builds the corresponding objects.
 */
class JsonDecoder extends Converter<String, Object> {
  final _Reviver _reviver;
  /**
   * Constructs a new JsonDecoder.
   *
   * The [reviver] may be `null`.
   */
  const JsonDecoder([reviver(var key, var value)]) : this._reviver = reviver;

  /**
   * Converts the given JSON-string [input] to its corresponding object.
   *
   * Parsed JSON values are of the types [num], [String], [bool], [Null],
   * [List]s of parsed JSON values or [Map]s from [String] to parsed
   * JSON values.
   *
   * If `this` was initialized with a reviver, then the parsing operation
   * invokes the reviver on every object or list property that has been parsed.
   * The arguments are the property name ([String]) or list index ([int]), and
   * the value is the parsed value. The return value of the reviver is used as
   * the value of that property instead the parsed value.
   *
   * Throws [FormatException] if the input is not valid JSON text.
   */
  dynamic convert(String input) => _parseJson(input, _reviver);

  /**
   * Starts a conversion from a chunked JSON string to its corresponding
   * object.
   *
   * The output [sink] receives exactly one decoded element through `add`.
   */
  StringConversionSink startChunkedConversion(Sink<Object> sink) {
    return new _JsonDecoderSink(_reviver, sink);
  }

  // Override the base-classes bind, to provide a better type.
  Stream<Object> bind(Stream<String> stream) => super.bind(stream);
}

/**
 * Implements the chunked conversion from a JSON string to its corresponding
 * object.
 *
 * The sink only creates one object, but its input can be chunked.
 */
// TODO(floitsch): don't accumulate everything before starting to decode.
class _JsonDecoderSink extends _StringSinkConversionSink {
  final _Reviver _reviver;
  final Sink<Object> _sink;

  _JsonDecoderSink(this._reviver, this._sink)
      : super(new StringBuffer());

  void close() {
    super.close();
    StringBuffer buffer = _stringSink;
    String accumulated = buffer.toString();
    buffer.clear();
    Object decoded = _parseJson(accumulated, _reviver);
    _sink.add(decoded);
    _sink.close();
  }
}

// Internal optimized JSON parsing implementation.
external _parseJson(String source, reviver(key, value));


// Implementation of encoder/stringifier.

Object _defaultToEncodable(object) => object.toJson();

class _JsonStringifier {
  // Character code constants.
  static const int BACKSPACE       = 0x08;
  static const int TAB             = 0x09;
  static const int NEWLINE         = 0x0a;
  static const int CARRIAGE_RETURN = 0x0d;
  static const int FORM_FEED       = 0x0c;
  static const int QUOTE           = 0x22;
  static const int CHAR_0          = 0x30;
  static const int BACKSLASH       = 0x5c;
  static const int CHAR_b          = 0x62;
  static const int CHAR_f          = 0x66;
  static const int CHAR_n          = 0x6e;
  static const int CHAR_r          = 0x72;
  static const int CHAR_t          = 0x74;
  static const int CHAR_u          = 0x75;

  final Function _toEncodable;
  final StringSink _sink;
  final List _seen;

  factory _JsonStringifier(StringSink sink, Function toEncodable,
      String indent) {
    if (indent == null) return new _JsonStringifier._(sink, toEncodable);
    return new _JsonStringifierPretty(sink, toEncodable, indent);
  }

  _JsonStringifier._(this._sink, this._toEncodable)
      : this._seen = new List();

  static String stringify(object, toEncodable(object), String indent) {
    if (toEncodable == null) toEncodable = _defaultToEncodable;
    StringBuffer output = new StringBuffer();
    printOn(object, output, toEncodable, indent);
    return output.toString();
  }

  static void printOn(object, StringSink output, toEncodable(object),
      String indent) {
    new _JsonStringifier(output, toEncodable, indent).stringifyValue(object);
  }

  static String numberToString(num x) {
    return x.toString();
  }

  // ('0' + x) or ('a' + x - 10)
  static int hexDigit(int x) => x < 10 ? 48 + x : 87 + x;

  void escape(String s) {
    int offset = 0;
    final int length = s.length;
    for (int i = 0; i < length; i++) {
      int charCode = s.codeUnitAt(i);
      if (charCode > BACKSLASH) continue;
      if (charCode < 32) {
        if (i > offset) _sink.write(s.substring(offset, i));
        offset = i + 1;
        _sink.writeCharCode(BACKSLASH);
        switch (charCode) {
        case BACKSPACE:
          _sink.writeCharCode(CHAR_b);
          break;
        case TAB:
          _sink.writeCharCode(CHAR_t);
          break;
        case NEWLINE:
          _sink.writeCharCode(CHAR_n);
          break;
        case FORM_FEED:
          _sink.writeCharCode(CHAR_f);
          break;
        case CARRIAGE_RETURN:
          _sink.writeCharCode(CHAR_r);
          break;
        default:
          _sink.writeCharCode(CHAR_u);
          _sink.writeCharCode(CHAR_0);
          _sink.writeCharCode(CHAR_0);
          _sink.writeCharCode(hexDigit((charCode >> 4) & 0xf));
          _sink.writeCharCode(hexDigit(charCode & 0xf));
          break;
        }
      } else if (charCode == QUOTE || charCode == BACKSLASH) {
        if (i > offset) _sink.write(s.substring(offset, i));
        offset = i + 1;
        _sink.writeCharCode(BACKSLASH);
        _sink.writeCharCode(charCode);
      }
    }
    if (offset == 0) {
      _sink.write(s);
    } else if (offset < length) {
      _sink.write(s.substring(offset, length));
    }
  }

  void checkCycle(object) {
    for (int i = 0; i < _seen.length; i++) {
      if (identical(object, _seen[i])) {
        throw new JsonCyclicError(object);
      }
    }
    _seen.add(object);
  }

  void stringifyValue(object) {
    // Tries stringifying object directly. If it's not a simple value, List or
    // Map, call toJson() to get a custom representation and try serializing
    // that.
    if (!stringifyJsonValue(object)) {
      checkCycle(object);
      try {
        var customJson = _toEncodable(object);
        if (!stringifyJsonValue(customJson)) {
          throw new JsonUnsupportedObjectError(object);
        }
        _removeSeen(object);
      } catch (e) {
        throw new JsonUnsupportedObjectError(object, cause: e);
      }
    }
  }

  /**
   * Serializes a [num], [String], [bool], [Null], [List] or [Map] value.
   *
   * Returns true if the value is one of these types, and false if not.
   * If a value is both a [List] and a [Map], it's serialized as a [List].
   */
  bool stringifyJsonValue(object) {
    if (object is num) {
      if (!object.isFinite) return false;
      _sink.write(numberToString(object));
      return true;
    } else if (identical(object, true)) {
      _sink.write('true');
      return true;
    } else if (identical(object, false)) {
      _sink.write('false');
       return true;
    } else if (object == null) {
      _sink.write('null');
      return true;
    } else if (object is String) {
      _sink.write('"');
      escape(object);
      _sink.write('"');
      return true;
    } else if (object is List) {
      checkCycle(object);
      List a = object;
      _sink.write('[');
      if (a.length > 0) {
        stringifyValue(a[0]);
        for (int i = 1; i < a.length; i++) {
          _sink.write(',');
          stringifyValue(a[i]);
        }
      }
      _sink.write(']');
      _removeSeen(object);
      return true;
    } else if (object is Map) {
      checkCycle(object);
      Map<String, Object> m = object;
      _sink.write('{');
      String separator = '"';
      for (String key in m.keys) {
        _sink.write(separator);
        separator = ',"';
        escape(key);
        _sink.write('":');
        stringifyValue(m[key]);
      }
      _sink.write('}');
      _removeSeen(object);
      return true;
    } else {
      return false;
    }
  }

  void _removeSeen(object) {
    assert(!_seen.isEmpty);
    assert(identical(_seen.last, object));
    _seen.removeLast();
  }
}

/**
 * A subclass of [_JsonStringifier] which indents the contents of [List] and
 * [Map] objects using the specified indent value.
 */
class _JsonStringifierPretty extends _JsonStringifier {
  final String _indent;

  int _indentLevel = 0;

  _JsonStringifierPretty(_sink, _toEncodable, this._indent)
      : super._(_sink, _toEncodable);

  void _write([String value = '']) {
    _sink.write(_indent * _indentLevel);
    _sink.write(value);
  }

  /**
   * Serializes a [num], [String], [bool], [Null], [List] or [Map] value.
   *
   * Returns true if the value is one of these types, and false if not.
   * If a value is both a [List] and a [Map], it's serialized as a [List].
   */
  bool stringifyJsonValue(final object) {
    if (object is List) {
      checkCycle(object);
      List a = object;
      if (a.isEmpty) {
        _sink.write('[]');
      } else {
        _sink.writeln('[');
        _indentLevel++;
        _write();
        stringifyValue(a[0]);
        for (int i = 1; i < a.length; i++) {
          _sink.writeln(',');
          _write();
          stringifyValue(a[i]);
        }
        _sink.writeln();
        _indentLevel--;
        _write(']');
      }
      _seen.remove(object);
      return true;
    } else if (object is Map) {
      checkCycle(object);
      Map<String, Object> m = object;
      if (m.isEmpty) {
        _sink.write('{}');
      } else {
        _sink.write('{');
        _sink.writeln();
        _indentLevel++;
        bool first = true;
        m.forEach((String key, Object value) {
          if (!first) {
            _sink.writeln(',');
          }
          _write('"');
          escape(key);
          _sink.write('": ');
          stringifyValue(value);
          first = false;
        });
        _sink.writeln();
        _indentLevel--;
        _write('}');
      }
      _seen.remove(object);
      return true;
    }
    return super.stringifyJsonValue(object);
  }
}
