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

#include "KeyPath.h"
#include "IDBObjectStore.h"
#include "Key.h"

#include "nsCharSeparatedTokenizer.h"
#include "nsJSUtils.h"
#include "xpcpublic.h"

USING_INDEXEDDB_NAMESPACE

namespace {

inline
bool
IgnoreWhitespace(PRUnichar c)
{
  return false;
}

typedef nsCharSeparatedTokenizerTemplate<IgnoreWhitespace> KeyPathTokenizer;

bool
IsValidKeyPathString(JSContext* aCx, const nsAString& aKeyPath)
{
  NS_ASSERTION(!aKeyPath.IsVoid(), "What?");

  KeyPathTokenizer tokenizer(aKeyPath, '.');

  while (tokenizer.hasMoreTokens()) {
    nsString token(tokenizer.nextToken());

    if (!token.Length()) {
      return false;
    }

    jsval stringVal;
    if (!xpc::StringToJsval(aCx, token, &stringVal)) {
      return false;
    }

    NS_ASSERTION(JSVAL_IS_STRING(stringVal), "This should never happen");
    JSString* str = JSVAL_TO_STRING(stringVal);

    JSBool isIdentifier = JS_FALSE;
    if (!JS_IsIdentifier(aCx, str, &isIdentifier) || !isIdentifier) {
      return false;
    }
  }

  // If the very last character was a '.', the tokenizer won't give us an empty
  // token, but the keyPath is still invalid.
  if (!aKeyPath.IsEmpty() &&
      aKeyPath.CharAt(aKeyPath.Length() - 1) == '.') {
    return false;
  }

  return true;
}

enum KeyExtractionOptions {
  DoNotCreateProperties,
  CreateProperties
};

nsresult
GetJSValFromKeyPathString(JSContext* aCx,
                          const JS::Value& aValue,
                          const nsAString& aKeyPathString,
                          JS::Value* aKeyJSVal,
                          KeyExtractionOptions aOptions,
                          KeyPath::ExtractOrCreateKeyCallback aCallback,
                          void* aClosure)
{
  NS_ASSERTION(aCx, "Null pointer!");
  NS_ASSERTION(IsValidKeyPathString(aCx, aKeyPathString),
               "This will explode!");
  NS_ASSERTION(!(aCallback || aClosure) || aOptions == CreateProperties,
               "This is not allowed!");
  NS_ASSERTION(aOptions != CreateProperties || aCallback,
               "If properties are created, there must be a callback!");

  nsresult rv = NS_OK;
  *aKeyJSVal = aValue;

  KeyPathTokenizer tokenizer(aKeyPathString, '.');

  nsString targetObjectPropName;
  JS::Rooted<JSObject*> targetObject(aCx, nullptr);
  JS::Rooted<JSObject*> obj(aCx,
    JSVAL_IS_PRIMITIVE(aValue) ? nullptr : JSVAL_TO_OBJECT(aValue));

  while (tokenizer.hasMoreTokens()) {
    const nsDependentSubstring& token = tokenizer.nextToken();

    NS_ASSERTION(!token.IsEmpty(), "Should be a valid keypath");

    const jschar* keyPathChars = token.BeginReading();
    const size_t keyPathLen = token.Length();

    JSBool hasProp;
    if (!targetObject) {
      // We're still walking the chain of existing objects
      if (!obj) {
        return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
      }

      JSBool ok = JS_HasUCProperty(aCx, obj, keyPathChars, keyPathLen,
                                   &hasProp);
      NS_ENSURE_TRUE(ok, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);

      if (hasProp) {
        // Get if the property exists...
        JS::Rooted<JS::Value> intermediate(aCx);
        JSBool ok = JS_GetUCProperty(aCx, obj, keyPathChars, keyPathLen,
                                     intermediate.address());
        NS_ENSURE_TRUE(ok, NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);

        // Treat explicitly undefined as an error.
        if (intermediate == JSVAL_VOID) {
          return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
        }
        if (tokenizer.hasMoreTokens()) {
          // ...and walk to it if there are more steps...
          if (JSVAL_IS_PRIMITIVE(intermediate)) {
            return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
          }
          obj = JSVAL_TO_OBJECT(intermediate);
        }
        else {
          // ...otherwise use it as key
          *aKeyJSVal = intermediate;
        }
      }
      else {
        // If the property doesn't exist, fall into below path of starting
        // to define properties, if allowed.
        if (aOptions == DoNotCreateProperties) {
          return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
        }

        targetObject = obj;
        targetObjectPropName = token;
      }
    }

    if (targetObject) {
      // We have started inserting new objects or are about to just insert
      // the first one.

      *aKeyJSVal = JSVAL_VOID;

      if (tokenizer.hasMoreTokens()) {
        // If we're not at the end, we need to add a dummy object to the
        // chain.
        JS::Rooted<JSObject*> dummy(aCx, JS_NewObject(aCx, nullptr, nullptr, nullptr));
        if (!dummy) {
          rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
          break;
        }

        if (!JS_DefineUCProperty(aCx, obj, token.BeginReading(),
                                 token.Length(),
                                 OBJECT_TO_JSVAL(dummy), nullptr, nullptr,
                                 JSPROP_ENUMERATE)) {
          rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
          break;
        }

        obj = dummy;
      }
      else {
        JS::Rooted<JSObject*> dummy(aCx, JS_NewObject(aCx, &IDBObjectStore::sDummyPropJSClass,
                                                      nullptr, nullptr));
        if (!dummy) {
          rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
          break;
        }

        if (!JS_DefineUCProperty(aCx, obj, token.BeginReading(),
                                 token.Length(), OBJECT_TO_JSVAL(dummy),
                                 nullptr, nullptr, JSPROP_ENUMERATE)) {
          rv = NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
          break;
        }

        obj = dummy;
      }
    }
  }

  // We guard on rv being a success because we need to run the property
  // deletion code below even if we should not be running the callback.
  if (NS_SUCCEEDED(rv) && aCallback) {
    rv = (*aCallback)(aCx, aClosure);
  }

  if (targetObject) {
    // If this fails, we lose, and the web page sees a magical property
    // appear on the object :-(
    JS::Rooted<JS::Value> succeeded(aCx);
    if (!JS_DeleteUCProperty2(aCx, targetObject,
                              targetObjectPropName.get(),
                              targetObjectPropName.Length(),
                              succeeded.address())) {
      return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
    }
    NS_ASSERTION(JSVAL_IS_BOOLEAN(succeeded), "Wtf?");
    NS_ENSURE_TRUE(JSVAL_TO_BOOLEAN(succeeded),
                   NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR);
  }

  NS_ENSURE_SUCCESS(rv, rv);
  return rv;
}

} // anonymous namespace

// static
nsresult
KeyPath::Parse(JSContext* aCx, const JS::Value& aValue, KeyPath* aKeyPath)
{
  KeyPath keyPath(0);

  aKeyPath->SetType(NONEXISTENT);

  // See if this is a JS array.
  if (!JSVAL_IS_PRIMITIVE(aValue) &&
      JS_IsArrayObject(aCx, JSVAL_TO_OBJECT(aValue))) {

    JS::Rooted<JSObject*> obj(aCx, JSVAL_TO_OBJECT(aValue));

    uint32_t length;
    if (!JS_GetArrayLength(aCx, obj, &length)) {
      return NS_ERROR_FAILURE;
    }

    if (!length) {
      return NS_ERROR_FAILURE;
    }

    keyPath.SetType(ARRAY);

    for (uint32_t index = 0; index < length; index++) {
      JS::Rooted<JS::Value> val(aCx);
      JSString* jsstr;
      nsDependentJSString str;
      if (!JS_GetElement(aCx, obj, index, val.address()) ||
          !(jsstr = JS_ValueToString(aCx, val)) ||
          !str.init(aCx, jsstr)) {
        return NS_ERROR_FAILURE;
      }

      if (!keyPath.AppendStringWithValidation(aCx, str)) {
        return NS_ERROR_FAILURE;
      }
    }
  }
  // Otherwise convert it to a string.
  else if (!JSVAL_IS_NULL(aValue) && !JSVAL_IS_VOID(aValue)) {
    JSString* jsstr;
    nsDependentJSString str;
    if (!(jsstr = JS_ValueToString(aCx, aValue)) ||
        !str.init(aCx, jsstr)) {
      return NS_ERROR_FAILURE;
    }

    keyPath.SetType(STRING);

    if (!keyPath.AppendStringWithValidation(aCx, str)) {
      return NS_ERROR_FAILURE;
    }
  }

  *aKeyPath = keyPath;
  return NS_OK;
}

void
KeyPath::SetType(KeyPathType aType)
{
  mType = aType;
  mStrings.Clear();
}

bool
KeyPath::AppendStringWithValidation(JSContext* aCx, const nsAString& aString)
{
  if (!IsValidKeyPathString(aCx, aString)) {
    return false;
  }

  if (IsString()) {
    NS_ASSERTION(mStrings.Length() == 0, "Too many strings!");
    mStrings.AppendElement(aString);
    return true;
  }

  if (IsArray()) {
    mStrings.AppendElement(aString);
    return true;
  }

  NS_NOTREACHED("What?!");
  return false;
}

nsresult
KeyPath::ExtractKey(JSContext* aCx, const JS::Value& aValue, Key& aKey) const
{
  uint32_t len = mStrings.Length();
  JS::Rooted<JS::Value> value(aCx);

  aKey.Unset();

  for (uint32_t i = 0; i < len; ++i) {
    nsresult rv = GetJSValFromKeyPathString(aCx, aValue, mStrings[i],
                                            value.address(),
                                            DoNotCreateProperties, nullptr,
                                            nullptr);
    if (NS_FAILED(rv)) {
      return rv;
    }

    if (NS_FAILED(aKey.AppendItem(aCx, IsArray() && i == 0, value))) {
      NS_ASSERTION(aKey.IsUnset(), "Encoding error should unset");
      return NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
    }
  }

  aKey.FinishArray();

  return NS_OK;
}

nsresult
KeyPath::ExtractKeyAsJSVal(JSContext* aCx, const JS::Value& aValue,
                           JS::Value* aOutVal) const
{
  NS_ASSERTION(IsValid(), "This doesn't make sense!");

  if (IsString()) {
    return GetJSValFromKeyPathString(aCx, aValue, mStrings[0], aOutVal,
                                     DoNotCreateProperties, nullptr, nullptr);
  }

  const uint32_t len = mStrings.Length();
  JS::RootedObject arrayObj(aCx, JS_NewArrayObject(aCx, len, nullptr));
  if (!arrayObj) {
    return NS_ERROR_OUT_OF_MEMORY;
  }

  JS::Rooted<JS::Value> value(aCx);
  for (uint32_t i = 0; i < len; ++i) {
    nsresult rv = GetJSValFromKeyPathString(aCx, aValue, mStrings[i],
                                            value.address(),
                                            DoNotCreateProperties, nullptr,
                                            nullptr);
    if (NS_FAILED(rv)) {
      return rv;
    }

    if (!JS_SetElement(aCx, arrayObj, i, value.address())) {
      return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
    }
  }

  *aOutVal = OBJECT_TO_JSVAL(arrayObj);
  return NS_OK;
}

nsresult
KeyPath::ExtractOrCreateKey(JSContext* aCx, const JS::Value& aValue,
                            Key& aKey, ExtractOrCreateKeyCallback aCallback,
                            void* aClosure) const
{
  NS_ASSERTION(IsString(), "This doesn't make sense!");

  JS::Rooted<JS::Value> value(aCx);

  aKey.Unset();

  nsresult rv = GetJSValFromKeyPathString(aCx, aValue, mStrings[0],
                                          value.address(),
                                          CreateProperties, aCallback,
                                          aClosure);
  if (NS_FAILED(rv)) {
    return rv;
  }

  if (NS_FAILED(aKey.AppendItem(aCx, false, value))) {
    NS_ASSERTION(aKey.IsUnset(), "Should be unset");
    return JSVAL_IS_VOID(value) ? NS_OK : NS_ERROR_DOM_INDEXEDDB_DATA_ERR;
  }

  aKey.FinishArray();

  return NS_OK;
}

void
KeyPath::SerializeToString(nsAString& aString) const
{
  NS_ASSERTION(IsValid(), "Check to see if I'm valid first!");

  if (IsString()) {
    aString = mStrings[0];
    return;
  }

  if (IsArray()) {
    // We use a comma in the beginning to indicate that it's an array of
    // key paths. This is to be able to tell a string-keypath from an
    // array-keypath which contains only one item.
    // It also makes serializing easier :-)
    uint32_t len = mStrings.Length();
    for (uint32_t i = 0; i < len; ++i) {
      aString.Append(NS_LITERAL_STRING(",") + mStrings[i]);
    }

    return;
  }

  NS_NOTREACHED("What?");
}

// static
KeyPath
KeyPath::DeserializeFromString(const nsAString& aString)
{
  KeyPath keyPath(0);

  if (!aString.IsEmpty() && aString.First() == ',') {
    keyPath.SetType(ARRAY);

    // We use a comma in the beginning to indicate that it's an array of
    // key paths. This is to be able to tell a string-keypath from an
    // array-keypath which contains only one item.
    nsCharSeparatedTokenizerTemplate<IgnoreWhitespace> tokenizer(aString, ',');
    tokenizer.nextToken();
    while (tokenizer.hasMoreTokens()) {
      keyPath.mStrings.AppendElement(tokenizer.nextToken());
    }

    return keyPath;
  }

  keyPath.SetType(STRING);
  keyPath.mStrings.AppendElement(aString);

  return keyPath;
}

nsresult
KeyPath::ToJSVal(JSContext* aCx, JS::MutableHandle<JS::Value> aValue) const
{
  if (IsArray()) {
    uint32_t len = mStrings.Length();
    JS::Rooted<JSObject*> array(aCx, JS_NewArrayObject(aCx, len, nullptr));
    if (!array) {
      NS_WARNING("Failed to make array!");
      return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
    }

    for (uint32_t i = 0; i < len; ++i) {
      JS::Rooted<JS::Value> val(aCx);
      nsString tmp(mStrings[i]);
      if (!xpc::StringToJsval(aCx, tmp, val.address())) {
        return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
      }

      if (!JS_SetElement(aCx, array, i, val.address())) {
        return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
      }
    }

    aValue.setObject(*array);
    return NS_OK;
  }

  if (IsString()) {
    nsString tmp(mStrings[0]);
    if (!xpc::StringToJsval(aCx, tmp, aValue.address())) {
      return NS_ERROR_DOM_INDEXEDDB_UNKNOWN_ERR;
    }
    return NS_OK;
  }

  aValue.setNull();
  return NS_OK;
}

nsresult
KeyPath::ToJSVal(JSContext* aCx, JS::Heap<JS::Value>& aValue) const
{
  JS::Rooted<JS::Value> value(aCx);
  nsresult rv = ToJSVal(aCx, &value);
  if (NS_SUCCEEDED(rv)) {
    aValue = value;
  }
  return rv;
}

bool
KeyPath::IsAllowedForObjectStore(bool aAutoIncrement) const
{
  // Any keypath that passed validation is allowed for non-autoIncrement
  // objectStores.
  if (!aAutoIncrement) {
    return true;
  }

  // Array keypaths are not allowed for autoIncrement objectStores.
  if (IsArray()) {
    return false;
  }

  // Neither are empty strings.
  if (IsEmpty()) {
    return false;
  }

  // Everything else is ok.
  return true;
}
