/**
   * Copies over the prepackaged DB into the main DB then deletes the existing DB to save storage
   * space. If we have a backup that exists
   *
   * @param databaseName The name of the database to copy over
   * @param prepackagedName The name of the prepackaged db file
   */
  public void movePrepackagedDB(String databaseName, String prepackagedName) {
    final File dbPath = FlowManager.getContext().getDatabasePath(databaseName);

    // If the database already exists, and is ok return
    if (dbPath.exists()
        && (!databaseDefinition.areConsistencyChecksEnabled()
            || (databaseDefinition.areConsistencyChecksEnabled() && isDatabaseIntegrityOk()))) {
      return;
    }

    // Make sure we have a path to the file
    dbPath.getParentFile().mkdirs();

    // Try to copy database file
    try {
      // check existing and use that as backup
      File existingDb = FlowManager.getContext().getDatabasePath(getTempDbFileName());
      InputStream inputStream;
      // if it exists and the integrity is ok we use backup as the main DB is no longer valid
      if (existingDb.exists()
          && (!databaseDefinition.backupEnabled()
              || databaseDefinition.backupEnabled()
                  && FlowManager.isDatabaseIntegrityOk(backupHelper))) {
        inputStream = new FileInputStream(existingDb);
      } else {
        inputStream = FlowManager.getContext().getAssets().open(prepackagedName);
      }
      writeDB(dbPath, inputStream);

    } catch (IOException e) {
      FlowLog.log(FlowLog.Level.W, "Failed to open file", e);
    }
  }
  /**
   * Will use the already existing app database if {@link BaseDatabaseDefinition#backupEnabled()} is
   * true. If the existing is not there we will try to use the prepackaged database for that
   * purpose.
   *
   * @param databaseName The name of the database to restore
   * @param prepackagedName The name of the prepackaged db file
   */
  public void restoreDatabase(String databaseName, String prepackagedName) {
    final File dbPath = FlowManager.getContext().getDatabasePath(databaseName);

    // If the database already exists, return
    if (dbPath.exists()) {
      return;
    }

    // Make sure we have a path to the file
    dbPath.getParentFile().mkdirs();

    // Try to copy database file
    try {
      // check existing and use that as backup
      File existingDb =
          FlowManager.getContext().getDatabasePath(databaseDefinition.getDatabaseFileName());
      InputStream inputStream;
      // if it exists and the integrity is ok
      if (existingDb.exists()
          && (databaseDefinition.backupEnabled()
              && FlowManager.isDatabaseIntegrityOk(backupHelper))) {
        inputStream = new FileInputStream(existingDb);
      } else {
        inputStream = FlowManager.getContext().getAssets().open(prepackagedName);
      }
      writeDB(dbPath, inputStream);
    } catch (IOException e) {
      FlowLog.logError(e);
    }
  }
  /**
   * Pulled partially from code, it runs a "PRAGMA quick_check(1)" to see if the database is ok.
   * This method will {@link #restoreBackUp()} if they are enabled on the database if this check
   * fails. So use with caution and ensure that you backup the database often!
   *
   * @return true if the database is ok, false if the consistency has been compromised.
   */
  public boolean isDatabaseIntegrityOk() {
    boolean integrityOk = true;

    SQLiteStatement prog = null;
    try {
      prog = getWritableDatabase().compileStatement("PRAGMA quick_check(1)");
      String rslt = prog.simpleQueryForString();
      if (!rslt.equalsIgnoreCase("ok")) {
        // integrity_checker failed on main or attached databases
        FlowLog.log(
            FlowLog.Level.E,
            "PRAGMA integrity_check on "
                + databaseDefinition.getDatabaseName()
                + " returned: "
                + rslt);

        integrityOk = false;

        if (databaseDefinition.backupEnabled()) {
          integrityOk = restoreBackUp();
        }
      }
    } finally {
      if (prog != null) {
        prog.close();
      }
    }
    return integrityOk;
  }
  /**
   * Saves the database as a backup on the {@link
   * com.raizlabs.android.dbflow.runtime.DBTransactionQueue} as the highest priority ever. This will
   * create a THIRD database to use as a backup to the backup in case somehow the overwrite fails.
   */
  public void backupDB() {
    if (!databaseDefinition.backupEnabled() || !databaseDefinition.areConsistencyChecksEnabled()) {
      throw new IllegalStateException(
          "Backups are not enabled for : "
              + databaseDefinition.getDatabaseName()
              + ". Please consider adding "
              + "both backupEnabled and consistency checks enabled to the Database annotation");
    }
    // highest priority ever!
    TransactionManager.getInstance()
        .addTransaction(
            new BaseTransaction(DBTransactionInfo.create(BaseTransaction.PRIORITY_UI + 1)) {
              @Override
              public Object onExecute() {

                Context context = FlowManager.getContext();
                File backup = context.getDatabasePath(getTempDbFileName());
                File temp =
                    context.getDatabasePath(
                        TEMP_DB_NAME + "-2-" + databaseDefinition.getDatabaseFileName());

                // if exists we want to delete it before rename
                if (temp.exists()) {
                  temp.delete();
                }

                backup.renameTo(temp);
                if (backup.exists()) {
                  backup.delete();
                }
                File existing = context.getDatabasePath(databaseDefinition.getDatabaseFileName());

                try {
                  backup.getParentFile().mkdirs();
                  writeDB(backup, new FileInputStream(existing));

                  temp.delete();
                } catch (Exception e) {
                  FlowLog.logError(e);
                }
                return null;
              }
            });
  }
  @Override
  public void onCreate(SQLiteDatabase db) {
    if (databaseHelperListener != null) {
      databaseHelperListener.onCreate(db);
    }

    checkForeignKeySupport(db);
    executeCreations(db);
    executeMigrations(db, -1, databaseDefinition.getDatabaseVersion());
  }
  /**
   * If integrity check fails, this method will use the backup db to fix itself. In order to prevent
   * loss of data, please backup often!
   */
  public boolean restoreBackUp() {
    boolean success = true;

    File db =
        FlowManager.getContext()
            .getDatabasePath(TEMP_DB_NAME + databaseDefinition.getDatabaseName());
    File corrupt = FlowManager.getContext().getDatabasePath(databaseDefinition.getDatabaseName());
    if (corrupt.delete()) {
      try {
        writeDB(corrupt, new FileInputStream(db));
      } catch (IOException e) {
        FlowLog.logError(e);
        success = false;
      }
    } else {
      FlowLog.log(FlowLog.Level.E, "Failed to delete DB");
    }
    return success;
  }
  public FlowSQLiteOpenHelper(
      BaseDatabaseDefinition databaseDefinition, DatabaseHelperListener listener) {
    super(
        FlowManager.getContext(),
        databaseDefinition.isInMemory() ? null : databaseDefinition.getDatabaseFileName(),
        null,
        databaseDefinition.getDatabaseVersion());
    databaseHelperListener = listener;
    this.databaseDefinition = databaseDefinition;

    movePrepackagedDB(
        this.databaseDefinition.getDatabaseFileName(),
        this.databaseDefinition.getDatabaseFileName());

    if (databaseDefinition.backupEnabled()) {
      // Temp database mirrors existing
      backupHelper =
          new SQLiteOpenHelper(
              FlowManager.getContext(),
              getTempDbFileName(),
              null,
              databaseDefinition.getDatabaseVersion()) {
            @Override
            public void onOpen(SQLiteDatabase db) {
              FlowSQLiteOpenHelper.this.onOpen(db);
            }

            @Override
            public void onCreate(SQLiteDatabase db) {
              FlowSQLiteOpenHelper.this.onCreate(db);
            }

            @Override
            public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
              FlowSQLiteOpenHelper.this.onUpgrade(db, oldVersion, newVersion);
            }
          };
      restoreDatabase(getTempDbFileName(), this.databaseDefinition.getDatabaseFileName());
      backupHelper.getWritableDatabase();
    }
  }
  /**
   * Supports multiline sql statements with ended with the standard ";"
   *
   * @param db The database to run it on
   * @param file the file name in assets/migrations that we read from
   */
  private void executeSqlScript(SQLiteDatabase db, String file) {
    try {
      final InputStream input =
          FlowManager.getContext()
              .getAssets()
              .open(MIGRATION_PATH + "/" + databaseDefinition.getDatabaseName() + "/" + file);
      final BufferedReader reader = new BufferedReader(new InputStreamReader(input));
      String line;

      // ends line with SQL
      String querySuffix = ";";

      // standard java comments
      String queryCommentPrefix = "\\";
      StringBuffer query = new StringBuffer();

      while ((line = reader.readLine()) != null) {
        line = line.trim();
        boolean isEndOfQuery = line.endsWith(querySuffix);
        if (line.startsWith(queryCommentPrefix)) {
          continue;
        }
        if (isEndOfQuery) {
          line = line.substring(0, line.length() - querySuffix.length());
        }
        query.append(" ").append(line);
        if (isEndOfQuery) {
          db.execSQL(query.toString());
          query = new StringBuffer();
        }
      }

      if (query.length() > 0) {
        db.execSQL(query.toString());
      }
    } catch (IOException e) {
      FlowLog.log(FlowLog.Level.E, "Failed to execute " + file, e);
    }
  }
 /**
  * @return the temporary database file name for when we have backups enabled {@link
  *     BaseDatabaseDefinition#backupEnabled()}
  */
 private String getTempDbFileName() {
   return TEMP_DB_NAME + databaseDefinition.getDatabaseName() + ".db";
 }
  private void executeMigrations(
      final SQLiteDatabase db, final int oldVersion, final int newVersion) {

    // will try migrations file or execute migrations from code
    try {
      final List<String> files =
          Arrays.asList(
              FlowManager.getContext()
                  .getAssets()
                  .list(MIGRATION_PATH + "/" + databaseDefinition.getDatabaseName()));
      Collections.sort(files, new NaturalOrderComparator());

      final Map<Integer, List<String>> migrationFileMap = new HashMap<>();
      for (String file : files) {
        try {
          final Integer version = Integer.valueOf(file.replace(".sql", ""));
          List<String> fileList = migrationFileMap.get(version);
          if (fileList == null) {
            fileList = new ArrayList<>();
            migrationFileMap.put(version, fileList);
          }
          fileList.add(file);
        } catch (NumberFormatException e) {
          FlowLog.log(FlowLog.Level.W, "Skipping invalidly named file: " + file, e);
        }
      }

      final Map<Integer, List<Migration>> migrationMap = databaseDefinition.getMigrations();

      final int curVersion = oldVersion + 1;

      TransactionManager.transact(
          db,
          new Runnable() {
            @Override
            public void run() {

              // execute migrations in order, migration file first before wrapped migration classes.
              for (int i = curVersion; i <= newVersion; i++) {
                List<String> migrationFiles = migrationFileMap.get(i);
                if (migrationFiles != null) {
                  for (String migrationFile : migrationFiles) {
                    executeSqlScript(db, migrationFile);
                    FlowLog.log(FlowLog.Level.I, migrationFile + " executed successfully.");
                  }
                }

                if (migrationMap != null) {
                  List<Migration> migrationsList = migrationMap.get(i);
                  if (migrationsList != null) {
                    for (Migration migration : migrationsList) {
                      // before migration
                      migration.onPreMigrate();

                      // migrate
                      migration.migrate(db);

                      // after migration cleanup
                      migration.onPostMigrate();
                      FlowLog.log(
                          FlowLog.Level.I, migration.getClass() + " executed successfully.");
                    }
                  }
                }
              }
            }
          });
    } catch (IOException e) {
      FlowLog.log(FlowLog.Level.E, "Failed to execute migrations.", e);
    }
  }
 /**
  * If foreign keys are supported, we turn it on the DB.
  *
  * @param database
  */
 private void checkForeignKeySupport(SQLiteDatabase database) {
   if (databaseDefinition.isForeignKeysSupported()) {
     database.execSQL("PRAGMA foreign_keys=ON;");
     FlowLog.log(FlowLog.Level.I, "Foreign Keys supported. Enabling foreign key features.");
   }
 }