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

#include "mozilla/dom/indexedDB/IDBResult.h"

class mozIStorageStatement;
class mozIStorageValueArray;

namespace IPC {

template <typename>
struct ParamTraits;

}  // namespace IPC

namespace mozilla::dom::indexedDB {

class Key {
  friend struct IPC::ParamTraits<Key>;

  nsCString mBuffer;

 public:
  enum {
    eTerminator = 0,
    eFloat = 0x10,
    eDate = 0x20,
    eString = 0x30,
    eBinary = 0x40,
    eArray = 0x50,
    eMaxType = eArray
  };

  static const uint8_t kMaxArrayCollapse = uint8_t(3);
  static const uint8_t kMaxRecursionDepth = uint8_t(64);

  Key() { Unset(); }

  explicit Key(nsCString aBuffer) : mBuffer(std::move(aBuffer)) {}

  bool operator==(const Key& aOther) const {
    MOZ_ASSERT(!mBuffer.IsVoid());
    MOZ_ASSERT(!aOther.mBuffer.IsVoid());

    return mBuffer.Equals(aOther.mBuffer);
  }

  bool operator!=(const Key& aOther) const {
    MOZ_ASSERT(!mBuffer.IsVoid());
    MOZ_ASSERT(!aOther.mBuffer.IsVoid());

    return !mBuffer.Equals(aOther.mBuffer);
  }

  bool operator<(const Key& aOther) const {
    MOZ_ASSERT(!mBuffer.IsVoid());
    MOZ_ASSERT(!aOther.mBuffer.IsVoid());

    return Compare(mBuffer, aOther.mBuffer) < 0;
  }

  bool operator>(const Key& aOther) const {
    MOZ_ASSERT(!mBuffer.IsVoid());
    MOZ_ASSERT(!aOther.mBuffer.IsVoid());

    return Compare(mBuffer, aOther.mBuffer) > 0;
  }

  bool operator<=(const Key& aOther) const {
    MOZ_ASSERT(!mBuffer.IsVoid());
    MOZ_ASSERT(!aOther.mBuffer.IsVoid());

    return Compare(mBuffer, aOther.mBuffer) <= 0;
  }

  bool operator>=(const Key& aOther) const {
    MOZ_ASSERT(!mBuffer.IsVoid());
    MOZ_ASSERT(!aOther.mBuffer.IsVoid());

    return Compare(mBuffer, aOther.mBuffer) >= 0;
  }

  void Unset() { mBuffer.SetIsVoid(true); }

  bool IsUnset() const { return mBuffer.IsVoid(); }

  bool IsFloat() const { return !IsUnset() && *BufferStart() == eFloat; }

  bool IsDate() const { return !IsUnset() && *BufferStart() == eDate; }

  bool IsString() const { return !IsUnset() && *BufferStart() == eString; }

  bool IsBinary() const { return !IsUnset() && *BufferStart() == eBinary; }

  bool IsArray() const { return !IsUnset() && *BufferStart() >= eArray; }

  double ToFloat() const {
    MOZ_ASSERT(IsFloat());
    const EncodedDataType* pos = BufferStart();
    double res = DecodeNumber(pos, BufferEnd());
    MOZ_ASSERT(pos >= BufferEnd());
    return res;
  }

  double ToDateMsec() const {
    MOZ_ASSERT(IsDate());
    const EncodedDataType* pos = BufferStart();
    double res = DecodeNumber(pos, BufferEnd());
    MOZ_ASSERT(pos >= BufferEnd());
    return res;
  }

  nsAutoString ToString() const {
    MOZ_ASSERT(IsString());
    const EncodedDataType* pos = BufferStart();
    auto res = DecodeString(pos, BufferEnd());
    MOZ_ASSERT(pos >= BufferEnd());
    return res;
  }

  Result<Ok, nsresult> SetFromString(const nsAString& aString);

  Result<Ok, nsresult> SetFromInteger(int64_t aInt) {
    mBuffer.Truncate();
    auto ret = EncodeNumber(double(aInt), eFloat);
    TrimBuffer();
    return ret;
  }

  // This function implements the standard algorithm "convert a value to a key".
  // A key return value is indicated by returning `true` whereas `false` means
  // either invalid (if `aRv.Failed()` is `false`) or an exception (otherwise).
  IDBResult<Ok, IDBSpecialValue::Invalid> SetFromJSVal(
      JSContext* aCx, JS::Handle<JS::Value> aVal);

  nsresult ToJSVal(JSContext* aCx, JS::MutableHandle<JS::Value> aVal) const;

  nsresult ToJSVal(JSContext* aCx, JS::Heap<JS::Value>& aVal) const;

  // See SetFromJSVal() for the meaning of values returned by this function.
  IDBResult<Ok, IDBSpecialValue::Invalid> AppendItem(
      JSContext* aCx, bool aFirstOfArray, JS::Handle<JS::Value> aVal);

  Result<Key, nsresult> ToLocaleAwareKey(const nsCString& aLocale) const;

  void FinishArray() { TrimBuffer(); }

  const nsCString& GetBuffer() const { return mBuffer; }

  nsresult BindToStatement(mozIStorageStatement* aStatement,
                           const nsACString& aParamName) const;

  nsresult SetFromStatement(mozIStorageStatement* aStatement, uint32_t aIndex);

  nsresult SetFromValueArray(mozIStorageValueArray* aValues, uint32_t aIndex);

  static int16_t CompareKeys(const Key& aFirst, const Key& aSecond) {
    int32_t result = Compare(aFirst.mBuffer, aSecond.mBuffer);

    if (result < 0) {
      return -1;
    }

    if (result > 0) {
      return 1;
    }

    return 0;
  }

 private:
  class MOZ_STACK_CLASS ArrayValueEncoder;

  using EncodedDataType = unsigned char;

  const EncodedDataType* BufferStart() const {
    // TODO it would be nicer if mBuffer was also using EncodedDataType
    return reinterpret_cast<const EncodedDataType*>(mBuffer.BeginReading());
  }

  const EncodedDataType* BufferEnd() const {
    return reinterpret_cast<const EncodedDataType*>(mBuffer.EndReading());
  }

  // Encoding helper. Trims trailing zeros off of mBuffer as a post-processing
  // step.
  void TrimBuffer() {
    const char* end = mBuffer.EndReading() - 1;
    while (!*end) {
      --end;
    }

    mBuffer.Truncate(end + 1 - mBuffer.BeginReading());
  }

  // Encoding functions. These append the encoded value to the end of mBuffer
  IDBResult<Ok, IDBSpecialValue::Invalid> EncodeJSVal(
      JSContext* aCx, JS::Handle<JS::Value> aVal, uint8_t aTypeOffset);

  Result<Ok, nsresult> EncodeString(const nsAString& aString,
                                    uint8_t aTypeOffset);

  template <typename T>
  Result<Ok, nsresult> EncodeString(Span<const T> aInput, uint8_t aTypeOffset);

  template <typename T>
  Result<Ok, nsresult> EncodeAsString(Span<const T> aInput, uint8_t aType);

  Result<Ok, nsresult> EncodeLocaleString(const nsAString& aString,
                                          uint8_t aTypeOffset,
                                          const nsCString& aLocale);

  Result<Ok, nsresult> EncodeNumber(double aFloat, uint8_t aType);

  Result<Ok, nsresult> EncodeBinary(JSObject* aObject, bool aIsViewObject,
                                    uint8_t aTypeOffset);

  // Decoding functions. aPos points into mBuffer and is adjusted to point
  // past the consumed value. (Note: this may be beyond aEnd).
  static nsresult DecodeJSVal(const EncodedDataType*& aPos,
                              const EncodedDataType* aEnd, JSContext* aCx,
                              JS::MutableHandle<JS::Value> aVal);

  static nsAutoString DecodeString(const EncodedDataType*& aPos,
                                   const EncodedDataType* aEnd);

  static double DecodeNumber(const EncodedDataType*& aPos,
                             const EncodedDataType* aEnd);

  static JSObject* DecodeBinary(const EncodedDataType*& aPos,
                                const EncodedDataType* aEnd, JSContext* aCx);

  // Returns the size of the decoded data for stringy (string or binary),
  // excluding a null terminator.
  // On return, aOutSectionEnd points to the last byte behind the current
  // encoded section, i.e. either aEnd, or the eTerminator.
  // T is the base type for the decoded data.
  template <typename T>
  static uint32_t CalcDecodedStringySize(
      const EncodedDataType* aBegin, const EncodedDataType* aEnd,
      const EncodedDataType** aOutEncodedSectionEnd);

  static uint32_t LengthOfEncodedBinary(const EncodedDataType* aPos,
                                        const EncodedDataType* aEnd);

  template <typename T>
  static void DecodeAsStringy(const EncodedDataType* aEncodedSectionBegin,
                              const EncodedDataType* aEncodedSectionEnd,
                              uint32_t aDecodedLength, T* aOut);

  template <EncodedDataType TypeMask, typename T, typename AcquireBuffer,
            typename AcquireEmpty>
  static void DecodeStringy(const EncodedDataType*& aPos,
                            const EncodedDataType* aEnd,
                            const AcquireBuffer& acquireBuffer,
                            const AcquireEmpty& acquireEmpty);

  IDBResult<Ok, IDBSpecialValue::Invalid> EncodeJSValInternal(
      JSContext* aCx, JS::Handle<JS::Value> aVal, uint8_t aTypeOffset,
      uint16_t aRecursionDepth);

  static nsresult DecodeJSValInternal(const EncodedDataType*& aPos,
                                      const EncodedDataType* aEnd,
                                      JSContext* aCx, uint8_t aTypeOffset,
                                      JS::MutableHandle<JS::Value> aVal,
                                      uint16_t aRecursionDepth);

  template <typename T>
  nsresult SetFromSource(T* aSource, uint32_t aIndex);
};

}  // namespace mozilla::dom::indexedDB

#endif  // mozilla_dom_indexeddb_key_h__