import EventEmitter from "event-emitter";
// import path from "path";
import {extend, defer} from "./utils/core";
import Url from "./utils/url";
import Path from "./utils/path";
import Spine from "./spine";
import Locations from "./locations";
import Container from "./container";
import Packaging from "./packaging";
import Navigation from "./navigation";
import Resources from "./resources";
import PageList from "./pagelist";
import Rendition from "./rendition";
import Archive from "./archive";
import request from "./utils/request";
import EpubCFI from "./epubcfi";
import { EVENTS } from "./utils/constants";

const CONTAINER_PATH = "META-INF/container.xml";
const EPUBJS_VERSION = "0.3";

/**
 * Creates a new Book
 * @class
 * @param {string} url
 * @param {object} options
 * @param {method} options.requestMethod a request function to use instead of the default
 * @param {boolean} [options.requestCredentials=undefined] send the xhr request withCredentials
 * @param {object} [options.requestHeaders=undefined] send the xhr request headers
 * @param {string} [options.encoding=binary] optional to pass 'binary' or base64' for archived Epubs
 * @param {string} [options.replacements=none] use base64, blobUrl, or none for replacing assets in archived Epubs
 * @returns {Book}
 * @example new Book("/path/to/book.epub", {})
 * @example new Book({ replacements: "blobUrl" })
 */
class Book {
	constructor(url, options) {
		// Allow passing just options to the Book
		if (typeof(options) === "undefined"
			&& typeof(url) === "object") {
			options = url;
			url = undefined;
		}

		this.settings = extend(this.settings || {}, {
			requestMethod: undefined,
			requestCredentials: undefined,
			requestHeaders: undefined,
			encoding: undefined,
			replacements: undefined,
			canonical: undefined
		});

		extend(this.settings, options);


		// Promises
		this.opening = new defer();
		/**
		 * @property {promise} opened returns after the book is loaded
		 */
		this.opened = this.opening.promise;
		this.isOpen = false;

		this.loading = {
			manifest: new defer(),
			spine: new defer(),
			metadata: new defer(),
			cover: new defer(),
			navigation: new defer(),
			pageList: new defer(),
			resources: new defer()
		};

		this.loaded = {
			manifest: this.loading.manifest.promise,
			spine: this.loading.spine.promise,
			metadata: this.loading.metadata.promise,
			cover: this.loading.cover.promise,
			navigation: this.loading.navigation.promise,
			pageList: this.loading.pageList.promise,
			resources: this.loading.resources.promise
		};

		// this.ready = RSVP.hash(this.loaded);
		/**
		 * @property {promise} ready returns after the book is loaded and parsed
		 * @private
		 */
		this.ready = Promise.all([
			this.loaded.manifest,
			this.loaded.spine,
			this.loaded.metadata,
			this.loaded.cover,
			this.loaded.navigation,
			this.loaded.resources
		]);


		// Queue for methods used before opening
		this.isRendered = false;
		// this._q = queue(this);

		/**
		 * @property {method} request
		 * @private
		 */
		this.request = this.settings.requestMethod || request;

		/**
		 * @property {Spine} spine
		 */
		this.spine = new Spine();

		/**
		 * @property {Locations} locations
		 */
		this.locations = new Locations(this.spine, this.load.bind(this));

		/**
		 * @property {Navigation} navigation
		 */
		this.navigation = undefined;

		/**
		 * @property {PageList} pagelist
		 */
		this.pageList = undefined;

		/**
		 * @property {Url} url
		 * @private
		 */
		this.url = undefined;

		/**
		 * @property {Path} path
		 * @private
		 */
		this.path = undefined;

		/**
		 * @property {boolean} archived
		 * @private
		 */
		this.archived = false;

		/**
		 * @property {Archive} archive
		 * @private
		 */
		this.archive = undefined;

		/**
		 * @property {Resources} resources
		 * @private
		 */
		this.resources = undefined;

		/**
		 * @property {Rendition} rendition
		 * @private
		 */
		this.rendition = undefined;

		this.container = undefined;
		this.packaging = undefined;
		this.toc = undefined;

		if(url) {
			this.open(url).catch((error) => {
				var err = new Error("Cannot load book at "+ url );
				this.emit(EVENTS.BOOK.OPEN_FAILED, err);
			});
		}
	}

	/**
	 * Open a epub or url
	 * @param {string} input URL, Path or ArrayBuffer
	 * @param {string} [what] to force opening
	 * @returns {Promise} of when the book has been loaded
	 * @example book.open("/path/to/book.epub")
	 */
	open(input, what) {
		var opening;
		var type = what || this.determineType(input);

		if (type === "binary") {
			this.archived = true;
			this.url = new Url("/", "");
			opening = this.openEpub(input);
		} else if (type === "base64") {
			this.archived = true;
			this.url = new Url("/", "");
			opening = this.openEpub(input, type);
		} else if (type === "epub") {
			this.archived = true;
			this.url = new Url("/", "");
			opening = this.request(input, "binary")
				.then(this.openEpub.bind(this));
		} else if(type == "opf") {
			this.url = new Url(input);
			opening = this.openPackaging(this.url.Path.toString());
		} else if(type == "json") {
			this.url = new Url(input);
			opening = this.openManifest(this.url.Path.toString());
		} else {
			this.url = new Url(input);
			opening = this.openContainer(CONTAINER_PATH)
				.then(this.openPackaging.bind(this));
		}

		return opening;
	}

	/**
	 * Open an archived epub
	 * @private
	 * @param  {binary} data
	 * @param  {[string]} encoding
	 * @return {Promise}
	 */
	openEpub(data, encoding) {
		return this.unarchive(data, encoding || this.settings.encoding)
			.then(() => {
				return this.openContainer(CONTAINER_PATH);
			})
			.then((packagePath) => {
				return this.openPackaging(packagePath);
			});
	}

	/**
	 * Open the epub container
	 * @private
	 * @param  {string} url
	 * @return {string} packagePath
	 */
	openContainer(url) {
		return this.load(url)
			.then((xml) => {
				this.container = new Container(xml);
				return this.resolve(this.container.packagePath);
			});
	}

	/**
	 * Open the Open Packaging Format Xml
	 * @private
	 * @param  {string} url
	 * @return {Promise}
	 */
	openPackaging(url) {
		this.path = new Path(url);
		return this.load(url)
			.then((xml) => {
				this.packaging = new Packaging(xml);
				return this.unpack(this.packaging);
			});
	}

	/**
	 * Open the manifest JSON
	 * @private
	 * @param  {string} url
	 * @return {Promise}
	 */
	openManifest(url) {
		this.path = new Path(url);
		return this.load(url)
			.then((json) => {
				this.packaging = new Packaging();
				this.packaging.load(json);
				return this.unpack(this.packaging);
			});
	}

	/**
	 * Load a resource from the Book
	 * @param  {string} path path to the resource to load
	 * @return {Promise}     returns a promise with the requested resource
	 */
	load(path) {
		var resolved;

		if(this.archived) {
			resolved = this.resolve(path);
			return this.archive.request(resolved);
		} else {
			resolved = this.resolve(path);
			return this.request(resolved, null, this.settings.requestCredentials, this.settings.requestHeaders);
		}
	}

	/**
	 * Resolve a path to it's absolute position in the Book
	 * @param  {string} path
	 * @param  {[boolean]} absolute force resolving the full URL
	 * @return {string}          the resolved path string
	 */
	resolve(path, absolute) {
		if (!path) {
			return;
		}
		var resolved = path;
		var isAbsolute = (path.indexOf("://") > -1);

		if (isAbsolute) {
			return path;
		}

		if (this.path) {
			resolved = this.path.resolve(path);
		}

		if(absolute != false && this.url) {
			resolved = this.url.resolve(resolved);
		}

		return resolved;
	}

	/**
	 * Get a canonical link to a path
	 * @param  {string} path
	 * @return {string} the canonical path string
	 */
	canonical(path) {
		var url = path;

		if (!path) {
			return "";
		}

		if (this.settings.canonical) {
			url = this.settings.canonical(path);
		} else {
			url = this.resolve(path, true);
		}

		return url;
	}

	/**
	 * Determine the type of they input passed to open
	 * @private
	 * @param  {string} input
	 * @return {string}  binary | directory | epub | opf
	 */
	determineType(input) {
		var url;
		var path;
		var extension;

		if (this.settings.encoding === "base64") {
			return "base64";
		}

		if(typeof(input) != "string") {
			return "binary";
		}

		url = new Url(input);
		path = url.path();
		extension = path.extension;

		if (!extension) {
			return "directory";
		}

		if(extension === "epub"){
			return "epub";
		}

		if(extension === "opf"){
			return "opf";
		}

		if(extension === "json"){
			return "json";
		}
	}


	/**
	 * unpack the contents of the Books packageXml
	 * @private
	 * @param {document} packageXml XML Document
	 */
	unpack(opf) {
		this.package = opf;

		this.spine.unpack(this.package, this.resolve.bind(this), this.canonical.bind(this));

		this.resources = new Resources(this.package.manifest, {
			archive: this.archive,
			resolver: this.resolve.bind(this),
			request: this.request.bind(this),
			replacements: this.settings.replacements || (this.archived ? "blobUrl" : "base64")
		});

		this.loadNavigation(this.package).then(() => {
			this.toc = this.navigation.toc;
			this.loading.navigation.resolve(this.navigation);
		});

		if (this.package.coverPath) {
			this.cover = this.resolve(this.package.coverPath);
		}
		// Resolve promises
		this.loading.manifest.resolve(this.package.manifest);
		this.loading.metadata.resolve(this.package.metadata);
		this.loading.spine.resolve(this.spine);
		this.loading.cover.resolve(this.cover);
		this.loading.resources.resolve(this.resources);
		this.loading.pageList.resolve(this.pageList);

		this.isOpen = true;

		if(this.archived || this.settings.replacements && this.settings.replacements != "none") {
			this.replacements().then(() => {
				this.opening.resolve(this);
			})
			.catch((err) => {
				console.error(err);
			});
		} else {
			// Resolve book opened promise
			this.opening.resolve(this);
		}

	}

	/**
	 * Load Navigation and PageList from package
	 * @private
	 * @param {document} opf XML Document
	 */
	loadNavigation(opf) {
		let navPath = opf.navPath || opf.ncxPath;
		let toc = opf.toc;

		// From json manifest
		if (toc) {
			return new Promise((resolve, reject) => {
				this.navigation = new Navigation(toc);

				if (opf.pageList) {
					this.pageList = new PageList(opf.pageList); // TODO: handle page lists from Manifest
				}

				resolve(this.navigation);
			});
		}

		if (!navPath) {
			return new Promise((resolve, reject) => {
				this.navigation = new Navigation();
				this.pageList = new PageList();

				resolve(this.navigation);
			});
		}

		return this.load(navPath, "xml")
			.then((xml) => {
				this.navigation = new Navigation(xml);
				this.pageList = new PageList(xml);
				return this.navigation;
			});
	}

	/**
	 * Alias for book.spine.get
	 * @param {string} target
	 */
	section(target) {
		return this.spine.get(target);
	}

	/**
	 * Sugar to render a book
	 * @param  {element} element element to add the views to
	 * @param  {[object]} options
	 * @return {Rendition}
	 */
	renderTo(element, options) {
		// var renderMethod = (options && options.method) ?
		//     options.method :
		//     "single";

		this.rendition = new Rendition(this, options);
		this.rendition.attachTo(element);

		return this.rendition;
	}

	/**
	 * Set if request should use withCredentials
	 * @param {boolean} credentials
	 */
	setRequestCredentials(credentials) {
		this.settings.requestCredentials = credentials;
	}

	/**
	 * Set headers request should use
	 * @param {object} headers
	 */
	setRequestHeaders(headers) {
		this.settings.requestHeaders = headers;
	}

	/**
	 * Unarchive a zipped epub
	 * @private
	 * @param  {binary} input epub data
	 * @param  {[string]} encoding
	 * @return {Archive}
	 */
	unarchive(input, encoding) {
		this.archive = new Archive();
		return this.archive.open(input, encoding);
	}

	/**
	 * Get the cover url
	 * @return {string} coverUrl
	 */
	coverUrl() {
		var retrieved = this.loaded.cover.
			then((url) => {
				if(this.archived) {
					// return this.archive.createUrl(this.cover);
					return this.resources.get(this.cover);
				}else{
					return this.cover;
				}
			});



		return retrieved;
	}

	/**
	 * load replacement urls
	 * @private
	 * @return {Promise} completed loading urls
	 */
	replacements() {
		this.spine.hooks.serialize.register((output, section) => {
			section.output = this.resources.substitute(output, section.url);
		});

		return this.resources.replacements().
			then(() => {
				return this.resources.replaceCss();
			});
	}

	/**
	 * Find a DOM Range for a given CFI Range
	 * @param  {EpubCFI} cfiRange a epub cfi range
	 * @return {Range}
	 */
	getRange(cfiRange) {
		var cfi = new EpubCFI(cfiRange);
		var item = this.spine.get(cfi.spinePos);
		var _request = this.load.bind(this);
		if (!item) {
			return new Promise((resolve, reject) => {
				reject("CFI could not be found");
			});
		}
		return item.load(_request).then(function (contents) {
			var range = cfi.toRange(item.document);
			return range;
		});
	}

	/**
	 * Generates the Book Key using the identifer in the manifest or other string provided
	 * @param  {[string]} identifier to use instead of metadata identifier
	 * @return {string} key
	 */
	key(identifier) {
		var ident = identifier || this.package.metadata.identifier || this.url.filename;
		return `epubjs:${EPUBJS_VERSION}:${ident}`;
	}

	destroy() {
		this.opened = undefined;
		this.loading = undefined;
		this.loaded = undefined;
		this.ready = undefined;

		this.isOpen = false;
		this.isRendered = false;

		this.spine && this.spine.destroy();
		this.locations && this.locations.destroy();
		this.pageList && this.pageList.destroy();
		this.archive && this.archive.destroy();
		this.resources && this.resources.destroy();
		this.container && this.container.destroy();
		this.packaging && this.packaging.destroy();
		this.rendition && this.rendition.destroy();

		this.spine = undefined;
		this.locations = undefined;
		this.pageList = undefined;
		this.archive = undefined;
		this.resources = undefined;
		this.container = undefined;
		this.packaging = undefined;
		this.rendition = undefined;

		this.navigation = undefined;
		this.url = undefined;
		this.path = undefined;
		this.archived = false;
		this.toc = undefined;
	}

}

//-- Enable binding events to book
EventEmitter(Book.prototype);

export default Book;
