/*
    ***** BEGIN LICENSE BLOCK *****
    
    Copyright © 2015 Center for History and New Media
                     George Mason University, Fairfax, Virginia, USA
                     http://zotero.org
    
    This file is part of Zotero.
    
    Zotero is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.
    
    Zotero 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 Affero General Public License for more details.
    
    You should have received a copy of the GNU Affero General Public License
    along with Zotero.  If not, see <http://www.gnu.org/licenses/>.
    
    ***** END LICENSE BLOCK *****
*/

if (!Zotero.Sync.Data) {
	Zotero.Sync.Data = {};
}

Zotero.Sync.Data.FullTextEngine = function (options) {
	if (options.apiClient == undefined) {
		throw new Error("options.apiClient not set");
	}
	if (options.libraryID == undefined) {
		throw new Error("options.libraryID not set");
	}
	
	this.MAX_BATCH_SIZE = 500000;
	this.MAX_BATCH_ITEMS = 10;
	
	this.apiClient = options.apiClient;
	this.libraryID = options.libraryID;
	this.library = Zotero.Libraries.get(options.libraryID);
	this.setStatus = options.setStatus || function () {};
	this.onError = options.onError || function (e) {};
	this.stopOnError = options.stopOnError;
	this._stopping = false;
	this.failed = false;
}

Zotero.Sync.Data.FullTextEngine.prototype.start = async function () {
	Zotero.debug("Starting full-text sync for " + this.library.name);
	
	// Get last full-text version in settings
	var libraryVersion = await Zotero.FullText.getLibraryVersion(this.libraryID);
	// If main library version has changed, check to see if there's no full-text content
	if (this.library.libraryVersion > libraryVersion) {
		await this._download(libraryVersion);
	}
	else {
		Zotero.debug("Library version hasn't changed -- skipping full-text download");
	}
	
	this._stopCheck();
	
	await this._upload();
}


Zotero.Sync.Data.FullTextEngine.prototype._download = async function (libraryVersion) {
	Zotero.debug("Downloading full-text content for " + this.library.name);
	
	// Get changed with ?since
	var results = await this.apiClient.getFullTextVersions(
		this.library.libraryType,
		this.library.libraryTypeID,
		libraryVersion
	);
	
	// Go through, checking local version against returned version
	var keys = [];
	for (let key in results.versions) {
		let id = Zotero.Items.getIDFromLibraryAndKey(this.libraryID, key);
		if (!id) {
			Zotero.debug(`Skipping full-text for missing item ${this.library.name}`);
			continue;
		}
		
		// Skip full text that's already up-to-date, which could happen due to a full sync or
		// interrupted sync
		let version = await Zotero.Fulltext.getItemVersion(id);
		if (version == results.versions[key]) {
			Zotero.debug(`Skipping up-to-date full text for ${this.library.name}`);
			continue;
		}
		keys.push(key);
	}
	
	// Process batches of 8 keys concurrently
	await Zotero.Utilities.Internal.forEachChunkAsync(keys, 8, async (keys) => {
		await Promise.all(keys.map(async (key) => {
			this._stopCheck();
			let results = await this.apiClient.getFullTextForItem(
				this.library.libraryType, this.library.libraryTypeID, key
			);
			this._stopCheck();
			if (!results) return;
			await Zotero.Fulltext.setItemContent(
				this.libraryID, key, results.data, results.version
			);
		}));
	});
	
	await Zotero.FullText.setLibraryVersion(this.libraryID, results.libraryVersion);
};


Zotero.Sync.Data.FullTextEngine.prototype._upload = async function () {
	if (!this.library.editable) return;
	
	Zotero.debug("Uploading full-text content for " + this.library.name);
	
	var libraryVersion = this.library.libraryVersion;
	var props = ['key', 'content', 'indexedChars', 'totalChars', 'indexedPages', 'totalPages'];
	
	let lastItemID = 0;
	while (true) {
		this._stopCheck();
		
		let objs = await Zotero.FullText.getUnsyncedContent(this.libraryID, {
			maxSize: this.MAX_BATCH_SIZE,
			maxItems: this.MAX_BATCH_ITEMS,
			lastItemID
		});
		if (!objs.length) {
			break;
		}
		
		let jsonArray = [];
		let results;
		
		for (let obj of objs) {
			let json = {};
			for (let prop of props) {
				json[prop] = obj[prop];
			}
			jsonArray.push(json);
			lastItemID = obj.itemID;
		}
		({ libraryVersion, results } = await this.apiClient.setFullTextForItems(
			this.library.libraryType,
			this.library.libraryTypeID,
			libraryVersion,
			jsonArray
		));
		await Zotero.DB.executeTransaction(async function () {
			for (let state of ['successful', 'unchanged']) {
				for (let index in results[state]) {
					let key = results[state][index].key;
					let itemID = Zotero.Items.getIDFromLibraryAndKey(this.libraryID, key);
					await Zotero.FullText.setItemSynced(itemID, libraryVersion);
				}
			}
			// Set both the library version and the full-text library version. The latter is necessary
			// because full-text sync can be turned off at any time, so we have to keep track of the
			// last version we've seen for full-text in case the main library version has advanced since.
			await Zotero.FullText.setLibraryVersion(this.libraryID, libraryVersion);
			this.library.libraryVersion = libraryVersion;
			await this.library.save();
		}.bind(this));
		
		for (let index in results.failed) {
			let { code, message, data } = results.failed[index];
			let e = new Error(message);
			e.name = "ZoteroObjectUploadError";
			e.code = code;
			if (data) {
				e.data = data;
			}
			Zotero.logError("Error uploading full text for item " + jsonArray[index].key + " in "
				+ this.library.name + ":\n\n" + e);
			
			if (this.onError) {
				this.onError(e);
			}
			if (this.stopOnError) {
				throw new Error(e);
			}
		}
	}
};


Zotero.Sync.Data.FullTextEngine.prototype.stop = async function () {
	// TODO: Cancel requests?
	this._stopping = true;
}


Zotero.Sync.Data.FullTextEngine.prototype._stopCheck = function () {
	if (!this._stopping) return;
	Zotero.debug("Full-text sync stopped for " + this.library.name);
	throw new Zotero.Sync.UserCancelledException;
}
