(function() {
    'use strict';

    var dummyStorage = {};

    // Config the localStorage backend, using options set in the config.
    function _initStorage(options) {
        var self = this;

        var dbInfo = {};
        if (options) {
            for (var i in options) {
                dbInfo[i] = options[i];
            }
        }

        dummyStorage[dbInfo.name] = dbInfo.db = {};

        self._dbInfo = dbInfo;
        return Promise.resolve();
    }

    var SERIALIZED_MARKER = '__lfsc__:';
    var SERIALIZED_MARKER_LENGTH = SERIALIZED_MARKER.length;

    // OMG the serializations!
    var TYPE_ARRAYBUFFER = 'arbf';
    var TYPE_BLOB = 'blob';
    var TYPE_INT8ARRAY = 'si08';
    var TYPE_UINT8ARRAY = 'ui08';
    var TYPE_UINT8CLAMPEDARRAY = 'uic8';
    var TYPE_INT16ARRAY = 'si16';
    var TYPE_INT32ARRAY = 'si32';
    var TYPE_UINT16ARRAY = 'ur16';
    var TYPE_UINT32ARRAY = 'ui32';
    var TYPE_FLOAT32ARRAY = 'fl32';
    var TYPE_FLOAT64ARRAY = 'fl64';
    var TYPE_SERIALIZED_MARKER_LENGTH =
        SERIALIZED_MARKER_LENGTH + TYPE_ARRAYBUFFER.length;

    function clear(callback) {
        var self = this;
        var promise = new Promise(function(resolve, reject) {
            self
                .ready()
                .then(function() {
                    var db = self._dbInfo.db;

                    for (var key in db) {
                        if (db.hasOwnProperty(key)) {
                            delete db[key];
                            // db[key] = undefined;
                        }
                    }

                    resolve();
                })
                .catch(reject);
        });

        executeCallback(promise, callback);
        return promise;
    }

    function getItem(key, callback) {
        var self = this;

        // Cast the key to a string, as that's all we can set as a key.
        if (typeof key !== 'string') {
            window.console.warn(
                key + ' used as a key, but it is not a string.'
            );
            key = String(key);
        }

        var promise = new Promise(function(resolve, reject) {
            self
                .ready()
                .then(function() {
                    try {
                        var db = self._dbInfo.db;
                        var result = db[key];

                        if (result) {
                            result = _deserialize(result);
                        }

                        resolve(result);
                    } catch (e) {
                        reject(e);
                    }
                })
                .catch(reject);
        });

        executeCallback(promise, callback);
        return promise;
    }

    function iterate(callback) {
        var self = this;

        var promise = new Promise(function(resolve, reject) {
            self
                .ready()
                .then(function() {
                    try {
                        var db = self._dbInfo.db;

                        for (var key in db) {
                            var result = db[key];

                            if (result) {
                                result = _deserialize(result);
                            }

                            callback(result, key);
                        }

                        resolve();
                    } catch (e) {
                        reject(e);
                    }
                })
                .catch(reject);
        });

        executeCallback(promise, callback);
        return promise;
    }

    function key(n, callback) {
        var self = this;
        var promise = new Promise(function(resolve, reject) {
            self
                .ready()
                .then(function() {
                    var db = self._dbInfo.db;
                    var result = null;
                    var index = 0;

                    for (var key in db) {
                        if (db.hasOwnProperty(key) && db[key] !== undefined) {
                            if (n === index) {
                                result = key;
                                break;
                            }
                            index++;
                        }
                    }

                    resolve(result);
                })
                .catch(reject);
        });

        executeCallback(promise, callback);
        return promise;
    }

    function keys(callback) {
        var self = this;
        var promise = new Promise(function(resolve, reject) {
            self
                .ready()
                .then(function() {
                    var db = self._dbInfo.db;
                    var keys = [];

                    for (var key in db) {
                        if (db.hasOwnProperty(key)) {
                            keys.push(key);
                        }
                    }

                    resolve(keys);
                })
                .catch(reject);
        });

        executeCallback(promise, callback);
        return promise;
    }

    function length(callback) {
        var self = this;
        var promise = new Promise(function(resolve, reject) {
            self
                .keys()
                .then(function(keys) {
                    resolve(keys.length);
                })
                .catch(reject);
        });

        executeCallback(promise, callback);
        return promise;
    }

    function removeItem(key, callback) {
        var self = this;

        // Cast the key to a string, as that's all we can set as a key.
        if (typeof key !== 'string') {
            window.console.warn(
                key + ' used as a key, but it is not a string.'
            );
            key = String(key);
        }

        var promise = new Promise(function(resolve, reject) {
            self
                .ready()
                .then(function() {
                    var db = self._dbInfo.db;
                    if (db.hasOwnProperty(key)) {
                        delete db[key];
                        // db[key] = undefined;
                    }

                    resolve();
                })
                .catch(reject);
        });

        executeCallback(promise, callback);
        return promise;
    }

    function setItem(key, value, callback) {
        var self = this;

        // Cast the key to a string, as that's all we can set as a key.
        if (typeof key !== 'string') {
            window.console.warn(
                key + ' used as a key, but it is not a string.'
            );
            key = String(key);
        }

        var promise = new Promise(function(resolve, reject) {
            self
                .ready()
                .then(function() {
                    // Convert undefined values to null.
                    // https://github.com/mozilla/localForage/pull/42
                    if (value === undefined) {
                        value = null;
                    }

                    // Save the original value to pass to the callback.
                    var originalValue = value;

                    _serialize(value, function(value, error) {
                        if (error) {
                            reject(error);
                        } else {
                            try {
                                var db = self._dbInfo.db;
                                db[key] = value;
                                resolve(originalValue);
                            } catch (e) {
                                reject(e);
                            }
                        }
                    });
                })
                .catch(reject);
        });

        executeCallback(promise, callback);
        return promise;
    }

    // _serialize just like in LocalStorage
    function _serialize(value, callback) {
        var valueString = '';
        if (value) {
            valueString = value.toString();
        }

        // Cannot use `value instanceof ArrayBuffer` or such here, as these
        // checks fail when running the tests using casper.js...
        //
        // TODO: See why those tests fail and use a better solution.
        if (
            value &&
            (value.toString() === '[object ArrayBuffer]' ||
                (value.buffer &&
                    value.buffer.toString() === '[object ArrayBuffer]'))
        ) {
            // Convert binary arrays to a string and prefix the string with
            // a special marker.
            var buffer;
            var marker = SERIALIZED_MARKER;

            if (value instanceof ArrayBuffer) {
                buffer = value;
                marker += TYPE_ARRAYBUFFER;
            } else {
                buffer = value.buffer;

                if (valueString === '[object Int8Array]') {
                    marker += TYPE_INT8ARRAY;
                } else if (valueString === '[object Uint8Array]') {
                    marker += TYPE_UINT8ARRAY;
                } else if (valueString === '[object Uint8ClampedArray]') {
                    marker += TYPE_UINT8CLAMPEDARRAY;
                } else if (valueString === '[object Int16Array]') {
                    marker += TYPE_INT16ARRAY;
                } else if (valueString === '[object Uint16Array]') {
                    marker += TYPE_UINT16ARRAY;
                } else if (valueString === '[object Int32Array]') {
                    marker += TYPE_INT32ARRAY;
                } else if (valueString === '[object Uint32Array]') {
                    marker += TYPE_UINT32ARRAY;
                } else if (valueString === '[object Float32Array]') {
                    marker += TYPE_FLOAT32ARRAY;
                } else if (valueString === '[object Float64Array]') {
                    marker += TYPE_FLOAT64ARRAY;
                } else {
                    callback(new Error('Failed to get type for BinaryArray'));
                }
            }

            callback(marker + _bufferToString(buffer));
        } else if (valueString === '[object Blob]') {
            // Conver the blob to a binaryArray and then to a string.
            var fileReader = new FileReader();

            fileReader.onload = function() {
                var str = _bufferToString(this.result);

                callback(SERIALIZED_MARKER + TYPE_BLOB + str);
            };

            fileReader.readAsArrayBuffer(value);
        } else {
            try {
                callback(JSON.stringify(value));
            } catch (e) {
                window.console.error(
                    "Couldn't convert value into a JSON " + 'string: ',
                    value
                );

                callback(e);
            }
        }
    }

    // _deserialize just like in LocalStorage
    function _deserialize(value) {
        // If we haven't marked this string as being specially serialized (i.e.
        // something other than serialized JSON), we can just return it and be
        // done with it.
        if (
            value.substring(0, SERIALIZED_MARKER_LENGTH) !== SERIALIZED_MARKER
        ) {
            return JSON.parse(value);
        }

        // The following code deals with deserializing some kind of Blob or
        // TypedArray. First we separate out the type of data we're dealing
        // with from the data itself.
        var serializedString = value.substring(TYPE_SERIALIZED_MARKER_LENGTH);
        var type = value.substring(
            SERIALIZED_MARKER_LENGTH,
            TYPE_SERIALIZED_MARKER_LENGTH
        );

        // Fill the string into a ArrayBuffer.
        // 2 bytes for each char.
        var buffer = new ArrayBuffer(serializedString.length * 2);
        var bufferView = new Uint16Array(buffer);
        for (var i = serializedString.length - 1; i >= 0; i--) {
            bufferView[i] = serializedString.charCodeAt(i);
        }

        // Return the right type based on the code/type set during
        // serialization.
        switch (type) {
            case TYPE_ARRAYBUFFER:
                return buffer;
            case TYPE_BLOB:
                return new Blob([buffer]);
            case TYPE_INT8ARRAY:
                return new Int8Array(buffer);
            case TYPE_UINT8ARRAY:
                return new Uint8Array(buffer);
            case TYPE_UINT8CLAMPEDARRAY:
                return new Uint8ClampedArray(buffer);
            case TYPE_INT16ARRAY:
                return new Int16Array(buffer);
            case TYPE_UINT16ARRAY:
                return new Uint16Array(buffer);
            case TYPE_INT32ARRAY:
                return new Int32Array(buffer);
            case TYPE_UINT32ARRAY:
                return new Uint32Array(buffer);
            case TYPE_FLOAT32ARRAY:
                return new Float32Array(buffer);
            case TYPE_FLOAT64ARRAY:
                return new Float64Array(buffer);
            default:
                throw new Error('Unkown type: ' + type);
        }
    }

    // _bufferToString just like in LocalStorage
    function _bufferToString(buffer) {
        var str = '';
        var uint16Array = new Uint16Array(buffer);

        try {
            str = String.fromCharCode.apply(null, uint16Array);
        } catch (e) {
            // This is a fallback implementation in case the first one does
            // not work. This is required to get the phantomjs passing...
            for (var i = 0; i < uint16Array.length; i++) {
                str += String.fromCharCode(uint16Array[i]);
            }
        }

        return str;
    }

    function executeCallback(promise, callback) {
        if (callback) {
            promise.then(
                function(result) {
                    callback(null, result);
                },
                function(error) {
                    callback(error);
                }
            );
        }
    }

    var dummyStorageDriver = {
        _driver: 'dummyStorageDriver',
        _initStorage: _initStorage,
        // _supports: function() { return true; }
        iterate: iterate,
        getItem: getItem,
        setItem: setItem,
        removeItem: removeItem,
        clear: clear,
        length: length,
        key: key,
        keys: keys
    };

    if (typeof define === 'function' && define.amd) {
        define('dummyStorageDriver', function() {
            return dummyStorageDriver;
        });
    } else if (
        typeof module !== 'undefined' &&
        module.exports &&
        typeof require !== 'undefined'
    ) {
        module.exports = dummyStorageDriver;
    } else {
        this.dummyStorageDriver = dummyStorageDriver;
    }
}.call(window));
