/*
    ***** BEGIN LICENSE BLOCK *****
    
    Copyright © 2009 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 *****
*/


Zotero.URI = new function () {
	Zotero.defineProperty(this, 'defaultPrefix', {
		value: 'http://zotero.org/'
	});
	
	// This should match all possible URIs. Match groups are as follows:
	// 1: users|groups
	// 2: local/|NULL
	// 3: userID|groupID|localUserKey
	// 4: publications|feeds/libraryID|NULL
	// 5: items|collections|NULL
	// 6: itemKey|collectionKey|NULL
	var uriPartsRe = new RegExp(
		'^' + Zotero.Utilities.quotemeta(this.defaultPrefix)
		+ '(users|groups)/(local/)?(\\w+)(?:/(publications|feeds/\\w+))?'
		+ '(?:/(items|collections)/(\\w+))?'
	);
	
	/**
	 * Get a URI with the user's local key, if there is one
	 *
	 * @return	{String|False}		e.g., 'http://zotero.org/users/local/v3aG8nQf'
	 */
	this.getLocalUserURI = function () {
		return this.defaultPrefix + "users/local/" + Zotero.Users.getLocalUserKey();
	}
	
	
	/**
	 * Get a URI for the user, creating a local user key if necessary
	 *
	 * @return	{String}
	 */
	this.getCurrentUserURI = function () {
		var userID = Zotero.Users.getCurrentUserID();
		if (userID) {
			return this.defaultPrefix + "users/" + userID;
		}
		
		return this.getLocalUserURI();
	}
	
	
	this.getCurrentUserLibraryURI = function () {
		var userID = Zotero.Users.getCurrentUserID();
		if (!userID) {
			return false;
		}
		return this.getCurrentUserURI() + "/items";
	}
	
	
	this.getLibraryURI = function (libraryID) {
		return this.defaultPrefix + this.getLibraryPath(libraryID);
	}
	
	
	/**
	 * Get path portion of library URI (e.g., users/6 or groups/1)
	 */
	this.getLibraryPath = function (libraryID) {
		var libraryType = Zotero.Libraries.get(libraryID).libraryType;
		
		switch (libraryType) {
			case 'user':
				var id = Zotero.Users.getCurrentUserID();
				if (!id) {
					id = 'local/' + Zotero.Users.getLocalUserKey();
				}
				
				if (libraryType == 'publications') {
					return "users/" + id + "/" + libraryType;
				}
				
				break;
			
			case 'feed':
				// Since feeds are not currently synced, generate a local URI
				return "users/local/" + Zotero.Users.getLocalUserKey() + "/feeds/" + libraryID;
			
			case 'group':
				var id = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
				break;
			
			default:
				throw new Error(`Unsupported library type '${libraryType}' for library ${libraryID}`);
		}
		
		return libraryType + "s/" + id;
	}
	
	
	/**
	 * Get library from path (e.g., users/6 or groups/1)
	 *
	 * @return {Zotero.Library|false}
	 */
	this.getPathLibrary = function (path) {
		let matches = path.match(/^\/\/?users\/(\d+)/);
		if (matches) {
			let userID = matches[1];
			let currentUserID = Zotero.Users.getCurrentUserID();
			if (userID != currentUserID) {
				Zotero.debug("User ID from streaming server doesn't match current id! "
					+ `(${userID} != ${currentUserID})`);
				return false;
			}
			return Zotero.Libraries.userLibrary;
		}
		matches = path.match(/^\/groups\/(\d+)/);
		if (matches) {
			let groupID = matches[1];
			return Zotero.Groups.get(groupID);
		}
	}
	
	
	/**
	 * Return URI of item, which might be a local URI if user hasn't synced
	 */
	this.getItemURI = function (item) {
		return this.getObjectURI(item);
	}
	
	
	/**
	 * Get path portion of item URI (e.g., users/6/items/ABCD1234 or groups/1/items/ABCD1234)
	 */
	this.getItemPath = function (item) {
		return this._getObjectPath(item);
	}
	
	
	this.getFeedItemURI = function (feedItem) {
		return this.getItemURI(feedItem);
	}
	
	this.getFeedItemPath = function (feedItem) {
		return this.getItemPath(feedItem);
	}
	
	/**
	 * Return URI of collection, which might be a local URI if user hasn't synced
	 */
	this.getCollectionURI = function (collection) {
		return this.getObjectURI(collection);
	}
	
	
	/**
	 * Get path portion of collection URI (e.g., users/6/collections/ABCD1234 or groups/1/collections/ABCD1234)
	 */
	this.getCollectionPath = function (collection) {
		return this._getObjectPath(collection);
	}
	
	this.getFeedURI = function (feed) {
		return this.getLibraryURI(feed);
	}
	
	this.getFeedPath = function (feed) {
		return this.getLibraryPath(feed);
	}
	
	
	this.getGroupsURL = function () {
		return ZOTERO_CONFIG.WWW_BASE_URL + "groups";
	}
	
	
	/**
	 * @param {Zotero.Group} group
	 * @param {Boolean} webRoot
	 * @return {String}
	 */
	this.getGroupURI = function (group, webRoot) {
		var uri = this.getObjectURI(group);
		if (webRoot) {
			this.toWebURL(uri);
		}
		return uri;
	}

	/**
	 * @param {Zotero.Search} search
	 * @param {Boolean} webRoot
	 * @return {String}
	 */
	this.getSearchURI = function (search, webRoot) {
		var uri = this.getObjectURI(search);
		if (webRoot) {
			uri = this.toWebURL(uri);
		}
		return uri;
	};
	
	this._getObjectPath = function (obj) {
		let path = this.getLibraryPath(obj.libraryID);
		if (obj instanceof Zotero.Library) {
			return path;
		}
		
		if (obj instanceof Zotero.Item) {
			return path + '/items/' + obj.key;
		}
		
		if (obj instanceof Zotero.Collection) {
			return path + '/collections/' + obj.key;
		}

		if (obj instanceof Zotero.Search) {
			return path + '/searches/' + obj.key;
		}
		
		throw new Error("Unsupported object type '" + obj._objectType + "'");
	}

	/**
	 * @param {Zotero.Item | Zotero.Collection | Zotero.Group | Zotero.Search} obj
	 * @return {String}
	 */
	this.getObjectURI = function (obj) {
		return this.defaultPrefix + this._getObjectPath(obj);
	};

	/**
	 * @param {Number} libraryID
	 * @param {String | Object} tag Tag name or tag object
	 */
	this.getTagURI = function (libraryID, tag) {
		return this.getLibraryURI(libraryID) + '/tags/'
			+ encodeURIComponent(tag.tag || tag).replaceAll('%20', '+');
	};
	
	/**
	 * Convert an item URI into an item
	 *
	 * @param	{String}				itemURI
	 * @return {Promise<Zotero.Item|false>}
	 */
	this.getURIItem = function (itemURI) {
		var obj = this._getURIObject(itemURI, 'item');
		if (!obj) return false;
		return Zotero.Items.getByLibraryAndKeyAsync(obj.libraryID, obj.key);
	};
	
	
	/**
	 * @param {String} itemURI
	 * @return {Object|FALSE} - Object with 'libraryID' and 'key', or FALSE if item not found
	 */
	this.getURIItemLibraryKey = function (itemURI) {
		return this._getURIObject(itemURI, 'item');
	}
	
	
	/**
	 * Convert an item URI into a libraryID and key from the database, without relying on global state
	 *
	 * Note that while the URI must point to a valid library, the item doesn't need to exist
	 */
	this.getURIItemLibraryKeyFromDB = function (itemURI) {
		return this._getURIObjectLibraryKeyFromDB(itemURI, 'item');
	}
	
	
	/**
	 * @param {String} itemURI
	 * @return {Integer|FALSE} - itemID of matching item, or FALSE if none
	 */
	this.getURIItemID = function (itemURI) {
		var obj = this._getURIObject(itemURI, 'item');
		if (!obj) return false;
		return Zotero.Items.getIDFromLibraryAndKey(obj.libraryID, obj.key);
	}
	
	
	/**
	 * Convert a collection URI into a collection
	 *
	 * @param	{String}				collectionURI
	 * @param	{Zotero.Collection|FALSE}
	 * @return {Promise<Zotero.Collection|false>}
	 */
	this.getURICollection = function (collectionURI) {
		var obj = this._getURIObject(collectionURI, 'collection');
		if (!obj) return false;
		return Zotero.Collections.getByLibraryAndKeyAsync(obj.libraryID, obj.key);
	};
	
	
	/**
	 * @param {String} collectionURI
	 * @return {Object|FALSE} - Object with 'libraryID' and 'key', or FALSE if item not found
	 */
	this.getURICollectionLibraryKey = function (collectionURI) {
		return this._getURIObject(collectionURI, 'collection');;
	}
	
	
	/**
	 * @param {String} collectionURI
	 * @return {Integer|FALSE} - collectionID of matching collection, or FALSE if none
	 */
	this.getURICollectionID = function (collectionURI) {
		var obj = this._getURIObject(collectionURI, 'collection');
		if (!obj) return false;
		return Zotero.Collections.getIDFromLibraryAndKey(obj.libraryID, obj.key);
	}
	
	
	/**
	 * Convert a library URI into a libraryID
	 *
	 * @param	{String}				libraryURI
	 * @return {Integer|FALSE} - libraryID, or FALSE if no matching library
	 */
	this.getURILibrary = function (libraryURI) {
		let library = this._getURIObjectLibrary(libraryURI);
		return library ? library.id : false;
	}


	this.getURIFeed = function (feedURI) {
		return this._getURIObjectLibrary(feedURI, 'feed');
	}

	/**
	 * @param {String} uri
	 * @param {String} [apiURL]
	 */
	this.toAPIURL = function (uri, apiURL) {
		if (!apiURL) {
			apiURL = ZOTERO_CONFIG.API_URL;
		}
		return uri
			.replace(ZOTERO_CONFIG.BASE_URI, apiURL)
			// Replace local user key with users/0
			.replace(/(http:\/\/localhost:\d+\/api\/users)\/local\/\w+/, '$1/0');
	};

	/**
	 * TODO: Use getLibraryWebURL() once #2893 is merged to get functional URLs
	 *
	 * @param {String} uri
	 */
	this.toWebURL = function (uri) {
		return uri.replace(ZOTERO_CONFIG.BASE_URI, ZOTERO_CONFIG.WWW_BASE_URL);
	};
	
	
	/**
	 * Convert an object URI into an object containing libraryID and key
	 *
	 * @param {String} objectURI
	 * @param {String} [type] Object type to expect
	 * @return {Object|FALSE} - An object containing libraryID, objectType and
	 *   key. Key and objectType may not be present if the URI references a
	 *   library itself
	 */
	this._getURIObject = function (objectURI, type) { 
		let uri = objectURI.replace(/\/+$/, ''); // Drop trailing /
		let uriParts = uri.match(uriPartsRe);
		
		if (!uriParts) {
			throw new Error("Could not parse object URI " + objectURI);
		}
		
		let library = this._getURIObjectLibrary(objectURI);
		if (!library) return false;
		
		let retObj = {libraryID: library.libraryID};
		if (!uriParts[5]) {
			// References the library itself
			return retObj;
		}
		
		retObj.objectType = uriParts[5] == 'items' ? 'item' : 'collection';
		retObj.key = uriParts[6];
		
		if (type && type != retObj.objectType) return false;
		
		return retObj;
	};
	
	
	/**
	 * Convert an object URI into a Zotero.Library that the object is in
	 *
	 * @param {String}	objectURI
	 * @return {Zotero.Library|FALSE} - An object referenced by the URI
	 */
	this._getURIObjectLibrary = function (objectURI) {
		let uri = objectURI.replace(/\/+$/, ''); // Drop trailing "/"
		let uriParts = uri.match(uriPartsRe);
		
		if (!uriParts) {
			throw new Error("Could not parse object URI " + objectURI);
		}
		
		let library;
		if (uriParts[1] == 'users') {
			let type = uriParts[4];
			if (!type) {
				// Handles local and synced libraries
				library = Zotero.Libraries.get(Zotero.Libraries.userLibraryID);
			} else {
				let feedID = type.split('/')[1];
				library = Zotero.Libraries.get(feedID);
			}
		} else {
			// Group libraries
			library = Zotero.Groups.get(uriParts[3]);
		}
		
		if (!library) {
			Zotero.debug("Could not find a library for URI " + objectURI, 2, true);
			return false;
		}
		
		return library;
	}
	
	
	/**
	 * Convert an object URI into a libraryID from the database, without relying on global state
	 *
	 * @param {String}	objectURI
	 * @return {Promise<Integer|FALSE>} - A promise for either a libraryID or FALSE if a matching
	 *     library couldn't be found
	 */
	this._getURIObjectLibraryID = async function (objectURI) {
		let uri = objectURI.replace(/\/+$/, ''); // Drop trailing "/"
		let uriParts = uri.match(uriPartsRe);
		
		let libraryID;
		if (uriParts[1] == 'users') {
			let type = uriParts[4];
			// Personal library
			if (!type || type == 'publications') {
				libraryID = await Zotero.DB.valueQueryAsync(
					"SELECT libraryID FROM libraries WHERE type='user'"
				);
			}
			// Feed libraries
			else {
				libraryID = type.split('/')[1];
			}
		}
		// Group libraries
		else {
			libraryID = await Zotero.DB.valueQueryAsync(
				"SELECT libraryID FROM groups WHERE groupID=?", uriParts[3]
			);
		}
		
		if (!libraryID) {
			Zotero.debug("Could not find a library for URI " + objectURI, 2, true);
			return false;
		}
		
		return libraryID;
	};
	
	
	
	/**
	 * Convert an object URI into a libraryID and key from the database, without relying on global state
	 *
	 * Note that while the URI must point to a valid library, the object doesn't need to exist
	 *
	 * @param {String} objectURI - Object URI
	 * @param {String} type - Object type
	 * @return {Promise<Object|FALSE>} - A promise for an object with 'objectType', 'libraryID', 'key'
	 *     or FALSE if library didn't exist
	 */
	this._getURIObjectLibraryKeyFromDB = async function (objectURI, type) {
		let uri = objectURI.replace(/\/+$/, ''); // Drop trailing /
		let uriParts = uri.match(uriPartsRe);
		
		if (!uriParts) {
			throw new Error("Could not parse object URI " + uri);
		}
		
		let libraryID = await this._getURIObjectLibraryID(uri);
		if (!libraryID) {
			return false;
		}
		
		let retObj = { libraryID };
		if (!uriParts[5]) {
			// References the library itself
			return false;
		}
		
		retObj.objectType = uriParts[5] == 'items' ? 'item' : 'collection';
		retObj.key = uriParts[6];
		
		if (type && type != retObj.objectType) return false;
		
		return retObj;
	};
}
