/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */

package com.facebook.react.modules.storage;

import javax.annotation.Nullable;

import java.util.Arrays;
import java.util.Iterator;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.text.TextUtils;

import com.facebook.react.bridge.ReadableArray;

import org.json.JSONException;
import org.json.JSONObject;

import static com.facebook.react.modules.storage.ReactDatabaseSupplier.KEY_COLUMN;
import static com.facebook.react.modules.storage.ReactDatabaseSupplier.TABLE_CATALYST;
import static com.facebook.react.modules.storage.ReactDatabaseSupplier.VALUE_COLUMN;

/**
 * Helper for database operations.
 */
public class AsyncLocalStorageUtil {

  /**
   * Build the String required for an SQL select statement:
   *  WHERE key IN (?, ?, ..., ?)
   * without 'WHERE' and with selectionCount '?'
   */
  /* package */ static String buildKeySelection(int selectionCount) {
    String[] list = new String[selectionCount];
    Arrays.fill(list, "?");
    return KEY_COLUMN + " IN (" + TextUtils.join(", ", list) + ")";
  }

  /**
   * Build the String[] arguments needed for an SQL selection, i.e.:
   *  {a, b, c}
   * to be used in the SQL select statement: WHERE key in (?, ?, ?)
   */
  /* package */ static String[] buildKeySelectionArgs(ReadableArray keys, int start, int count) {
    String[] selectionArgs = new String[count];
    for (int keyIndex = 0; keyIndex < count; keyIndex++) {
      selectionArgs[keyIndex] = keys.getString(start + keyIndex);
    }
    return selectionArgs;
  }

  /**
   * Returns the value of the given key, or null if not found.
   */
  public static @Nullable String getItemImpl(SQLiteDatabase db, String key) {
    String[] columns = {VALUE_COLUMN};
    String[] selectionArgs = {key};

    Cursor cursor = db.query(
        TABLE_CATALYST,
        columns,
        KEY_COLUMN + "=?",
        selectionArgs,
        null,
        null,
        null);

    try {
      if (!cursor.moveToFirst()) {
        return null;
      } else {
        return cursor.getString(0);
      }
    } finally {
      cursor.close();
    }
  }

  /**
   * Sets the value for the key given, returns true if successful, false otherwise.
   */
  /* package */ static boolean setItemImpl(SQLiteDatabase db, String key, String value) {
    ContentValues contentValues = new ContentValues();
    contentValues.put(KEY_COLUMN, key);
    contentValues.put(VALUE_COLUMN, value);

    long inserted = db.insertWithOnConflict(
        TABLE_CATALYST,
        null,
        contentValues,
        SQLiteDatabase.CONFLICT_REPLACE);

    return (-1 != inserted);
  }

  /**
   * Does the actual merge of the (key, value) pair with the value stored in the database.
   * NB: This assumes that a database lock is already in effect!
   * @return the errorCode of the operation
   */
  /* package */ static boolean mergeImpl(SQLiteDatabase db, String key, String value)
      throws JSONException {
    String oldValue = getItemImpl(db, key);
    String newValue;

    if (oldValue == null) {
      newValue = value;
    } else {
      JSONObject oldJSON = new JSONObject(oldValue);
      JSONObject newJSON = new JSONObject(value);
      deepMergeInto(oldJSON, newJSON);
      newValue = oldJSON.toString();
    }

    return setItemImpl(db, key, newValue);
  }

  /**
   * Merges two {@link JSONObject}s. The newJSON object will be merged with the oldJSON object by
   * either overriding its values, or merging them (if the values of the same key in both objects
   * are of type {@link JSONObject}). oldJSON will contain the result of this merge.
   */
  private static void deepMergeInto(JSONObject oldJSON, JSONObject newJSON)
      throws JSONException {
    Iterator<?> keys = newJSON.keys();
    while (keys.hasNext()) {
      String key = (String) keys.next();

      JSONObject newJSONObject = newJSON.optJSONObject(key);
      JSONObject oldJSONObject = oldJSON.optJSONObject(key);
      if (newJSONObject != null && oldJSONObject != null) {
        deepMergeInto(oldJSONObject, newJSONObject);
        oldJSON.put(key, oldJSONObject);
      } else {
        oldJSON.put(key, newJSON.get(key));
      }
    }
  }
}
