/*******************************************************************************

    uMatrix - a browser extension to block requests.
    Copyright (C) 2016-2017 The uBlock Origin authors

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see {http://www.gnu.org/licenses/}.

    Home: https://github.com/gorhill/uBlock
*/

/* global indexedDB, IDBDatabase */

'use strict';

/******************************************************************************/

// The code below has been originally manually imported from:
// Commit: https://github.com/nikrolls/uBlock-Edge/commit/d1538ea9bea89d507219d3219592382eee306134
// Commit date: 29 October 2016
// Commit author: https://github.com/nikrolls
// Commit message: "Implement cacheStorage using IndexedDB"

// The original imported code has been subsequently modified as it was not
// compatible with Firefox.
// (a Promise thing, see https://github.com/dfahlander/Dexie.js/issues/317)
// Furthermore, code to migrate from browser.storage.local to vAPI.cacheStorage
// has been added, for seamless migration of cache-related entries into
// indexedDB.

// Imported from uBlock Origin project.

vAPI.cacheStorage = (function() {
    const STORAGE_NAME = 'uMatrixCacheStorage';
    var db;
    var pending = [];

    // prime the db so that it's ready asap for next access.
    getDb(noopfn);

    return { get, set, remove, clear, getBytesInUse };

    function get(input, callback) {
        if ( typeof callback !== 'function' ) { return; }
        if ( input === null ) {
            return getAllFromDb(callback);
        }
        var toRead, output = {};
        if ( typeof input === 'string' ) {
            toRead = [ input ];
        } else if ( Array.isArray(input) ) {
            toRead = input;
        } else /* if ( typeof input === 'object' ) */ {
            toRead = Object.keys(input);
            output = input;
        }
        return getFromDb(toRead, output, callback);
    }

    function set(input, callback) {
        putToDb(input, callback);
    }

    function remove(key, callback) {
        deleteFromDb(key, callback);
    }

    function clear(callback) {
        clearDb(callback);
    }

    function getBytesInUse(keys, callback) {
        // TODO: implement this
        callback(0);
    }

    function genericErrorHandler(error) {
        console.error('[uMatrix cacheStorage]', error);
    }

    function noopfn() {
    }

    function processPendings() {
        var cb;
        while ( (cb = pending.shift()) ) {
            cb(db);
        }
    }

    function getDb(callback) {
        if ( pending === undefined ) {
            return callback();
        }
        if ( pending.length !== 0 ) {
            return pending.push(callback);
        }
        if ( db instanceof IDBDatabase ) {
            return callback(db);
        }
        pending.push(callback);
        if ( pending.length !== 1 ) { return; }
        // This will fail in private browsing mode.
        var req = indexedDB.open(STORAGE_NAME, 1);
        req.onupgradeneeded = function(ev) {
            db = ev.target.result;
            db.onerror = genericErrorHandler;
            var table = db.createObjectStore(STORAGE_NAME, { keyPath: 'key' });
            table.createIndex('value', 'value', { unique: false });
        };
        req.onsuccess = function(ev) {
            db = ev.target.result;
            db.onerror = genericErrorHandler;
            processPendings();
        };
        req.onerror = function() {
            console.log(this.error);
            processPendings();
            pending = undefined;
        };
    }

    function getFromDb(keys, store, callback) {
        if ( typeof callback !== 'function' ) { return; }
        if ( keys.length === 0 ) { return callback(store); }
        var notfoundKeys = new Set(keys);
        var gotOne = function() {
            if ( typeof this.result === 'object' ) {
                store[this.result.key] = this.result.value;
                notfoundKeys.delete(this.result.key);
            }
        };
        getDb(function(db) {
            if ( !db ) { return callback(); }
            var transaction = db.transaction(STORAGE_NAME);
            transaction.oncomplete = transaction.onerror = function() {
                if ( notfoundKeys.size === 0 ) {
                    return callback(store);
                }
                maybeMigrate(Array.from(notfoundKeys), store, callback);
            };
            var table = transaction.objectStore(STORAGE_NAME);
            for ( var key of keys ) {
                var req = table.get(key);
                req.onsuccess = gotOne;
                req.onerror = noopfn;
            }
        });
    }

    // Migrate from storage API
    // TODO: removes once all users are migrated to the new cacheStorage.
    function maybeMigrate(keys, store, callback) {
        var toMigrate = new Set(),
            i = keys.length;
        while ( i-- ) {
            var key = keys[i];
            toMigrate.add(key);
            // If migrating a compiled list, also migrate the non-compiled
            // counterpart.
            if ( /^cache\/compiled\//.test(key) ) {
                toMigrate.add(key.replace('/compiled', ''));
            }
        }
        vAPI.storage.get(Array.from(toMigrate), function(bin) {
            if ( bin instanceof Object === false ) {
                return callback(store);
            }
            var migratedKeys = Object.keys(bin);
            if ( migratedKeys.length === 0 ) {
                return callback(store);
            }
            var i = migratedKeys.length;
            while ( i-- ) {
                var key = migratedKeys[i];
                store[key] = bin[key];
            }
            vAPI.storage.remove(migratedKeys);
            vAPI.cacheStorage.set(bin);
            callback(store);
        });
    }

    function getAllFromDb(callback) {
        if ( typeof callback !== 'function' ) {
            callback = noopfn;
        }
        getDb(function(db) {
            if ( !db ) { return callback(); }
            var output = {};
            var transaction = db.transaction(STORAGE_NAME);
            transaction.oncomplete = transaction.onerror = function() {
                callback(output);
            };
            var table = transaction.objectStore(STORAGE_NAME),
                req = table.openCursor();
            req.onsuccess = function(ev) {
                var cursor = ev.target.result;
                if ( !cursor ) { return; }
                output[cursor.key] = cursor.value;
                cursor.continue();
            };
        });
    }

    function putToDb(input, callback) {
        if ( typeof callback !== 'function' ) {
            callback = noopfn;
        }
        var keys = Object.keys(input);
        if ( keys.length === 0 ) { return callback(); }
        getDb(function(db) {
            if ( !db ) { return callback(); }
            var transaction = db.transaction(STORAGE_NAME, 'readwrite');
            transaction.oncomplete = transaction.onerror = callback;
            var table = transaction.objectStore(STORAGE_NAME),
                entry = {};
            for ( var key of keys ) {
                entry.key = key;
                entry.value = input[key];
                table.put(entry);
            }
        });
    }

    function deleteFromDb(input, callback) {
        if ( typeof callback !== 'function' ) {
            callback = noopfn;
        }
        var keys = Array.isArray(input) ? input.slice() : [ input ];
        if ( keys.length === 0 ) { return callback(); }
        getDb(function(db) {
            if ( !db ) { return callback(); }
            var transaction = db.transaction(STORAGE_NAME, 'readwrite');
            transaction.oncomplete = transaction.onerror = callback;
            var table = transaction.objectStore(STORAGE_NAME);
            for ( var key of keys ) {
                table.delete(key);
            }
        });
        // TODO: removes once all users are migrated to the new cacheStorage.
        vAPI.storage.remove(keys);
    }

    function clearDb(callback) {
        if ( typeof callback !== 'function' ) {
            callback = noopfn;
        }
        getDb(function(db) {
            if ( !db ) { return callback(); }
            var req = db.transaction(STORAGE_NAME, 'readwrite')
                        .objectStore(STORAGE_NAME)
                        .clear();
            req.onsuccess = req.onerror = callback;
        });
    }
}());

/******************************************************************************/