import LocalForage from './index';
import { createBlob, idb } from './util';
import { Deferred } from '../utils';

interface EncodedBlob {
  __local_forage_encoded_blob: boolean;
  data: string;
  type: string;
}

interface DBContext {
	// Running localForages sharing a database.
	forages: LocalForage[];
	// Shared database.
	db: IDBDatabase;
	// Database readiness (promise).
	dbReady: Promise<void>;
	// Deferred operations on the database.
	deferredOperations: Deferred<void>[];
}

// Some code originally from async_storage.js in
// [Gaia](https://github.com/mozilla-b2g/gaia).
const DETECT_BLOB_SUPPORT_STORE = 'local-forage-detect-blob-support';
export const dbContexts = new Map<string, DBContext>();

// Transaction Modes
export const READ_ONLY = 'readonly';
export const READ_WRITE = 'readwrite';

/**
 * Transform a binary string to an array buffer, because otherwise
 * weird stuff happens when you try to work with the binary string directly.
 * It is known.
 * From http://stackoverflow.com/questions/14967647/ (continues on next line)
 * encode-decode-image-with-base64-breaks-image (2013-04-21)
 */
function binStringToArrayBuffer(bin: string) {
  const length = bin.length;
  const buf = new ArrayBuffer(length);
  const arr = new Uint8Array(buf);
  for (let i = 0; i < length; i++) {
    arr[i] = bin.charCodeAt(i);
  }
  return buf;
}

function deferReadiness(dbInfo: DBInfo) {
  const dbContext = dbContexts.get(dbInfo.name)!;

  // Create a deferred object representing the current database operation.
  const deferredOperation = new Deferred();

  // Enqueue the deferred operation.
  dbContext.deferredOperations.push(deferredOperation);

  // Chain its promise to the database readiness.
  if (!dbContext.dbReady) {
    dbContext.dbReady = deferredOperation.promise;
  } else {
    dbContext.dbReady = dbContext.dbReady.then(() => {
      return deferredOperation.promise;
    });
  }
}

function advanceReadiness(dbInfo: DBInfo) {
  const dbContext = dbContexts.get(dbInfo.name)!;

  // Dequeue a deferred operation.
  const deferredOperation = dbContext.deferredOperations.pop();

  // Resolve its promise (which is part of the database readiness
  // chain of promises).
  if (deferredOperation) {
    deferredOperation.resolve();
  }
}

function rejectReadiness(dbInfo: DBInfo, err: Error) {
  const dbContext = dbContexts.get(dbInfo.name)!;

  // Dequeue a deferred operation.
  const deferredOperation = dbContext.deferredOperations.pop();

  // Reject its promise (which is part of the database readiness
  // chain of promises).
  if (deferredOperation) {
    deferredOperation.reject(err);
  }
}

export function getConnection(dbInfo: DBInfo, upgradeNeeded: boolean) {
	return new Promise<IDBDatabase>((resolve, reject) => {

		if (dbInfo.db) {
			if (upgradeNeeded) {
				deferReadiness(dbInfo);
				dbInfo.db.close();
			} else {
				return resolve(dbInfo.db);
			}
		}

		const openreq = upgradeNeeded
			?	idb.open(dbInfo.name, dbInfo.version)
			: idb.open(dbInfo.name);

		if (upgradeNeeded) {
			openreq.onupgradeneeded = function (e) {
				const db = openreq.result;
				try {
					db.createObjectStore(dbInfo.storeName);
					if (e.oldVersion <= 1) {
						// Added when support for blob shims was added
						db.createObjectStore(DETECT_BLOB_SUPPORT_STORE);
					}
				} catch (ex) {
					if (ex.name === 'ConstraintError') {
						console.warn(`The database “${ dbInfo.name }”` +
							` has been upgraded from version ${ e.oldVersion
							} to version ${ e.newVersion
							}, but the storage “${ dbInfo.storeName }” already exists.`);
					} else {
						throw ex;
					}
				}
			};
		}

		openreq.onerror = function (e) {
			e.preventDefault();
			reject(openreq.error);
		};

		openreq.onsuccess = function () {
			resolve(openreq.result);
			advanceReadiness(dbInfo);
		};
	});
}

export function isUpgradeNeeded(dbInfo: DBInfo, defaultVersion: number) {
	if (!dbInfo.db) {
		return true;
	}

	const isNewStore = !dbInfo.db.objectStoreNames.contains(dbInfo.storeName);
	const isDowngrade = dbInfo.version < dbInfo.db.version;
	const isUpgrade = dbInfo.version > dbInfo.db.version;

	if (isDowngrade) {
		// If the version is not the default one
		// then warn for impossible downgrade.
		if (dbInfo.version !== defaultVersion) {
			console.warn(`The database “${ dbInfo.name }”` +
				` can’t be downgraded from version ${ dbInfo.db.version
				} to version ${ dbInfo.version }.`);
		}
		// Align the versions to prevent errors.
		dbInfo.version = dbInfo.db.version;
	}

	if (isUpgrade || isNewStore) {
		// If the store is new then increment the version (if needed).
		// This will trigger an “upgradeneeded” event which is required
		// for creating a store.
		if (isNewStore) {
			const incVersion = dbInfo.db.version + 1;
			if (incVersion > dbInfo.version) {
				dbInfo.version = incVersion;
			}
		}

		return true;
	}

	return false;
}

// encode a blob for indexeddb engines that don’t support blobs
export function encodeBlob(blob: Blob) {
  return new Promise<EncodedBlob>((resolve, reject) => {
    const reader = new FileReader();
    reader.onerror = reject;
    reader.onloadend = () => {
      const base64 = btoa(reader.result || '');
      resolve({
        __local_forage_encoded_blob: true,
        data: base64,
        type: blob.type,
      });
    };
    reader.readAsBinaryString(blob);
  });
}

// decode an encoded blob
export function decodeBlob(encodedBlob: EncodedBlob) {
  const arrayBuff = binStringToArrayBuffer(atob(encodedBlob.data));
  return createBlob([arrayBuff], {
    type: encodedBlob.type,
  });
}

// is this one of our fancy encoded blobs?
export function isEncodedBlob(value: any): value is EncodedBlob {
  return value && value.__local_forage_encoded_blob;
}

// Try to establish a new db connection to replace the
// current one which is broken (i.e. experiencing
// InvalidStateError while creating a transaction).
async function tryReconnect(dbInfo: DBInfo) {
	deferReadiness(dbInfo);

	const dbContext = dbContexts.get(dbInfo.name)!;
	const forages = dbContext.forages;

	for (const forage of forages) {
		if (forage.dbInfo.db) {
			forage.dbInfo.db.close();
			forage.dbInfo.db = undefined;
		}
	}

	try {
		const db = await getConnection(dbInfo, false);
		for (const forage of forages) {
			forage.dbInfo.db = db;
		}
	} catch (err) {
		rejectReadiness(dbInfo, err);
		throw err;
	}
}

// FF doesn’t like Promises (micro-tasks) and IDDB store operations,
// so we have to do it with callbacks
export async function createTransaction(dbIn
 'readonly' | 'readwrite' | 'versionchange') {
	try {
    return dbInfo.db!.transaction(dbInfo.storeName, mode);
	} catch (err) {
		if (!dbInfo.db || err.name === 'InvalidStateError') {
			await tryReconnect(dbInfo);
      return dbInfo.db!.transaction(dbInfo.storeName, mode);
		}
		throw err;
	}
}

fo: DBInfo, mode: