/*
 * Copyright (c) 2015, Andrzej Porebski
 * Copyright (c) 2012-2015, Chris Brody
 * Copyright (c) 2005-2010, Nitobi Software Inc.
 * Copyright (c) 2010, IBM Corporation
 */

package io.liteglue;

import android.content.Context;

import com.facebook.common.logging.FLog;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Callback;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.ReadableType;
import com.facebook.react.bridge.WritableArray;
import com.facebook.react.bridge.WritableMap;

import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.lang.IllegalArgumentException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;

import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;

public class SQLitePlugin extends ReactContextBaseJavaModule {

    public static final String TAG = SQLitePlugin.class.getSimpleName();

    /**
     * Multiple database runner map (static).
     * NOTE: no public static accessor to db (runner) map since it would not work with db threading.
     * FUTURE put DBRunner into a public class that can provide external accessor.
     */
    static ConcurrentHashMap<String, DBRunner> dbrmap = new ConcurrentHashMap<String, DBRunner>();

    /**
     * SQLiteGlueConnector (instance of SQLiteConnector) for NDK version:
     */
    static SQLiteConnector connector = new SQLiteConnector();

    protected ExecutorService threadPool;
    private Context context;

    public SQLitePlugin(ReactApplicationContext reactContext) {
        super(reactContext);
        this.context = reactContext.getApplicationContext();
        this.threadPool = Executors.newCachedThreadPool();
    }

    /**
     * Required React Native method
     */
    @Override
    public String getName() {
        return "SQLite";
    }

    @ReactMethod
    public void open(ReadableMap args, Callback success, Callback error) {
        String actionAsString = "open";
        try {
            this.execute(actionAsString, args, new CallbackContext(success, error));
        } catch (Exception ex){
            error.invoke("Unexpected error:"+ex.getMessage());
        }
    }

    @ReactMethod
    public void close(ReadableMap args, Callback success, Callback error) {
        String actionAsString = "close";
        try {
            this.execute(actionAsString, args, new CallbackContext(success, error));
        } catch (Exception ex){
            error.invoke("Unexpected error"+ex.getMessage());
        }
    }

    @ReactMethod
    public void attach(ReadableMap args, Callback success, Callback error) {
        String actionAsString = "attach";
        try {
            this.execute(actionAsString, args, new CallbackContext(success, error));
        } catch (Exception ex){
            error.invoke("Unexpected error"+ex.getMessage());
        }
    }

    @ReactMethod
    public void delete(ReadableMap args, Callback success, Callback error) {
        String actionAsString = "delete";
        try {
            this.execute(actionAsString, args, new CallbackContext(success, error));
        } catch (Exception ex){
            error.invoke("Unexpected error"+ex.getMessage());
        }
    }

    @ReactMethod
    public void backgroundExecuteSqlBatch(ReadableMap args, Callback success, Callback error) {
        String actionAsString = "backgroundExecuteSqlBatch";
        try {
            this.execute(actionAsString, args, new CallbackContext(success, error));
        } catch (Exception ex){
            error.invoke("Unexpected error"+ex.getMessage());
        }
    }

    @ReactMethod
    public void executeSqlBatch(ReadableMap args, Callback success, Callback error) {
        String actionAsString = "executeSqlBatch";
        try {
            this.execute(actionAsString, args, new CallbackContext(success, error));
        } catch (Exception ex){
            error.invoke("Unexpected error");
        }
    }

    @ReactMethod
    public void echoStringValue(ReadableMap args, Callback success, Callback error) {
        String actionAsString = "echoStringValue";
        try {
            this.execute(actionAsString, args, new CallbackContext(success, error));
        } catch (Exception ex){
            error.invoke("Unexpected error");
        }
    }

    protected ExecutorService getThreadPool(){
        return this.threadPool;
    }

    protected Context getContext(){
        return this.context;
    }

    /**
     * Executes the request and returns PluginResult.
     *
     * @param actionAsString The action to execute.
     * @param args   JSONArray of arguments for the plugin.
     * @param cbc    Callback context from Cordova API
     * @return       Whether the action was valid.
     */
    protected boolean execute(String actionAsString, ReadableMap args, CallbackContext cbc) throws Exception{

        Action action;
        try {
            action = Action.valueOf(actionAsString);
        } catch (IllegalArgumentException ex) {
            // shouldn't ever happen
            FLog.e(TAG, "unexpected error", ex);
            cbc.error("Unexpected error executing processing SQLite query");
            throw ex;
        }
        try {
            return executeAndPossiblyThrow(action, args, cbc);
        } catch (Exception ex) {
            FLog.e(TAG, "unexpected error", ex);
            cbc.error("Unexpected error executing processing SQLite query");
            throw ex;
        }
    }

    private boolean executeAndPossiblyThrow(Action action, ReadableMap args, CallbackContext cbc){
        String dbname;

        switch (action) {
            case echoStringValue:
                String echo_value = SQLitePluginConverter.getString(args,"value","");
                cbc.success(echo_value);
                break;

            case open:
                dbname = SQLitePluginConverter.getString(args,"name","");
                // open database and start reading its queue
                this.startDatabase(dbname, args, cbc);
                break;

            case close:
                dbname = SQLitePluginConverter.getString(args,"path","");
                // put request in the q to close the db
                this.closeDatabase(dbname, cbc);
                break;

            case attach:
                dbname = SQLitePluginConverter.getString(args,"path","");
                String dbAlias = SQLitePluginConverter.getString(args,"dbAlias","");
                String dbNameToAttach = SQLitePluginConverter.getString(args,"dbName","");
                this.attachDatabase(dbname,dbNameToAttach,dbAlias,cbc);
                break;

            case delete:
                dbname = SQLitePluginConverter.getString(args,"path","");
                deleteDatabase(dbname, cbc);
                break;

            case executeSqlBatch:
            case backgroundExecuteSqlBatch:
                String [] queries;
                String [] queryIDs = null;
                ReadableArray[] queryParams = null;
                ReadableMap dbArgs = (ReadableMap) SQLitePluginConverter.get(args,"dbargs",null);
                dbname = SQLitePluginConverter.getString(dbArgs,"dbname","");
                ReadableArray txArgs = (ReadableArray) SQLitePluginConverter.get(args,"executes",null);

                if (txArgs.isNull(0)) {
                    queries = new String[0];
                } else {
                    int len = txArgs.size();
                    queries = new String[len];
                    queryIDs = new String[len];
                    queryParams = new ReadableArray[len];

                    for (int i = 0; i < len; i++) {
                        ReadableMap queryArgs = (ReadableMap) SQLitePluginConverter.get(txArgs,i,null);
                        queries[i] = SQLitePluginConverter.getString(queryArgs,"sql","");
                        queryIDs[i] = SQLitePluginConverter.getString(queryArgs,"qid","");
                        queryParams[i] = (ReadableArray) SQLitePluginConverter.get(queryArgs,"params",null);
                    }
                }

                // put db query in the queue to be executed in the db thread:
                DBQuery q = new DBQuery(queries, queryIDs, queryParams, cbc);
                DBRunner r = dbrmap.get(dbname);
                if (r != null) {
                    try {
                        r.q.put(q);
                    } catch(Exception ex) {
                        FLog.e(TAG, "couldn't add to queue", ex);
                        cbc.error("couldn't add to queue");
                    }
                } else {
                    cbc.error("database not open");
                }
                break;
        }

        return true;
    }

    /**
     * Clean up and close all open databases.
     */
    public void closeAllOpenDatabases() {
        while (!dbrmap.isEmpty()) {
            String dbname = dbrmap.keySet().iterator().next();

            this.closeDatabaseNow(dbname);

            DBRunner r = dbrmap.get(dbname);
            try {
                // stop the db runner thread:
                r.q.put(new DBQuery());
            } catch(Exception ex) {
                FLog.e(TAG, "couldn't stop db thread for db: " + dbname,ex);
            }
            dbrmap.remove(dbname);
        }
    }

    /**
     *
     * @param dbname - The name of the database file
     * @param options - options passed in from JS
     * @param cbc - JS callback context
     */
    private void startDatabase(String dbname, ReadableMap options, CallbackContext cbc) {
        // TODO: is it an issue that we can orphan an existing thread?  What should we do here?
        // If we re-use the existing DBRunner it might be in the process of closing...
        DBRunner r = dbrmap.get(dbname);

        // Brody TODO: It may be better to terminate the existing db thread here & start a new one, instead.
        if (r != null) {
            // don't orphan the existing thread; just re-open the existing database.
            // In the worst case it might be in the process of closing, but even that's less serious
            // than orphaning the old DBRunner.
            cbc.success("database started");
        } else {
            r = new DBRunner(dbname, options, cbc);
            dbrmap.put(dbname, r);
            this.getThreadPool().execute(r);
        }
    }

    /**
     * Open a database.
     *
     * @param dbname   The name of the database file
     */
    private SQLiteAndroidDatabase openDatabase(String dbname, String assetFilePath, int openFlags, CallbackContext cbc, boolean old_impl) throws Exception {
        InputStream in = null;
        File dbfile = null;
        try {
            boolean assetImportError = false;
            boolean assetImportRequested = assetFilePath != null && assetFilePath.length() > 0;
            if (assetImportRequested) {
                if (assetFilePath.compareTo("1") == 0) {
                    assetFilePath = "www/" + dbname;
                    in = this.getContext().getAssets().open(assetFilePath);
                    try {
                        in = this.getContext().getAssets().open(assetFilePath);
                        FLog.v(TAG, "Pre-populated DB asset FOUND  in app bundle www subdirectory: " + assetFilePath);
                    } catch (Exception ex){
                        assetImportError = true;
                        FLog.e(TAG, "pre-populated DB asset NOT FOUND in app bundle www subdirectory: " + assetFilePath);
                    }
                } else if (assetFilePath.charAt(0) == '~') {
                    assetFilePath = assetFilePath.startsWith("~/") ? assetFilePath.substring(2) : assetFilePath.substring(1);
                    try {
                        in = this.getContext().getAssets().open(assetFilePath);
                        FLog.v(TAG, "Pre-populated DB asset FOUND in app bundle subdirectory: " + assetFilePath);
                    } catch (Exception ex){
                        assetImportError = true;
                        FLog.e(TAG, "pre-populated DB asset NOT FOUND in app bundle www subdirectory: " + assetFilePath);
                    }
                } else {
                    File filesDir = this.getContext().getFilesDir();
                    assetFilePath = assetFilePath.startsWith("/") ? assetFilePath.substring(1) : assetFilePath;

                    try {
                        File assetFile = new File(filesDir, assetFilePath);
                        in = new FileInputStream(assetFile);
                        FLog.v(TAG, "Pre-populated DB asset FOUND in Files subdirectory: " + assetFile.getCanonicalPath());
                        if (openFlags == SQLiteOpenFlags.READONLY) {
                            dbfile = assetFile;
                            FLog.v(TAG, "Detected read-only mode request for external asset.");
                        }
                    } catch (Exception ex){
                        assetImportError = true;
                        FLog.e(TAG, "Error opening pre-populated DB asset in app bundle www subdirectory: " + assetFilePath);
                    }
                }
            }

            if (dbfile == null) {
                openFlags = SQLiteOpenFlags.CREATE | SQLiteOpenFlags.READWRITE;
                dbfile = this.getContext().getDatabasePath(dbname);

                if (!dbfile.exists() && assetImportRequested) {
                    if (assetImportError || in == null) {
                        FLog.e(TAG, "Unable to import pre-populated db asset");
                        throw new Exception("Unable to import pre-populated db asset");
                    } else {
                        FLog.v(TAG, "Copying pre-populated db asset to destination");
                        try {
                            this.createFromAssets(dbname, dbfile, in);
                        } catch (Exception ex){
                            FLog.e(TAG, "Error importing pre-populated DB asset", ex);
                            throw new Exception("Error importing pre-populated DB asset");
                        }
                    }
                }

                if (!dbfile.exists()) {
                    dbfile.getParentFile().mkdirs();
                }
            }

            // Pass in mode to open call
            SQLiteAndroidDatabase mydb = old_impl ? new SQLiteAndroidDatabase() : new SQLiteDatabaseNDK();
            mydb.open(dbfile, openFlags);

            if (cbc != null)
                cbc.success("database open");

            return mydb;
        } finally {
            closeQuietly(in);
        }
    }

    /**
     * If a prepopulated DB file exists in the assets folder it is copied to the dbPath.
     * Only runs the first time the app runs.
     *
     * @param dbName The name of the database file - could be used as filename for imported asset
     * @param dbfile The File of the destination db
     * @param assetFileInputStream input file stream for pre-populated db asset
     */
    private void createFromAssets(String dbName, File dbfile, InputStream assetFileInputStream) throws Exception
    {
        OutputStream out = null;
        try {
            String dbPath = dbfile.getAbsolutePath();
            dbPath = dbPath.substring(0, dbPath.lastIndexOf("/") + 1);

            File dbPathFile = new File(dbPath);
            if (!dbPathFile.exists())
                dbPathFile.mkdirs();

            File newDbFile = new File(dbPath + dbName);
            out = new FileOutputStream(newDbFile);

            // XXX TODO: this is very primitive, other alternatives at:
            // http://www.journaldev.com/861/4-ways-to-copy-file-in-java
            byte[] buf = new byte[1024];
            int len;
            while ((len = assetFileInputStream.read(buf)) > 0)
                out.write(buf, 0, len);

            FLog.v(TAG, "Copied pre-populated DB content to: " + newDbFile.getAbsolutePath());
        } finally {
            closeQuietly(out);
        }
    }

    /**
     * Close a database (in another thread).
     *
     * @param dbname   The name of the database file
     */
    private void closeDatabase(String dbname, CallbackContext cbc) {
        DBRunner r = dbrmap.get(dbname);
        if (r != null) {
            try {
                r.q.put(new DBQuery(false, cbc));
            } catch(Exception ex) {
                if (cbc != null) {
                    cbc.error("couldn't close database" + ex);
                }
                FLog.e(TAG, "couldn't close database", ex);
            }
        } else {
            if (cbc != null) {
                cbc.success("couldn't close database");
            }
        }
    }

    /**
     * Close a database (in the current thread).
     *
     * @param dbname   The name of the database file
     */
    private void closeDatabaseNow(String dbname) {
        DBRunner r = dbrmap.get(dbname);

        if (r != null) {
            SQLiteAndroidDatabase mydb = r.mydb;

            if (mydb != null)
                mydb.closeDatabaseNow();
        }
    }

    /**
     * Attach a database
     *
     * @param dbName - The name of the database file
     * @param dbNameToAttach - The name of the database file to attach
     * @param dbAlias - The alias of the attached database
     * @param cbc - JS callback
     */
    private void attachDatabase(String dbName, String dbNameToAttach, String dbAlias, CallbackContext cbc) {
        DBRunner runner = dbrmap.get(dbName);
        if (runner != null) {
            File dbfile = this.getContext().getDatabasePath(dbNameToAttach);
            String filePathToAttached = dbfile.getAbsolutePath();
            String stmt = "ATTACH DATABASE '" + filePathToAttached + "' AS " + dbAlias;
            // TODO: remove qid it's hardcoded in js to be 1111 always anyway
            DBQuery query = new DBQuery(new String[]{stmt}, new String[]{"1111"}, null, cbc);
            try {
                runner.q.put(query);
            } catch (InterruptedException ex) {
                cbc.error("Can't put querry into the queue");
            }
        } else {
            cbc.error("Can't attach to database - it's not open yet");
        }
    }

    private void deleteDatabase(String dbname, CallbackContext cbc) {
        DBRunner r = dbrmap.get(dbname);
        if (r != null) {
            try {
                r.q.put(new DBQuery(true, cbc));
            } catch(Exception ex) {
                if (cbc != null) {
                    cbc.error("couldn't close database" + ex);
                }
                FLog.e(TAG, "couldn't close database", ex);
            }
        } else {
            boolean deleteResult = this.deleteDatabaseNow(dbname);
            if (deleteResult) {
                cbc.success("database deleted");
            } else {
                cbc.error("couldn't delete database");
            }
        }
    }

    /**
     * Delete a database.
     *
     * @param dbname   The name of the database file
     *
     * @return true if successful or false if an exception was encountered
     */
    private boolean deleteDatabaseNow(String dbname) {
        File dbfile = this.getContext().getDatabasePath(dbname);

        try {
            return this.getContext().deleteDatabase(dbfile.getAbsolutePath());
        } catch (Exception ex) {
            FLog.e(TAG, "couldn't delete database", ex);
            return false;
        }
    }

    private void closeQuietly(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (IOException ex) {
                // ignore
            }
        }
    }

    // NOTE: class hierarchy is ugly, done to reduce number of modules for manual installation.
    // FUTURE TBD SQLiteDatabaseNDK class belongs in its own module.
    class SQLiteDatabaseNDK extends SQLiteAndroidDatabase {
        SQLiteConnection mydb;

        /**
         * Open a database.
         *
         * @param dbFile   The database File specification
         */
        @Override
        void open(File dbFile) throws Exception {
            this.open(dbFile, SQLiteOpenFlags.READWRITE | SQLiteOpenFlags.CREATE);
        }

        /**
         * Open a database.
         *
         * @param dbFile   The database File specification
         */
        @Override
        void open(File dbFile, int mode) throws Exception {
            mydb = connector.newSQLiteConnection(dbFile.getAbsolutePath(),mode);
        }

        /**
         * Close a database (in the current thread).
         */
        @Override
        void closeDatabaseNow() {
            try {
                if (mydb != null)
                    mydb.dispose();
            } catch (Exception ex) {
                FLog.e(TAG, "couldn't close database, ignoring", ex);
            }
        }

        /**
         * Ignore Android bug workaround for NDK version
         */
        @Override
        void bugWorkaround() { }

        /**
         * Executes a batch request and sends the results via cbc.
         *
         * @param queryarr   Array of query strings
         * @param queryParams Array of JSON query parameters
         * @param queryIDs   Array of query ids
         * @param cbc        Callback context from Cordova API
         */
        @Override
        void executeSqlBatch( String[] queryarr, ReadableArray[]  queryParams,
                              String[] queryIDs, CallbackContext cbc) {

            if (mydb == null) {
                // not allowed - can only happen if someone has closed (and possibly deleted) a database and then re-used the database
                cbc.error("database has been closed");
                return;
            }

            int len = queryarr.length;
            WritableArray batchResults = Arguments.createArray();

            for (int i = 0; i < len; i++) {
                String query_id = queryIDs[i];

                WritableMap queryResult = null;
                String errorMessage = "unknown";

                try {
                    String query = queryarr[i];

                    long lastTotal = mydb.getTotalChanges();
                    queryResult = this.executeSqlStatementNDK(query, queryParams != null ? queryParams[i] : null, cbc);
                    long newTotal = mydb.getTotalChanges();
                    long rowsAffected = newTotal - lastTotal;

                    queryResult.putDouble("rowsAffected", rowsAffected);
                    if (rowsAffected > 0) {
                        long insertId = mydb.getLastInsertRowid();
                        if (insertId > 0) {
                            queryResult.putDouble("insertId", insertId);
                        }
                    }
                } catch (Exception ex) {
                    errorMessage = ex.getMessage();
                    FLog.e(TAG, "SQLitePlugin.executeSql[Batch]() failed", ex);
                }

                try {
                    if (queryResult != null) {
                        WritableMap r = Arguments.createMap();
                        r.putString("qid", query_id);

                        r.putString("type", "success");
                        r.putMap("result", queryResult);

                        batchResults.pushMap(r);
                    } else {
                        WritableMap r = Arguments.createMap();
                        r.putString("qid", query_id);
                        r.putString("type", "error");

                        WritableMap er = Arguments.createMap();
                        er.putString("message", errorMessage);
                        r.putMap("result", er);

                        batchResults.pushMap(r);
                    }
                } catch (Exception ex) {
                    FLog.e(TAG, "SQLitePlugin.executeSql[Batch]() failed", ex);
                }
            }

            cbc.success(batchResults);
        }

        /**
         * Get rows results from query cursor.
         *
         * @return results in string form
         */
        private WritableMap executeSqlStatementNDK(String query, ReadableArray queryArgs,
                                                   CallbackContext cbc) throws Exception {
            WritableMap rowsResult = Arguments.createMap();

            boolean hasRows;

            SQLiteStatement myStatement = null;
            try {
                try {
                    myStatement = mydb.prepareStatement(query);
                    if (queryArgs != null) {
                        for (int i = 0; i < queryArgs.size(); ++i) {
                            ReadableType type = queryArgs.getType(i);
                            if (type == ReadableType.Number){
                                double tmp = queryArgs.getDouble(i);
                                if (tmp == (long) tmp) {
                                    myStatement.bindLong(i + 1, (long) tmp);
                                } else {
                                    myStatement.bindDouble(i + 1, tmp);
                                }
                            } else if (queryArgs.isNull(i)) {
                                myStatement.bindNull(i + 1);
                            } else {
                                myStatement.bindTextNativeString(i + 1, SQLitePluginConverter.getString(queryArgs,i,""));
                            }
                        }
                    }

                    hasRows = myStatement.step();
                } catch (Exception ex) {
                    FLog.e(TAG, "SQLitePlugin.executeSql[Batch]() failed", ex);
                    throw ex;
                }

                // If query result has rows
                if (hasRows) {
                    WritableArray rowsArrayResult = Arguments.createArray();
                    String key;
                    int colCount = myStatement.getColumnCount();

                    // Build up JSON result object for each row
                    do {
                        WritableMap row = Arguments.createMap();
                        for (int i = 0; i < colCount; ++i) {
                            key = myStatement.getColumnName(i);

                            switch (myStatement.getColumnType(i)) {
                                case SQLColumnType.NULL:
                                    row.putNull(key);
                                    break;

                                case SQLColumnType.REAL:
                                    row.putDouble(key, myStatement.getColumnDouble(i));
                                    break;

                                case SQLColumnType.INTEGER:
                                    row.putDouble(key, myStatement.getColumnLong(i));
                                    break;

                                case SQLColumnType.BLOB:
                                case SQLColumnType.TEXT:
                                default:
                                    row.putString(key, myStatement.getColumnTextNativeString(i));
                            }

                        }

                        rowsArrayResult.pushMap(row);
                    } while (myStatement.step());

                    rowsResult.putArray("rows", rowsArrayResult);
                }
            } finally {
                if (myStatement != null) {
                    myStatement.dispose();
                }
            }

            return rowsResult;
        }
    }

    private class DBRunner implements Runnable {
        final String dbname;
        private String assetFilename;
        private boolean oldImpl;
        private boolean androidLockWorkaround;
        final int openFlags;
        final BlockingQueue<DBQuery> q;
        final CallbackContext openCbc;

        SQLiteAndroidDatabase mydb;

        DBRunner(final String dbname, ReadableMap options, CallbackContext cbc) {
            this.dbname = dbname;
            int openFlags = SQLiteOpenFlags.CREATE | SQLiteOpenFlags.READWRITE;
            try {
                this.assetFilename = SQLitePluginConverter.getString(options,"assetFilename",null);
                if (this.assetFilename != null && this.assetFilename.length() > 0) {
                    boolean readOnly = SQLitePluginConverter.getBoolean(options,"readOnly",false);
                    openFlags = readOnly ? SQLiteOpenFlags.READONLY : openFlags;
                }
            } catch (Exception ex){
                FLog.e(TAG,"Error retrieving assetFilename or mode from options:",ex);
            }
            this.openFlags = openFlags;
            this.oldImpl = SQLitePluginConverter.getBoolean(options,"androidOldDatabaseImplementation",false);
            FLog.v(TAG, "Android db implementation: " + (oldImpl ? "OLD" : "sqlite4java (NDK)"));
            this.androidLockWorkaround = this.oldImpl && SQLitePluginConverter.getBoolean(options,"androidLockWorkaround",false);
            if (this.androidLockWorkaround)
                FLog.i(TAG, "Android db closing/locking workaround applied");

            this.q = new LinkedBlockingQueue<>();
            this.openCbc = cbc;
        }

        public void run() {
            try {
                this.mydb = openDatabase(dbname, this.assetFilename, this.openFlags, this.openCbc, this.oldImpl);
            } catch (Exception ex) {
                FLog.e(TAG, "Error opening database, stopping db thread", ex);
                if (this.openCbc != null) {
                    this.openCbc.error("Can't open database." + ex);
                }
                dbrmap.remove(dbname);
                return;
            }

            DBQuery dbq = null;

            try {
                dbq = q.take();

                while (!dbq.stop) {
                    mydb.executeSqlBatch(dbq.queries, dbq.queryParams, dbq.queryIDs, dbq.cbc);

                    // NOTE: androidLock[Bug]Workaround is not necessary and IGNORED for sqlite4java (NDK version).
                    if (this.androidLockWorkaround && dbq.queries.length == 1 && dbq.queries[0].equals("COMMIT"))
                        mydb.bugWorkaround();

                    dbq = q.take();
                }
            } catch (Exception ex) {
                FLog.e(TAG, "unexpected error", ex);
            }

            if (dbq != null && dbq.close) {
                try {
                    closeDatabaseNow(dbname);

                    dbrmap.remove(dbname); // (should) remove ourself

                    if (!dbq.delete) {
                        dbq.cbc.success("database deleted");
                    } else {
                        try {
                            boolean deleteResult = deleteDatabaseNow(dbname);
                            if (deleteResult) {
                                dbq.cbc.success("database deleted");
                            } else {
                                dbq.cbc.error("couldn't delete database");
                            }
                        } catch (Exception ex) {
                            FLog.e(TAG, "couldn't delete database", ex);
                            dbq.cbc.error("couldn't delete database: " + ex);
                        }
                    }
                } catch (Exception ex) {
                    FLog.e(TAG, "couldn't close database", ex);
                    if (dbq.cbc != null) {
                        dbq.cbc.error("couldn't close database: " + ex);
                    }
                }
            }
        }
    }

    private final class DBQuery {
        // XXX TODO replace with DBRunner action enum:
        final boolean stop;
        final boolean close;
        final boolean delete;
        final String[] queries;
        final String[] queryIDs;
        final ReadableArray[] queryParams;
        final CallbackContext cbc;

        DBQuery(String[] myqueries, String[] qids, ReadableArray[] params, CallbackContext c) {
            this.stop = false;
            this.close = false;
            this.delete = false;
            this.queries = myqueries;
            this.queryIDs = qids;
            this.queryParams = params;
            this.cbc = c;
        }

        DBQuery(boolean delete, CallbackContext cbc) {
            this.stop = true;
            this.close = true;
            this.delete = delete;
            this.queries = null;
            this.queryIDs = null;
            this.queryParams = null;
            this.cbc = cbc;
        }

        // signal the DBRunner thread to stop:
        DBQuery() {
            this.stop = true;
            this.close = false;
            this.delete = false;
            this.queries = null;
            this.queryIDs = null;
            this.queryParams = null;
            this.cbc = null;
        }
    }

    private enum Action {
        open,
        close,
        attach,
        delete,
        executeSqlBatch,
        backgroundExecuteSqlBatch,
        echoStringValue
    }
}

/* vim: set expandtab : */