/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.stetho.inspector.database;

import android.annotation.TargetApi;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteStatement;

import com.facebook.stetho.common.Util;
import com.facebook.stetho.inspector.protocol.module.Database;
import com.facebook.stetho.inspector.protocol.module.DatabaseConstants;
import com.facebook.stetho.inspector.protocol.module.DatabaseDescriptor;
import com.facebook.stetho.inspector.protocol.module.DatabaseDriver2;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.annotation.concurrent.ThreadSafe;

@ThreadSafe
public class SqliteDatabaseDriver
    extends DatabaseDriver2<SqliteDatabaseDriver.SqliteDatabaseDescriptor> {
  private static final String[] UNINTERESTING_FILENAME_SUFFIXES = new String[]{
      "-journal",
      "-shm",
      "-uid",
      "-wal"
  };

  private final DatabaseFilesProvider mDatabaseFilesProvider;
  private final DatabaseConnectionProvider mDatabaseConnectionProvider;

  /**
   * Constructs the object with a {@link DatabaseFilesProvider} that supplies the database files
   * from {@link Context#databaseList()}.
   *
   * @param context the context
   * @deprecated use {@link SqliteDatabaseDriver#SqliteDatabaseDriver(Context, String, DatabaseFilesProvider, DatabaseConnectionProvider)}
   */
  @Deprecated
  public SqliteDatabaseDriver(Context context) {
    this(
        context,
        new DefaultDatabaseFilesProvider(context),
        new DefaultDatabaseConnectionProvider());
  }

  /**
   * @deprecated use {@link SqliteDatabaseDriver#SqliteDatabaseDriver(Context, String, DatabaseFilesProvider, DatabaseConnectionProvider)}
   */
  @Deprecated
  public SqliteDatabaseDriver(
      Context context,
      DatabaseFilesProvider databaseFilesProvider) {
    this(
        context,
        databaseFilesProvider,
        new DefaultDatabaseConnectionProvider());
  }

  /**
   * @param context the context
   * @param namespace label to apply to the driver when it appears in the UI
   * @param databaseFilesProvider a database file name provider
   * @param databaseConnectionProvider a database connection provider
   */
  public SqliteDatabaseDriver(
      Context context,
      DatabaseFilesProvider databaseFilesProvider,
      DatabaseConnectionProvider databaseConnectionProvider) {
    super(context);
    mDatabaseFilesProvider = databaseFilesProvider;
    mDatabaseConnectionProvider = databaseConnectionProvider;
  }

  @Override
  public List<SqliteDatabaseDescriptor> getDatabaseNames() {
    ArrayList<SqliteDatabaseDescriptor> databases = new ArrayList<>();
    List<File> potentialDatabaseFiles = mDatabaseFilesProvider.getDatabaseFiles();
    Collections.sort(potentialDatabaseFiles);
    Iterable<File> tidiedList = tidyDatabaseList(potentialDatabaseFiles);
    for (File database : tidiedList) {
      databases.add(new SqliteDatabaseDescriptor(database));
    }
    return databases;
  }

  /**
   * Attempt to smartly eliminate uninteresting shadow databases such as -journal and -uid.  Note
   * that this only removes the database if it is true that it shadows another database lacking
   * the uninteresting suffix.
   *
   * @param databaseFiles Raw list of database files.
   * @return Tidied list with shadow databases removed.
   */
  // @VisibleForTesting
  static List<File> tidyDatabaseList(List<File> databaseFiles) {
    Set<File> originalAsSet = new HashSet<File>(databaseFiles);
    List<File> tidiedList = new ArrayList<File>();
    for (File databaseFile : databaseFiles) {
      String databaseFilename = databaseFile.getPath();
      String sansSuffix = removeSuffix(databaseFilename, UNINTERESTING_FILENAME_SUFFIXES);
      if (sansSuffix.equals(databaseFilename) || !originalAsSet.contains(new File(sansSuffix))) {
        tidiedList.add(databaseFile);
      }
    }
    return tidiedList;
  }

  private static String removeSuffix(String str, String[] suffixesToRemove) {
    for (String suffix : suffixesToRemove) {
      if (str.endsWith(suffix)) {
        return str.substring(0, str.length() - suffix.length());
      }
    }
    return str;
  }

  public List<String> getTableNames(SqliteDatabaseDescriptor databaseDesc)
      throws SQLiteException {
    SQLiteDatabase database = openDatabase(databaseDesc);
    try {
      Cursor cursor = database.rawQuery("SELECT name FROM sqlite_master WHERE type IN (?, ?)",
          new String[] { "table", "view" });
      try {
        List<String> tableNames = new ArrayList<String>();
        while (cursor.moveToNext()) {
          tableNames.add(cursor.getString(0));
        }
        return tableNames;
      } finally {
        cursor.close();
      }
    } finally {
      database.close();
    }
  }

  public Database.ExecuteSQLResponse executeSQL(
      SqliteDatabaseDescriptor databaseDesc,
      String query,
      ExecuteResultHandler<Database.ExecuteSQLResponse> handler)
          throws SQLiteException {
    Util.throwIfNull(query);
    Util.throwIfNull(handler);
    SQLiteDatabase database = openDatabase(databaseDesc);
    try {
      String firstWordUpperCase = getFirstWord(query).toUpperCase();
      switch (firstWordUpperCase) {
        case "UPDATE":
        case "DELETE":
          return executeUpdateDelete(database, query, handler);
        case "INSERT":
          return executeInsert(database, query, handler);
        case "SELECT":
        case "PRAGMA":
        case "EXPLAIN":
          return executeSelect(database, query, handler);
        default:
          return executeRawQuery(database, query, handler);
      }
    } finally {
      database.close();
    }
  }

  private static String getFirstWord(String s) {
    s = s.trim();
    int firstSpace = s.indexOf(' ');
    return firstSpace >= 0 ? s.substring(0, firstSpace) : s;
  }

  @TargetApi(DatabaseConstants.MIN_API_LEVEL)
  private <T> T executeUpdateDelete(
      SQLiteDatabase database,
      String query,
      ExecuteResultHandler<T> handler) {
    SQLiteStatement statement = database.compileStatement(query);
    int count = statement.executeUpdateDelete();
    return handler.handleUpdateDelete(count);
  }

  private <T> T executeInsert(
      SQLiteDatabase database,
      String query,
      ExecuteResultHandler<T> handler) {
    SQLiteStatement statement = database.compileStatement(query);
    long count = statement.executeInsert();
    return handler.handleInsert(count);
  }

  private <T> T executeSelect(
      SQLiteDatabase database,
      String query,
      ExecuteResultHandler<T> handler) {
    Cursor cursor = database.rawQuery(query, null);
    try {
      return handler.handleSelect(cursor);
    } finally {
      cursor.close();
    }
  }

  private <T> T executeRawQuery(
      SQLiteDatabase database,
      String query,
      ExecuteResultHandler<T> handler) {
    database.execSQL(query);
    return handler.handleRawQuery();
  }

  private SQLiteDatabase openDatabase(
      SqliteDatabaseDescriptor databaseDesc)
      throws SQLiteException {
    Util.throwIfNull(databaseDesc);
    return mDatabaseConnectionProvider.openDatabase(databaseDesc.file);
  }

  static class SqliteDatabaseDescriptor implements DatabaseDescriptor {
    public final File file;

    public SqliteDatabaseDescriptor(File file) {
      this.file = file;
    }

    @Override
    public String name() {
      return file.getName();
    }
  }
}