/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "JSStreamWriter.h"

#include "mozilla/ArrayUtils.h" // for ArrayLength
#include "nsDataHashtable.h"
#include "nsString.h"
#include "nsTArray.h"
#include "nsUTF8Utils.h"

#if _MSC_VER
 #define snprintf _snprintf
#endif

#define ARRAY (void*)1
#define OBJECT (void*)2

// Escape a UTF8 string to a stream. When an illegal encoding
// is found it will insert "INVALID" and the function will return.
static void EscapeToStream(std::ostream& stream, const char* str) {
  stream << "\"";

  size_t len = strlen(str);
  const char* end = &str[len];
  while (str < end) {
    bool err;
    const char* utf8CharStart = str;
    uint32_t ucs4Char = UTF8CharEnumerator::NextChar(&str, end, &err);

    if (err) {
      // Encoding error
      stream << "INVALID\"";
      return;
    }

    // See http://www.ietf.org/rfc/rfc4627.txt?number=4627
    // characters that must be escaped: quotation mark,
    // reverse solidus, and the control characters
    // (U+0000 through U+001F).
    if (ucs4Char == '\"') {
      stream << "\\\"";
    } else if (ucs4Char == '\\') {
      stream << "\\\\";
    } else if (ucs4Char > 0xFF) {
      char16_t chr[2];
      ConvertUTF8toUTF16 encoder(chr);
      encoder.write(utf8CharStart, uint32_t(str-utf8CharStart));
      char escChar[13];
      snprintf(escChar, mozilla::ArrayLength(escChar), "\\u%04X\\u%04X", chr[0], chr[1]);
      stream << escChar;
    } else if (ucs4Char < 0x1F || ucs4Char > 0xFF) {
      char escChar[7];
      snprintf(escChar, mozilla::ArrayLength(escChar), "\\u%04X", ucs4Char);
      stream << escChar;
    } else {
      stream << char(ucs4Char);
    }
  }
  stream << "\"";
}

JSStreamWriter::JSStreamWriter(std::ostream& aStream)
  : mStream(aStream)
  , mNeedsComma(false)
  , mNeedsName(false)
{ }

JSStreamWriter::~JSStreamWriter()
{
  MOZ_ASSERT(mStack.GetSize() == 0);
}

void
JSStreamWriter::BeginObject()
{
  MOZ_ASSERT(!mNeedsName);
  if (mNeedsComma && mStack.Peek() == ARRAY) {
    mStream << ",";
  }
  mStream << "{";
  mNeedsComma = false;
  mNeedsName = true;
  mStack.Push(OBJECT);
}

void
JSStreamWriter::EndObject()
{
  MOZ_ASSERT(mStack.Peek() == OBJECT);
  mStream << "}";
  mNeedsComma = true;
  mNeedsName = false;
  mStack.Pop();
  if (mStack.GetSize() > 0 && mStack.Peek() == OBJECT) {
    mNeedsName = true;
  }
}

void
JSStreamWriter::BeginArray()
{
  MOZ_ASSERT(!mNeedsName);
  if (mNeedsComma && mStack.Peek() == ARRAY) {
    mStream << ",";
  }
  mStream << "[";
  mNeedsComma = false;
  mStack.Push(ARRAY);
}

void
JSStreamWriter::EndArray()
{
  MOZ_ASSERT(!mNeedsName);
  MOZ_ASSERT(mStack.Peek() == ARRAY);
  mStream << "]";
  mNeedsComma = true;
  mStack.Pop();
  if (mStack.GetSize() > 0 && mStack.Peek() == OBJECT) {
    mNeedsName = true;
  }
}

void
JSStreamWriter::Name(const char *aName)
{
  MOZ_ASSERT(mNeedsName);
  if (mNeedsComma && mStack.Peek() == OBJECT) {
    mStream << ",";
  }
  EscapeToStream(mStream, aName);
  mStream << ":";
  mNeedsName = false;
}

void
JSStreamWriter::Value(int aValue)
{
  MOZ_ASSERT(!mNeedsName);
  if (mNeedsComma && mStack.Peek() == ARRAY) {
    mStream << ",";
  }
  mStream << aValue;
  mNeedsComma = true;
  if (mStack.Peek() == OBJECT) {
    mNeedsName = true;
  }
}

void
JSStreamWriter::Value(double aValue)
{
  MOZ_ASSERT(!mNeedsName);
  if (mNeedsComma && mStack.Peek() == ARRAY) {
    mStream << ",";
  }
  mStream.precision(18);
  mStream << aValue;
  mNeedsComma = true;
  if (mStack.Peek() == OBJECT) {
    mNeedsName = true;
  }
}

void
JSStreamWriter::Value(const char *aValue)
{
  MOZ_ASSERT(!mNeedsName);
  if (mNeedsComma && mStack.Peek() == ARRAY) {
    mStream << ",";
  }
  EscapeToStream(mStream, aValue);
  mNeedsComma = true;
  if (mStack.Peek() == OBJECT) {
    mNeedsName = true;
  }
}
