/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 * vim: set ts=8 sts=4 et sw=4 tw=99:
 * 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/. */

#ifndef util_StringBuffer_h
#define util_StringBuffer_h

#include "mozilla/DebugOnly.h"
#include "mozilla/MaybeOneOf.h"

#include "js/Vector.h"
#include "vm/JSContext.h"

namespace js {

/*
 * String builder that eagerly checks for over-allocation past the maximum
 * string length.
 *
 * Any operation which would exceed the maximum string length causes an
 * exception report on the context and results in a failed return value.
 *
 * Well-sized extractions (which waste no more than 1/4 of their char
 * buffer space) are guaranteed for strings built by this interface.
 * See |extractWellSized|.
 */
class StringBuffer {
  /*
   * The Vector's buffer may be either stolen or copied, so we need to use
   * TempAllocPolicy and account for the memory manually when stealing.
   */
  typedef Vector<Latin1Char, 64> Latin1CharBuffer;
  typedef Vector<char16_t, 32> TwoByteCharBuffer;

  JSContext* cx;

  /*
   * If Latin1 strings are enabled, cb starts out as a Latin1CharBuffer. When
   * a TwoByte char is appended, inflateChars() constructs a TwoByteCharBuffer
   * and copies the Latin1 chars.
   */
  mozilla::MaybeOneOf<Latin1CharBuffer, TwoByteCharBuffer> cb;

#ifdef DEBUG
  /*
   * Make sure ensureTwoByteChars() is called before calling
   * infallibleAppend(char16_t).
   */
  bool hasEnsuredTwoByteChars_;
#endif

  /* Number of reserve()'d chars, see inflateChars. */
  size_t reserved_;

  StringBuffer(const StringBuffer& other) = delete;
  void operator=(const StringBuffer& other) = delete;

  MOZ_ALWAYS_INLINE bool isLatin1() const {
    return cb.constructed<Latin1CharBuffer>();
  }
  MOZ_ALWAYS_INLINE bool isTwoByte() const { return !isLatin1(); }

  MOZ_ALWAYS_INLINE TwoByteCharBuffer& twoByteChars() {
    return cb.ref<TwoByteCharBuffer>();
  }

  MOZ_ALWAYS_INLINE const TwoByteCharBuffer& twoByteChars() const {
    return cb.ref<TwoByteCharBuffer>();
  }

  MOZ_MUST_USE bool inflateChars();

 public:
  explicit StringBuffer(JSContext* cx)
      : cx(cx)
#ifdef DEBUG
        ,
        hasEnsuredTwoByteChars_(false)
#endif
        ,
        reserved_(0) {
    cb.construct<Latin1CharBuffer>(cx);
  }

  MOZ_ALWAYS_INLINE Latin1CharBuffer& latin1Chars() {
    return cb.ref<Latin1CharBuffer>();
  }

  MOZ_ALWAYS_INLINE const Latin1CharBuffer& latin1Chars() const {
    return cb.ref<Latin1CharBuffer>();
  }

  void clear() {
    if (isLatin1())
      latin1Chars().clear();
    else
      twoByteChars().clear();
  }
  MOZ_MUST_USE bool reserve(size_t len) {
    if (len > reserved_) reserved_ = len;
    return isLatin1() ? latin1Chars().reserve(len)
                      : twoByteChars().reserve(len);
  }
  MOZ_MUST_USE bool resize(size_t len) {
    return isLatin1() ? latin1Chars().resize(len) : twoByteChars().resize(len);
  }
  bool empty() const {
    return isLatin1() ? latin1Chars().empty() : twoByteChars().empty();
  }
  size_t length() const {
    return isLatin1() ? latin1Chars().length() : twoByteChars().length();
  }
  char16_t getChar(size_t idx) const {
    return isLatin1() ? latin1Chars()[idx] : twoByteChars()[idx];
  }

  MOZ_MUST_USE bool ensureTwoByteChars() {
    if (isLatin1() && !inflateChars()) return false;

#ifdef DEBUG
    hasEnsuredTwoByteChars_ = true;
#endif
    return true;
  }

  MOZ_MUST_USE bool append(const char16_t c) {
    if (isLatin1()) {
      if (c <= JSString::MAX_LATIN1_CHAR)
        return latin1Chars().append(Latin1Char(c));
      if (!inflateChars()) return false;
    }
    return twoByteChars().append(c);
  }
  MOZ_MUST_USE bool append(Latin1Char c) {
    return isLatin1() ? latin1Chars().append(c) : twoByteChars().append(c);
  }
  MOZ_MUST_USE bool append(char c) { return append(Latin1Char(c)); }

  TwoByteCharBuffer& rawTwoByteBuffer() {
    MOZ_ASSERT(hasEnsuredTwoByteChars_);
    return twoByteChars();
  }

  inline MOZ_MUST_USE bool append(const char16_t* begin, const char16_t* end);

  MOZ_MUST_USE bool append(const char16_t* chars, size_t len) {
    return append(chars, chars + len);
  }

  MOZ_MUST_USE bool append(const Latin1Char* begin, const Latin1Char* end) {
    return isLatin1() ? latin1Chars().append(begin, end)
                      : twoByteChars().append(begin, end);
  }
  MOZ_MUST_USE bool append(const Latin1Char* chars, size_t len) {
    return append(chars, chars + len);
  }

  MOZ_MUST_USE bool append(const JS::ConstCharPtr chars, size_t len) {
    return append(chars.get(), chars.get() + len);
  }
  MOZ_MUST_USE bool appendN(Latin1Char c, size_t n) {
    return isLatin1() ? latin1Chars().appendN(c, n)
                      : twoByteChars().appendN(c, n);
  }

  inline MOZ_MUST_USE bool append(JSString* str);
  inline MOZ_MUST_USE bool append(JSLinearString* str);
  inline MOZ_MUST_USE bool appendSubstring(JSString* base, size_t off,
                                           size_t len);
  inline MOZ_MUST_USE bool appendSubstring(JSLinearString* base, size_t off,
                                           size_t len);

  MOZ_MUST_USE bool append(const char* chars, size_t len) {
    return append(reinterpret_cast<const Latin1Char*>(chars), len);
  }

  template <size_t ArrayLength>
  MOZ_MUST_USE bool append(const char (&array)[ArrayLength]) {
    return append(array, ArrayLength - 1); /* No trailing '\0'. */
  }

  /* Infallible variants usable when the corresponding space is reserved. */
  void infallibleAppend(Latin1Char c) {
    if (isLatin1())
      latin1Chars().infallibleAppend(c);
    else
      twoByteChars().infallibleAppend(c);
  }
  void infallibleAppend(char c) { infallibleAppend(Latin1Char(c)); }
  void infallibleAppend(const Latin1Char* chars, size_t len) {
    if (isLatin1())
      latin1Chars().infallibleAppend(chars, len);
    else
      twoByteChars().infallibleAppend(chars, len);
  }
  void infallibleAppend(const char* chars, size_t len) {
    infallibleAppend(reinterpret_cast<const Latin1Char*>(chars), len);
  }

  void infallibleAppendSubstring(JSLinearString* base, size_t off, size_t len);

  /*
   * Because inflation is fallible, these methods should only be used after
   * calling ensureTwoByteChars().
   */
  void infallibleAppend(const char16_t* chars, size_t len) {
    MOZ_ASSERT(hasEnsuredTwoByteChars_);
    twoByteChars().infallibleAppend(chars, len);
  }
  void infallibleAppend(char16_t c) {
    MOZ_ASSERT(hasEnsuredTwoByteChars_);
    twoByteChars().infallibleAppend(c);
  }

  bool isUnderlyingBufferLatin1() const { return isLatin1(); }

  char16_t* rawTwoByteBegin() { return twoByteChars().begin(); }
  char16_t* rawTwoByteEnd() { return twoByteChars().end(); }
  const char16_t* rawTwoByteBegin() const { return twoByteChars().begin(); }
  const char16_t* rawTwoByteEnd() const { return twoByteChars().end(); }

  Latin1Char* rawLatin1Begin() { return latin1Chars().begin(); }
  Latin1Char* rawLatin1End() { return latin1Chars().end(); }
  const Latin1Char* rawLatin1Begin() const { return latin1Chars().begin(); }
  const Latin1Char* rawLatin1End() const { return latin1Chars().end(); }

  /*
   * Creates a string from the characters in this buffer, then (regardless
   * whether string creation succeeded or failed) empties the buffer.
   */
  JSFlatString* finishString();

  /* Identical to finishString() except that an atom is created. */
  JSAtom* finishAtom();

  /*
   * Creates a raw string from the characters in this buffer.  The string is
   * exactly the characters in this buffer (inflated to TwoByte), it is *not*
   * null-terminated unless the last appended character was '\0'.
   */
  char16_t* stealChars();
};

inline bool StringBuffer::append(const char16_t* begin, const char16_t* end) {
  MOZ_ASSERT(begin <= end);
  if (isLatin1()) {
    while (true) {
      if (begin >= end) return true;
      if (*begin > JSString::MAX_LATIN1_CHAR) break;
      if (!latin1Chars().append(*begin)) return false;
      ++begin;
    }
    if (!inflateChars()) return false;
  }
  return twoByteChars().append(begin, end);
}

inline bool StringBuffer::append(JSLinearString* str) {
  JS::AutoCheckCannotGC nogc;
  if (isLatin1()) {
    if (str->hasLatin1Chars())
      return latin1Chars().append(str->latin1Chars(nogc), str->length());
    if (!inflateChars()) return false;
  }
  return str->hasLatin1Chars()
             ? twoByteChars().append(str->latin1Chars(nogc), str->length())
             : twoByteChars().append(str->twoByteChars(nogc), str->length());
}

inline void StringBuffer::infallibleAppendSubstring(JSLinearString* base,
                                                    size_t off, size_t len) {
  MOZ_ASSERT(off + len <= base->length());
  MOZ_ASSERT_IF(base->hasTwoByteChars(), isTwoByte());

  JS::AutoCheckCannotGC nogc;
  if (base->hasLatin1Chars())
    infallibleAppend(base->latin1Chars(nogc) + off, len);
  else
    infallibleAppend(base->twoByteChars(nogc) + off, len);
}

inline bool StringBuffer::appendSubstring(JSLinearString* base, size_t off,
                                          size_t len) {
  MOZ_ASSERT(off + len <= base->length());

  JS::AutoCheckCannotGC nogc;
  if (isLatin1()) {
    if (base->hasLatin1Chars())
      return latin1Chars().append(base->latin1Chars(nogc) + off, len);
    if (!inflateChars()) return false;
  }
  return base->hasLatin1Chars()
             ? twoByteChars().append(base->latin1Chars(nogc) + off, len)
             : twoByteChars().append(base->twoByteChars(nogc) + off, len);
}

inline bool StringBuffer::appendSubstring(JSString* base, size_t off,
                                          size_t len) {
  JSLinearString* linear = base->ensureLinear(cx);
  if (!linear) return false;

  return appendSubstring(linear, off, len);
}

inline bool StringBuffer::append(JSString* str) {
  JSLinearString* linear = str->ensureLinear(cx);
  if (!linear) return false;

  return append(linear);
}

/* ES5 9.8 ToString, appending the result to the string buffer. */
extern bool ValueToStringBufferSlow(JSContext* cx, const Value& v,
                                    StringBuffer& sb);

inline bool ValueToStringBuffer(JSContext* cx, const Value& v,
                                StringBuffer& sb) {
  if (v.isString()) return sb.append(v.toString());

  return ValueToStringBufferSlow(cx, v, sb);
}

/* ES5 9.8 ToString for booleans, appending the result to the string buffer. */
inline bool BooleanToStringBuffer(bool b, StringBuffer& sb) {
  return b ? sb.append("true") : sb.append("false");
}

} /* namespace js */

#endif /* util_StringBuffer_h */
