class Connector extends CUI.Element

	api:
		search: null
		suggest: null
		eas: null

	init: ->

		@__unreachableEasydbs = []

		deferred = new CUI.Deferred()
		if ez5.session.getPref("connector")
			connector = CUI.util.copyObject(ez5.session.getPref("connector"), true)
			if not CUI.util.isArray(connector?.easydbs)
				connector = undefined

		else
			connector = undefined

		baseConfig = ez5.session.getBaseConfig("plugin", "easydb-connector-plugin")
		baseConfig = baseConfig.system or baseConfig # TODO: Remove this after #64076 is merged.
		@__base_config_fylr = baseConfig?.fylr_url or {}

		pluginUrl = ez5.pluginManager.getPlugin("easydb-connector-plugin").getPluginURL()

		if CUI.util.isEmpty(pluginUrl)
			console.warn("Connector::init -> The connect was not initialized because the 'plugin url' is empty.")
			return CUI.rejectedPromise()

		ez5.server(
			url: ez5.getAbsoluteURL(pluginUrl) + "/list?gettokens=1"
			handle_error: => true
		).done((data) =>
			for easydb, idx in data?.easydbs or []
				if not easydb.token and not easydb.error
					continue

				if connector != undefined
					active = false
					for _easydb in connector.easydbs or []
						if _easydb.name == easydb.name and _easydb.active
							active = true
							break
				else
					active = true

				url = easydb.url
				if url.endsWith("/")
					url = url.substr(0, url.length-1)

				@__config.easydbs.push
					idx: idx
					active: active
					name: easydb.name
					url: url
					version: easydb.version
					token: easydb.token
					error: easydb.error

			if not @isEnabled()
				return deferred.resolve()


			for easydb, idx in @__config.easydbs

				reasydb = new RemoteEasydb(easydb: easydb)

				if not easydb.token and not CUI.util.isEmpty(easydb.error) and easydb.active
					# If we dont have token and we have error we dont add this remote easydb to the list for split request.
					# Requests will fail as we dont have token.
					@addUnrecheableEasydb(reasydb)
					continue

				console.info("Remote easydb:", reasydb.getServerUrl(), "active:", reasydb.isActive())

				if not reasydb.isActive()
					continue

				@__easydbs.push(reasydb)

			# wrapServerRequestApi is installed eagerly at module load

			deferred.resolve()
		).fail((xhr) =>
			if xhr.responseJSON?.code == "error.user.no_system_right"
				console.warn("Connector::init - No rights to use connector.")
				return deferred.resolve()
			console.error("Connector::init - Error getting the tokens of the instances.", xhr)
			deferred.reject()
		)
		return deferred.promise()

	getActiveEasydbs: ->
		@__easydbs or []

	addUnrecheableEasydb: (easydb) ->
		if not @__unreachableEasydbs
			@__unreachableEasydbs = []
		CUI.util.pushOntoArray(easydb, @__unreachableEasydbs)

	getAvailableEasydbs: ->
		@__config.easydbs or []

	isEnabled: ->
		if not @hasRights()
			return false
		return true

	hasRights: ->
		return ez5.session.hasSystemRight("root", "plugin.easydb-connector-plugin.allow_use")

	isAvailable: ->
		@isEnabled() and @getAvailableEasydbs().length > 0

	logout: ->
		promises = []
		for easydb in @__easydbs or []
			promises.push(easydb.logout())
		CUI.when(promises)

	readOpts: ->
		super()
		@__easydb_by_instance = {}

	showConfiguration: ->
		data = CUI.util.copyObject(@__config, true)

		for easydb in data.easydbs
			if easydb.active
				for reasydb in @__easydbs
					if reasydb._easydb.idx == easydb.idx
						easydb.status = $$("connector.configuration.status.connected")
						easydb.__class = "ez5-connector--active"

				for reasydb in @__unreachableEasydbs
					if reasydb._easydb.idx == easydb.idx
						easydb.status = reasydb.getLastError() or $$("remote.easydb.connect.unknown_error")
						easydb.__class = "ez5-connector--error"

			else
				easydb.status = $$("connector.configuration.status.inactive")
				easydb.__class = "ez5-connector--inactive"

			easydb.internet_address = "["+easydb.url+"]("+easydb.url+")"

			delete(easydb.url)
			delete(easydb.login)
			delete(easydb.password)

		tbl = new CUI.DataTable
			data: data
			maximize: true
			class: "ez5-connector-config-table"
			name: "easydbs"
			new_rows: "none"
			fields: [
				type: CUI.Checkbox
				group: "connector"
				name: "active"
				form:
					label: $$("connector.configuration.label.active")
			,
				type: CUI.Output
				name: "name"
				form:
					label: $$("connector.configuration.label.name")
			,
				type: CUI.Output
				name: "internet_address"
				markdown: true
				form:
					label: $$("connector.configuration.label.internet_address")
			,
				type: CUI.Output
				name: "status"
				markdown: true
				multiline: true
				tooltip:
					markdown: true
					content: (tooltip) ->
						data = CUI.dom.data(tooltip.getElement(), "element").getData()
						if data
							msg = data.error?.msg or data.status
							if CUI.util.isPlainObject(msg)
								msg = CUI.util.dump(msg)
						return new CUI.Label(text: "#{msg}", multiline: true)
				form:
					column: "maximize"
					label: $$("connector.configuration.label.status")
			]

		tbl.start()

		save_btn = new LocaButton
			loca_key: "connector.save.button"
			primary: true
			left: true
			onClick: (ev, btn) =>
				if ev.hasModifierKey()
					console.debug(CUI.util.dump(data))
					return

				btn.disable()
				btn.startSpinner()

				ez5.session.savePref("connector", data)
				.always =>
					btn.enable()
					btn.stopSpinner()

				.done =>
					mod.destroy()
					dfr.resolve()

		mod = new CUI.Modal
			cancel: true
			onCancel: =>
				dfr.reject()
				return
			pane:
				header_left: new LocaLabel(loca_key: "connector.configuration.title")
				# padded: true
				content: [
					tbl
					new LocaLabel
						multiline: true
						appearance: "secondary"
						padded: true
						markdown: true
						loca_key: "connector.configuration.hint.md"
				]
				footer_right: save_btn
		mod.show()

		dfr = new CUI.Deferred()
		dfr.promise()

	displaynameByInstance: (instance) ->
		@getRemoteEasydbByInstance(instance).getDisplayname()

	getRemoteEasydbByInstance: (instance) ->
		@__easydb_by_instance[instance]

	get_user_key: (realm, key) ->
		idx = key.indexOf('@')
		if idx == -1 or realm != 'CURRENT'
			return null

		instance = key.substr(idx+1, 36)
		loca_key = @replaceInstance(key, instance)

		@__easydb_by_instance[instance].get_user_key_CURRENT(loca_key)

	getCollectionTools: (collection) ->
		tools = []

		if not @isAvailable()
			return tools

		tools.push new ToolboxTool
			group: collection.getToolGroup()
			name: collection.getToolNamePrefix()+".replace_easydb4_objects"
			sort: "J:1"
			disabled: collection.getCount() > 1000
			run: =>
				@replaceEasydb4Objects(collection)

		tools

	@idRegexp: /^([0-9]{2})([0-9]{2})([0-9]{7})@easydb4$/

	replaceEasydb4Objects: (collection) ->
		console.error "replace connector objects", collection

		show_error = =>
			CUI.problem(text: $$("connector.replace_easydb4_objects.problem.getting_info"))
			return

		objects = null

		load_objects = =>
			ez5.splash.show("connector.replace_easydb4_objects.searching")
			collection.loadObjects()
			.always =>
				ez5.splash.hide()
			.fail(show_error)
			.done (_objects) =>
				objects = _objects

				# ids which are easydb4 and from an active db
				searchable_ids = {}

				searchable_count = 0

				# ids which are easydb4 but not from an active db
				not_searchable_ids = []

				for obj in objects
					if not obj._global_object_id.endsWith("@easydb4")
						continue

					[_, easydb4_id, table_id, object_id] = Connector.idRegexp.exec(obj._global_object_id)

					match = null
					for easydb, easydb_idx in @__easydbs
						ez4info = easydb.getEasydb4Info()
						if not ez4info
							continue

						if ez4info.easydb_id == easydb4_id and ez4info.table_id == table_id
							match = ez4info
							break

					if match == null
						not_searchable_ids.push(obj._global_object_id)

						continue

					if not searchable_ids[easydb_idx]
						searchable_ids[easydb_idx] = []

					searchable_ids[easydb_idx].push(parseInt(object_id))
					searchable_count = searchable_count + 1

				console.debug "objects:", objects, searchable_ids, not_searchable_ids

				# no searchable ids
				if CUI.util.isEmptyObject(searchable_ids) and not_searchable_ids.length == 0
					CUI.alert(text: $$("collection.replace_easydb4_objects.alert.no_objects.md"), markdown: true)
				else if CUI.util.isEmptyObject(searchable_ids)
					CUI.alert(text: $$("collection.replace_easydb4_objects.alert.no_matching_easydb4.md", count_not_searchable: not_searchable_ids.length, not_searchable_ids: not_searchable_ids.slice(0,10)), markdown: true)
				else if not_searchable_ids.length > 0
					CUI.confirm(text: $$("collection.replace_easydb4_objects.alert.partial_matching_easydb4.md", count_not_searchable: not_searchable_ids.length, count_searchable: searchable_count, not_searchable_ids: not_searchable_ids.slice(0,10)), markdown: true)
					.done =>
						do_search(searchable_ids)
				else
					CUI.confirm(text: $$("collection.replace_easydb4_objects.alert.all_matching_easydb4.md", count_searchable: searchable_count), markdown: true)
					.done =>
						do_search(searchable_ids)


		do_search = (searchable_ids) =>
			searchableEntries = Object.entries(searchable_ids)
			requests = []

			for [easydb_idx, object_ids] from searchableEntries
				easydb = @__easydbs[easydb_idx]
				easydb4 = easydb.getEasydb4Info()

				console.debug easydb, easydb4, object_ids

				inst = easydb.getInstance()
				field_name = easydb4.objecttype+"@"+inst+"."+easydb4.table_ref_col
				requests.push(new Promise((resolve) => ez5.api.search({
					json_data:
						type: "object"
						objecttypes: [ easydb4.objecttype+"@"+inst ]
						include_fields: [ field_name ]
						generate_rights: false
						format: "short"
						search: [
							type: "in"
							fields: [ field_name ]
							in: (easydb4.table_name + ":" +object_id for object_id in object_ids)
						]
					handle_error: () =>
						return true
					}).done(resolve).fail((data) =>
						rejected = [data...].find(({ state }) => state == "rejected")
						resolve({ error: rejected.args[0], objects: [], })
					)
				))

			totalSearchIdCount = searchableEntries.reduce(((r, v) => r + v[1].length), 0)

			Promise.all(requests).then((responses) =>
				map = {}
				missingCount = 0

				for [index, data] from responses.entries()
					missingCount += searchableEntries[index][1].length - data.objects.length

					if data.error
						continue

					for obj in data.objects
						easydb = @getRemoteEasydbByInstance(obj._instance)
						easydb4 = easydb.getEasydb4Info()

						ref = obj[obj._objecttype]?[easydb4.table_ref_col]

						if not ref
							missingCount += 1
							continue

						object_id = parseInt(ref.split(":")[1])

						easydb4_id = easydb4.easydb_id * Math.pow(10,9) + easydb4.table_id * Math.pow(10,7) + object_id

						console.debug ref, object_id, easydb4, easydb4_id

						map[easydb4_id+"@easydb4"] = obj._global_object_id

				console.debug "received data from remote", data, map

				if missingCount == totalSearchIdCount
					return CUI.alert
						text: $$("collection.replace_easydb4_objects.alert.no_results.md")
						markdown: true

				sd = collection.getSaveData()

				sd.collection._version = sd.collection._version + 1

				for slide in sd.collection.webfrontend_props.presentation?.slides or []
					for k in ["left", "center", "right"]
						gid = slide[k]?.global_object_id
						if gid and map[gid]
							# console.warn "replace:", gid, map[gid]
							slide[k].global_object_id = map[gid]

				newObjects = objects.reduce(
					(result, object) =>
						newId = map[object._global_object_id]

						if newId
							# it can happen that the mapped remote object is in the collection already
							# in that case it won't be added again, resulting in the ez4 object to be removed
							if not objects.find((o) => o._global_object_id == newId)
								newObject = structuredClone(object)
								newObject._global_object_id = newId
								result.push(newObject)
						else
							result.push(structuredClone(object))

						return result
					[]
				)

				# for co in newObjects
				# 	if map[co._global_object_id]
				# 		co._global_object_id = map[co._global_object_id]

				if not ez5.version("6")
					sd._objects = newObjects

				ez5.api.collection
					type: "POST"
					api: "/"+collection.getId()
					json_data: sd
				.done =>
					refresh = () =>
						CUI.alert
							text: if missingCount then $$("collection.replace_easydb4_objects.alert.success_with_misses.md", { missingCount }) else $$("collection.replace_easydb4_objects.alert.success.md")
							markdown: true
						collection.refresh()

					if ez5.version("6")
						return ez5.api.collection
							type: "POST"
							api: "/objects/" + collection.getId()
							json_data:
								objects: newObjects
						.done(() => refresh())

					refresh()

				# console.debug "save data for collection:", CUI.util.dump(sd)
				return
			)

		collection.loadFromDb()
		.fail(show_error)
		.done(load_objects)

		return

	startDownload: (_opts) ->
		opts = CUI.Element.readOpts _opts, "Connector.startDownload",
			manager:
				mandatory: true
				check: DownloadManager


		dfr = new CUI.Deferred()

		manager = opts.manager

		eas_info = manager.getEASColumnsInfo()
		# group all downloadable versions by preview class

		fields = []

		# console.debug "EAS INFO:", eas_info
		show_warning = false

		for instance, info of eas_info.counts.by_instance

			if instance == 'local'
				eas_config = ez5.session.getEASConfig()
				lbl = ez5.session.getEasydbName()
				easydb = null
			else
				easydb = @getRemoteEasydbByInstance(instance)
				lbl = easydb.getDisplayname()
				eas_config = easydb.getSession().getEASConfig()

				if not easydb.getSession().hasSystemRight("root", "frontend_features[download]")
					console.info("Connector: Download not allowed for:", easydb.getDisplayname())
					show_warning = true
					continue

			fields.push
				type: CUI.Form
				name: instance
				fields: manager.getFormFieldsFromStats(info.by_class, eas_config, "connector.download.manager.")
				form:
					label: lbl
				_easydb: easydb

		fields.sort (a, b) =>
			CUI.util.compareIndex(
				CUI.util.idxInArray(a._easydb, @__easydbs)
				CUI.util.idxInArray(b._easydb, @__easydbs)
			)


		console.debug instance, "INFO:", info, eas_config

		go_on2 = =>
			form_data = {}

			form = new CUI.Form
				fields: fields
				data: form_data
				onDataChanged: =>
					set_state()

			form.start()

			dfr = new CUI.Deferred()

			urls = null
			events_by_instance = null

			set_state = =>
				bytes = 0
				urls = []
				events_by_instance = local: []

				for instance, data of form_data
					for cls, versionNames of data
						# the download dialog adds fields of its own (custom renditions, preset
						# selections) whose keys have no version statistics here
						if not CUI.util.isArray(versionNames)
							continue

						class_info = eas_info.counts.by_instance[instance]?.by_class[cls]
						if not class_info
							continue

						versions = []
						for versionName in versionNames
							vinfo = class_info.versions[versionName]
							continue if not vinfo
							bytes += vinfo.acc_filesize
							versions = versions.concat(vinfo.versions)

						for version in versions
							obj = version.__object_data
							asset = version.__asset.getValue()

							if not events_by_instance[instance]
								events_by_instance[instance] = []

							if (easydb = @getRemoteEasydbByInstance(instance))?.isFylr()
								url = easydb.addTokenToUrl(version.url) + "&disposition=attachment"
							else
								url = version.download_url

							if not easydb?.isFylr()
								filename = ez5.basename(url)

								if instance != 'local'
									prefix = @getRemoteEasydbByInstance(instance).getDisplayname() + '-'
								else
									prefix = ez5.session.getEasydbName() + '-'

								if filename.indexOf(".") == -1 # no filename available, add standard text
									filename = prefix + ez5.loca.getBestDatabaseValue(obj._standard[1].text)+"-"+asset._id+"."+asset.extension
									filename = filename.replace(/[|\/\\<>*?":!]/g, "_") # fix for emacs syntax highlight: "
									url = url + '/' + encodeURIComponent(filename)
								else
									# add asset id to the filename
									parts = url.split("/")
									filename = parts[parts.length-1]

									parts[parts.length-1] = prefix + ez5.bareBasename(filename)+"-"+asset._id+"."+ez5.extension(filename)

									url = parts.join("/")

								# console.debug "versions:", version, filename, url

							if url.startsWith("/")
								urls.push(ez5.getAbsoluteURL(url))
							else
								urls.push(url)

							info = Connector.getSessionInfoForEvent()

							for k, v of {
								original_filename: asset.original_filename
								filename: filename
								filesize: asset.filesize
								class: asset.class
								extension: asset.extension
								version: version.version
								system_object_id: obj._system_object_id
							}
								info[k] = v

							events_by_instance[instance].push
								object_id: parseInt((asset._id+"").replace('@'+instance, ''))
								schema: "BASE"
								basetype: "asset"
								type: "ASSET_CONNECTOR_DOWNLOAD"
								pollable: false
								info: info

							if instance != 'local'
								info = CUI.util.copyObject(info, true)
								info.system_object_id = obj._system_object_id + "@" + instance # Add the instance so we can access to the object.
								events_by_instance["local"].push
									schema: "BASE"
									basetype: "asset"
									type: "ASSET_CONNECTOR_REMOTE_DOWNLOAD"
									pollable: false
									info: info

				if urls.length > 0
					console.debug "save events?", events_by_instance
					mod_info.setText($$("connector.download.info", size: ez5.format_filesize(bytes), count: urls.length))
					btn_download.enable()
				else
					mod_info.setText("")
					btn_download.disable()


			mod_info = new CUI.Label(text: "")

			btn_download = new LocaButton
				loca_key: "connector.download.button.download"
				left: true
				disabled: true
				onClick: (ev, btn) =>
					btn.disable()
					btn.startSpinner()
					promises = []
					for instance, events of events_by_instance
						if instance != "local"
							easydb = @getRemoteEasydbByInstance(instance)
						else
							easydb = null

						for event in events
							promises.push(EventPoller.saveEvent(event, easydb))

					CUI.when(promises)
					.done =>
						if urls.length == 1
							ez5.download_file(href: urls[0])
						else
							if ez5.version("6")
								files = (href: url for url in urls)
								return ez5.download_files(files)
							if not @__base_config_fylr.url?.endsWith("/zip")
								CUI.problem(text: $$("connector.download.fylr_url_not_properly_configured"))
								return

							files = (href: url for url in urls)
							ez5.download_multiple_files(files, @__base_config_fylr.url, $$("connector.download.zip_download_name", count: files.length))
					.fail =>
						btn.stopSpinner()
						btn.enable()

					mod.destroy()

			mod = new CUI.ConfirmationDialog
				icon: $$("download.manager.modal.icon")
				title: manager.getTitle(eas_info.total)
				class: "ez5-download-manager-confirmation-dialog ez5-event-poller"
				footer_left: mod_info
				content: # panels
					form
					# form2
				buttons: [ btn_download ]
				cancel: true
				onCancel: =>
					dfr.reject("user_rejected")
					return

			mod.show()
			return

		go_on = =>
			idx = 0
			next_field = =>
				if idx == fields.length
					go_on2()
					return

				field = fields[idx]
				manager.showUserAdminMessages(field._easydb)
				.done =>
					idx = idx + 1
					next_field()
				.fail(dfr.reject)

			next_field()

		if show_warning
			CUI.problem(text: $$("connector.download.remote_not_allowed"))
			.done =>
				if fields.length == 0
					dfr.reject()
				else
					go_on()
		else
			go_on()

		return dfr.promise()


	get_user_key_localized: (realm, key, _default) ->
		idx = key.indexOf('@')
		if idx == -1 or realm != 'CURRENT'
			return null

		v = ez5.loca.getBestFrontendValue(@get_user_key(realm, key))
		if CUI.util.isEmpty(v)
			instance = key.substr(idx+1, 36)
			@replaceInstance(_default, instance)
		else
			v

	replaceInstance: (str, instance) ->
		str?.replace(new RegExp(CUI.util.escapeRegExp('@'+instance), 'g'), '')

	isReady: ->
		@__loadDone == true

	# fires immediately if already loaded
	onReady: (callback) ->
		if @__loadDone
			callback()
			return
		@__readyCallbacks ?= []
		@__readyCallbacks.push(callback)
		return

	__fireReadyCallbacks: ->
		@__loadDone = true
		callbacks = @__readyCallbacks or []
		@__readyCallbacks = []
		for cb in callbacks
			try
				cb()
			catch e
				console.error("Connector.onReady callback threw:", e)
		return

	# blocks interaction while we negotiate tokens with remote instances
	__showLoadingModal: ->
		return if @__loadingModal
		@__loadingModal = new CUI.Modal
			cancel: false
			class: 'ez5-connector-loading-modal'
			pane:
				padded: true
				content: new CUI.Label
					text: $$("connector.loading.banner")
					icon: new CUI.Icon(icon: "spinner", class: "fa-spin")
		@__loadingModal.show()
		return

	__hideLoadingModal: ->
		return if not @__loadingModal
		@__loadingModal.destroy()
		@__loadingModal = null
		return

	load: ->
		@__config = easydbs: []
		@__easydbs = []
		@__loadDone = false

		if not @hasRights()
			@__fireReadyCallbacks()
			return CUI.resolvedPromise()

		# wait for the app theme before showing the modal, otherwise it renders
		# with easydb (ng) styling instead of fylr. Older frontends don't ship
		# whenThemeLoaded yet -> keep the previous behavior (show right away).
		if ez5.session.whenThemeLoaded?
			ez5.session.whenThemeLoaded().done =>
				@__showLoadingModal() if not @__loadDone
		else
			@__showLoadingModal()
		@onReady => @__hideLoadingModal()

		deferred = new CUI.Deferred()
		deferred.always =>
			@__fireReadyCallbacks()
		@init().done(=>
			promises = []

			for easydb in @__easydbs

				do (easydb) =>
					dfr = new CUI.Deferred()
					promises.push(dfr.promise())

					easydb.load()
					.fail(dfr.reject)
					.done =>
						@__easydb_by_instance[easydb.getInstance()] = easydb

						easydb.loadSchema()
						.fail(dfr.reject)
						.done =>
							for mask in easydb.mask.CURRENT.masks
								if not ez5.mask.CURRENT._masks_by_table_id[mask.table_id]
									ez5.mask.CURRENT._masks_by_table_id[mask.table_id] = []
								ez5.mask.CURRENT._masks_by_table_id[mask.table_id].push(mask)
								ez5.mask.CURRENT.masks.push(mask)
								ez5.mask.CURRENT._mask_by_id[mask.mask_id] = mask
								ez5.mask.CURRENT._mask_by_name[mask.name] = mask

							for table in easydb.schema.CURRENT.tables
								ez5.schema.CURRENT.tables.push(table)
								ez5.schema.CURRENT._table_by_id[table.table_id] = table
								ez5.schema.CURRENT._table_by_name[table.name] = table

								if not table.owned_by
									ez5.schema.CURRENT._objecttypes.push(table);
									ez5.schema.CURRENT._objecttype_by_name[table.name] = table

							for mask in easydb.mask.CURRENT.masks
								ez5.mask.CURRENT._mask_instance_by_name[mask.name] = new Mask('CURRENT', null, mask)

							dfr.resolve()
							return

					return

			CUI.whenAll(promises).done =>
				CUI.util.removeFromArray(null, @__easydbs, (easydb) =>
					if easydb.isReachable()
						return false

					@addUnrecheableEasydb(easydb)
					return true
				)

				deferred.resolve()
		).fail(deferred.resolve)

		return deferred.promise()

	checkRemoval: (v, instance) ->
		if not instance
			v.indexOf('@') > -1
		else
			v.indexOf('@'+instance) == -1

	wrapServerRequestApi: ->
		console.info("Connector: Wrapping Server Request API.")
		@wrapSearchApi()
		@wrapEASApi()
		@wrapSuggestApi()
		@wrapServerRequestCredentials()

	# remote requests authenticate via token, never via cookies; fylr >= 6.35
	# answers unknown origins with credential-less CORS, so the browser blocks
	# credentialed requests to the remote (#79575)
	wrapServerRequestCredentials: ->
		prepareRequest = ServerRequest::__prepareRequest
		ServerRequest::__prepareRequest = ->
			if @_remoteEasydb
				@_withCredentials = false
			prepareRequest.call(@)

	# wait for connector ready, then dispatch through wrappedFn; on timeout
	# fall back to originalFn (local-only) to avoid hanging the caller forever
	__deferUntilReady: (opts, wrappedFn, originalFn, timeoutMs = 30000) ->
		dfr = new CUI.Deferred()
		promise = dfr.promise()
		innerPromise = null
		aborted = false
		fired = false

		promise.abort = ->
			aborted = true
			innerPromise?.abort?()
			return

		dispatchWith = (fn) =>
			return if fired
			fired = true
			return if aborted
			innerPromise = fn(opts)
			innerPromise.done(dfr.resolve).fail(dfr.reject)
			return

		@onReady => dispatchWith(wrappedFn)
		setTimeout (=> dispatchWith(originalFn)), timeoutMs
		return promise

	wrapSuggestApi: ->
		ez5.api.suggest = (opts) =>
			# pure string-massaging, no split - no need to wait for connector
			if opts.data.BODY and opts.data.BODY.indexOf('@') > -1

				json_data = JSON.parse(opts.data.BODY)

				for k in ['linked_objecttypes', 'objecttypes', 'fields']
					if not json_data[k]
						continue

					CUI.util.removeFromArray(null, json_data[k], (v) =>
						v.indexOf('@') > -1
					)

				if json_data.objecttypes.length == 0
					# no objecttypes, no result
					promise = CUI.resolvedPromise(
						suggestions:
							tokens: []
							fields: []
							linked_objects: []
					)
					promise.abort = -> # this is a faked function so suggest can be aborted
					return promise


				opts.data.BODY = JSON.stringify(json_data)


			@api.suggest(opts)


	cleanSearch: (search, instance, level=0) ->
		# console.debug "cleanSearch:", instance, level, CUI.util.copyObject(search, true)

		for item, item_idx in search
			if item.type in ['complex', 'nested']
				if item.type == "nested"
					regexp = new RegExp(CUI.util.escapeRegExp("@"+instance), "g")
					item.path = item.path.replace(regexp, "")

				@cleanSearch(item.search, instance, level+1)

				# This is necessary to avoid getting unwanted objects.
				# We need to leave the search in here, if our parent
				# has more than one (which would be us) search item
				if item.search?.length == 0 and
					item.__filter == "SearchInput" and
					search.length > 1
						item.search.push
							type: "in"
							bool: "must"
							fields: [ "_system_object_id" ]
							in: [-1]

				continue

			# Check if the item has an objecttype property and filter it
			if item.objecttype
				if @checkRemoval(item.objecttype, instance)
					# This objecttype doesn't belong to this instance, mark fields as empty to remove the item
					item.fields = []
					continue
				else if instance
					# Clean the instance from the objecttype
					regexp = new RegExp(CUI.util.escapeRegExp("@"+instance), "g")
					item.objecttype = item.objecttype.replace(regexp, "")

			# Range-type items (e.g. date facets) carry a singular "field" instead
			# of a "fields" array, so mirror the "fields" handling: strip the
			# @<instance> for this instance and drop the item when the field
			# belongs to a different one.
			if item.field and not item.fields
				if not item.field.startsWith('_')
					if @checkRemoval(item.field, instance)
						item.fields = []
						continue
					if instance
						repl_regexp = new RegExp(CUI.util.escapeRegExp("@"+instance), "g")
						item.field = item.field.replace(repl_regexp, '')
				continue

			if not item.fields
				continue

			if item.fields and item.in

				item.__instance = instance

				# remove values from list?
				remove = false

				# remove @<instance> from ids?
				clean = false

				# which instance use for cleaning?
				use_instance = instance

				# use int?
				use_int = false

				switch item.fields[0]
					when '_global_object_id'
						remove = true
						clean = false
						if not instance
							use_instance = ez5.session.getInstanceGlobalName()
					when '_objecttype', '_mask'
						remove = true
						clean = true
					else
						# pool ids carry a "@<instance>" as value, other ids don't
						if item.fields[0].endsWith("_pool.pool._id") or
							item.fields[0].endsWith("_pool._path.pool._id")
								remove = true
								clean = true
								use_int = true

				if remove

					CUI.util.removeFromArray(null, item.in, (v) =>
						@checkRemoval(v+"", use_instance)
					)

					if clean and use_instance
						for v, idx in item.in
							item.in[idx] = v.replace('@'+instance, '')
							if use_int
								item.in[idx] = parseInt(item.in[idx])

					if item.in.length == 0
						# skip this search
						if item.bool == "must_not" or item.__filter == "Facet"
							if use_int
								item.in.push(-1)
							else
								item.in.push("__non_existing_object")
						else
							item.fields = []

				CUI.util.removeFromArray(null, item.fields, (v) =>
					if v.startsWith('_') and
						v != "_collections._id" and
						v != "_system_object_id"
							return false

					@checkRemoval(v, instance)
				)

				if item.fields.length == 0 and item.__filter == "Facet"
					item.fields = [ "_system_object_id" ]
					item.in = [-1]

				if instance
					for v, idx in item.fields
						# Not sure whether we should ship all fields starting with underscore.
						# For sure we need to make an exception for _path, because it could include the instance id
						# (SearchHierarchyManager uses it, for example)
						if v.startsWith('_') and not v.startsWith("_path.")
							continue

						repl_regexp = new RegExp(CUI.util.escapeRegExp("@"+instance), "g")
						item.fields[idx] = v.replace(repl_regexp, '')
			else
				# TODO: Move to a function, this is a copy paste.
				CUI.util.removeFromArray(null, item.fields, (v) =>
					if v.startsWith('_')
						return false

					@checkRemoval(v, instance)
				)

				if instance
					for v, idx in item.fields
						if v.startsWith('_') and not v.startsWith("_path.")
							continue

						repl_regexp = new RegExp(CUI.util.escapeRegExp("@"+instance), "g")
						item.fields[idx] = v.replace(repl_regexp, '')

		CUI.util.removeFromArray(null, search, (item) ->
			if item.fields and item.fields.length == 0
				return true
			else if item.type == "complex" and item.search.length == 0
				return true
			else
				return false
		)

		return


	splitRequest: (opts) ->
		reqs = []

		# console.warn "splitRequest", CUI.util.copyObject(opts, true)

		# 0 is the local db, we run till <= easydb.length
		for idx in [0..@getActiveEasydbs().length] by 1
			# The debug object could have reference to class instances, such as 'Search'.
			# To avoid copying it, we set the same to all requests.
			debugObject = opts.debug
			delete opts.debug

			# If we have instances defined on request data, we only send the request to the instances defined.
			if opts.data?.instances
				inst = if idx > 0 then @__easydbs[idx-1].getInstance() else "local"
				if inst not in opts.data.instances
					# We dont have to send the request to this instance.
					reqs[idx] = null
					continue

			req = reqs[idx] = CUI.util.copyObject(opts, true)
			req.debug = debugObject

			if req.json_data?.objecttypes and req.json_data.objecttypes.length == 0
				delete(req.json_data.objecttypes)

			if opts.error
				# add a dummy function to handle the error
				req.error = =>

			if opts.success
				# add a dummy function to handle success
				req.success = =>

			if idx > 0
				easydb = @__easydbs[idx-1]
				instance = easydb.getInstance()
				repl_regexp = new RegExp(CUI.util.escapeRegExp("@"+instance), "g")
				req.remoteEasydb = easydb
			else
				repl_regexp = null
				instance = null

			req.__instance = instance

			if instance and not easydb.isFylr?()
				# On ez5 inheritance modes dont exist
				if req.json_data.format == "long_inheritance"
					req.json_data.format = "long"

			if req.json_data.sort
				sort = []
				for sortObject in req.json_data.sort
					if sortObject.field.startsWith("_") # Top level fields are ok.
						sort.push(sortObject)
						continue

					if not @checkRemoval(sortObject.field, instance)
						if instance
							sortObject.field = sortObject.field.replace(repl_regexp, '')
						sort.push(sortObject)

				if sort.length > 0
					req.json_data.sort = sort
				else
					delete req.json_data.sort

			if req.json_data.fields

				if instance
					for v in req.json_data.fields
						if v.field == "_objecttype" or
							v.field == "_pool"
								v.key = v.key + "@" + instance

				CUI.util.removeFromArray(null, req.json_data.fields, (v) =>
					if v.field.startsWith("_")
						return false
					@checkRemoval(v.field, instance)
				)
				if instance
					for def, idx in req.json_data.fields
						req.json_data.fields[idx].field = def.field.replace(repl_regexp, '')


			if req.json_data.objecttypes
				CUI.util.removeFromArray(null, req.json_data.objecttypes, (v) =>
					@checkRemoval(v, instance)
				)
				if instance
					for ot, idx in req.json_data.objecttypes
						req.json_data.objecttypes[idx] = ot.replace('@'+instance, '')

			for k in ["exclude_fields", "include_fields"]

				if not req.json_data[k]
					continue

				for ex_field in req.json_data[k].splice(0)
					if ex_field.indexOf(".") == -1
						req.json_data[k].push(ex_field)
					else if instance == null
						if ex_field.indexOf("@") == -1
							req.json_data[k].push(ex_field)
					else
						if ex_field.indexOf("@"+instance) > -1
							req.json_data[k].push(ex_field.replace(repl_regexp, ""))

			if req.json_data.aggregations
				for agg, def of req.json_data.aggregations
					if agg.startsWith("_pool") # check if an objecttype has a pool
						pool_ok = false
						for _ot in req.json_data.objecttypes or []
							if instance
								ot = _ot + '@' + instance
							else
								ot = _ot

							if ez5.schema.CURRENT._objecttype_by_name[ot].pool_link
								pool_ok = true
								if CUI.util.isString(def.filter_parent)
									filterParentInstance = def.filter_parent.substring(def.filter_parent.indexOf("@") + 1)
									if filterParentInstance != instance
										pool_ok = false
								break

						if not pool_ok
							delete(req.json_data.aggregations[agg])
							continue

					if agg == '_tags'
						if idx == 0 and ez5.version("6")
							hasTags = false
							for _ot in req.json_data.objecttypes or []
								otInfo = ez5.schema.CURRENT._objecttype_by_name[_ot]
								if otInfo?.has_tags
									hasTags = true
									break
							if not hasTags
								# If the local instance has no tags we have to delete the agg for tags.
								delete(req.json_data.aggregations[agg])
							continue
						if instance
							# Check if any of the objecttypes in the request has tags.
							hasTags = false
							for _ot in req.json_data.objecttypes or []
								otInfo = ez5.schema.CURRENT._objecttype_by_name[_ot + '@' + instance]
								if otInfo?.has_tags
									hasTags = true
									break
							if not hasTags
								delete(req.json_data.aggregations[agg])
						continue

					# We adjust the _eas facet to the remote instance version. Ez5 and fylr differ on how the eas facet is
					if instance and agg == "_eas"
						req.json_data.aggregations[agg].type = if easydb.isFylr?() then "term" else "asset"
						# We try to remove _linked._asset. from the field name no matter what is the local instance version
						req.json_data.aggregations[agg].field = req.json_data.aggregations[agg].field.replace("_linked._asset.", "")
						if easydb.isFylr?()
							# If the remote instance is fylr we need to add again _linked._asset. to the field name
							req.json_data.aggregations[agg].field = "_linked._asset."+req.json_data.aggregations[agg].field

					if agg == '_asset' or agg == '_asset_class_version_status'
						if instance and ez5.version("6") and not easydb.isFylr?()
							# We are on fylr and the request will be sent to ez5
							req.json_data.aggregations[agg].type = "asset"
							req.json_data.aggregations[agg].field = req.json_data.aggregations[agg].field.replace("_linked._asset.", "")
							continue
						if instance and not ez5.version("6") and easydb.isFylr?()
							# We are on ez5 and the request will be sent to fylr
							req.json_data.aggregations[agg].type = "term"
							req.json_data.aggregations[agg].field = "_linked._asset."+req.json_data.aggregations[agg].field

						# If we are on fylr and we are connecting to instance with assets but our instance has no assets we need
						# to remove the asset aggr. So we are going to check here if any of the objecttypes has assets.
						# The same applies if we are connecting to a fylr instance
						if (not instance and ez5.version("6")) or (instance and easydb.isFylr?())
							# We are going to use an objecttypemanager to load all objecttypes for each instance request
							# and check if any of them has assets. If not we are going to remove the asset aggr.
							otm = new ObjecttypeManager()
							otm.addObjecttypes((table) =>
								if not instance
									return table.name() in req.json_data.objecttypes
								else
									# we need to add the instance on the name of req.json_data.objecttypes elements
									# at this point the objecttype names are cleaned and they don't have the instance on the name
									return req.json_data.objecttypes.some( (ot) => table.name() == ot + "@" + instance )
							)
							if not otm.getEASFields().length > 0
								delete(req.json_data.aggregations[agg])
						continue

					if agg.startsWith('_eas')
						continue

					if instance and agg == "_system_tags" and ez5.version("6") and not easydb.isFylr?()
						delete(req.json_data.aggregations[agg])
						continue

					if def.field == "_pool"
						if instance
							if CUI.util.isString(def.filter_parent)
								if def.filter_parent.indexOf('@') > -1
									def.filter_parent = parseInt(def.filter_parent)


					for k in ["field", "objecttype"]
						if not def[k]
							continue

						if def[k].startsWith('_')
							continue

						if not instance
							if def[k].indexOf('@') > -1
								delete(req.json_data.aggregations[agg])
						else
							if def[k].indexOf('@'+instance) == -1
								delete(req.json_data.aggregations[agg])
							else
								def[k] = def[k].replace(repl_regexp, '')


			if req.json_data.search?.length > 0
				@cleanSearch(req.json_data.search, instance)
				if req.json_data.search.length == 0
					# signal the wrapper to not search in that instance
					req.json_data.objecttypes = []

		# for req in reqs
		# 	console.info "req:", req.__instance, CUI.util.copyObject(req, true)
		reqs

	@LOCAL_INSTANCE = "local"
	wrapEASApi: ->
		ez5.api.eas = (opts) =>
			if not opts.data?.ids or opts.skip_connector
				return @api.eas(opts)

			# only wait if the ids reference a remote instance
			needsConnector = opts.data.ids?.indexOf?("@") > -1
			if needsConnector and not @__loadDone
				return @__deferUntilReady(opts, ez5.api.eas, @api.eas)

			if not @__easydbs?.length
				return @api.eas(opts)

			dfr = new CUI.Deferred()

			if opts.handle_error
				dfr.fail (responses) =>
					for response in responses
						if response.state == "rejected"
							opts.handle_error.call(opts.handle_error, response.args[0]) # pass the XHR
							return
					# this can happen if all requests are skipped
					opts.handle_error.call(opts.handle_error)

			dataPerInstance = {}

			ids = JSON.parse(opts.data.ids)
			for id in ids
				if CUI.util.isString(id)
					idSplitted = id.split("@")
					instance = idSplitted[1]
					id = idSplitted[0]
				else
					instance = Connector.LOCAL_INSTANCE
				if not dataPerInstance[instance]
					dataPerInstance[instance] = ids: []
				dataPerInstance[instance].ids.push(parseInt(id))

			promises = []
			instances = []
			if dataPerInstance[Connector.LOCAL_INSTANCE]
				_opts = CUI.util.copyObject(opts)
				_opts.data.ids = JSON.stringify(dataPerInstance[Connector.LOCAL_INSTANCE].ids)
				promises.push(@api.eas(_opts))
				instances.push(Connector.LOCAL_INSTANCE)

			for remoteEasydb in @__easydbs
				remoteOpts = dataPerInstance[remoteEasydb.getInstance()]
				if not remoteOpts
					continue

				_opts = CUI.util.copyObject(opts)
				_opts.data.ids = JSON.stringify(remoteOpts.ids)
				_opts.remoteEasydb = remoteEasydb

				# Ez5 use short and fylr use standard for eas simple format
				if _opts.data?.format and _opts.data.format == "standard" and not remoteEasydb.isFylr?()
					_opts.data.format = "short"
				else if _opts.data?.format and _opts.data.format == "short" and remoteEasydb.isFylr?()
					_opts.data.format = "standard"

				promises.push(@api.eas(_opts))
				instances.push(remoteEasydb.getInstance())

			CUI.whenAll(promises).done =>
				responses = arguments
				data = {}
				for response, index in responses
					responseData = response.args[0]
					if response.state == 'rejected'
						continue
					instance = instances[index]
					if instance == "local"
						Object.assign(data, responseData)
						continue

					for idObject, object of responseData
						id = "#{idObject}@#{instance}"
						object._id = id
						data[id] = object
				return dfr.resolve(data)
			return dfr.promise()

	wrapSearchApi: ->

		isFakeSearch = (search) ->
			# Fake search is when we have only one search item and it is a search for _global_object_id == -1
			# this is used in some cases like the presentationDownloadManager when we dont have assets
			# in this cases we dont whant the connector to send the request to the remote easydbs only perform the search
			if search?.length == 1
				return search[0]["type"] == "in" and search[0]["fields"]?[0] == "_global_object_id" and search[0]?["in"][0] == "-1"
			return false

		ez5.api.search = (opts) =>
			# short-circuit calls that don't get split, before checking readiness
			# (otherwise a session_ready handler returning e.g. a type=collection
			# search would block startup waiting for the connector)
			if not opts.json_data or opts.skip_connector or isFakeSearch(opts.json_data.search)
				return @api.search(opts)

			# Sub-endpoints such as /search/parse (query language) are not object
			# searches: the response carries no objects to merge, so the split would
			# resolve the query against every remote datamodel and then die in
			# __sort. Run them against the local instance only.
			if opts.api
				return @api.search(opts)

			if opts.json_data.type and opts.json_data.type != "object"
				# only wrap type "object"

				# console.debug "not wrapping:", opts.json_data, opts.json_data.type
				return @api.search(opts)

			if not @__loadDone
				return @__deferUntilReady(opts, ez5.api.search, @api.search)

			if not @__easydbs?.length
				return @api.search(opts)

			# console.warn "wrapSearchApi:", CUI.util.copyObject(opts, true)

			dfr = new CUI.Deferred()

			if opts.debug
				#We store the debug data because in the split we will remove it from the opts object
				# and we need this data for event logs at the end.
				debugData = opts.debug

			if opts.error
				dfr.fail (responses) =>
					console.error "failed:", responses

					# we get here if all sent requests get rejected
					# we pass the first rejected on
					for response in responses
						if response.state == "rejected"
							opts.error.call(opts.error, response.args[0]) # pass the XHR
							return

					# this can happen if all requests are skipped
					opts.error.call(opts.error)

					return

			if opts.success
				dfr.done =>
					opts.success.apply(opts.success, arguments)


			reqs = @splitRequest(opts)

			promises = []

			# if JSON.stringify(reqs[0]) == JSON.stringify(opts)
			# 	# console.error "wrap id completed:", Connector.wrap
			# 	return api_search(opts)

			requestSorts = {}

			for req, idx in reqs
				if not req
					continue
				do (req, idx) =>
					if idx == 0
						easydb = instance = "local"
					else
						remoteEasyDb = @__easydbs[idx-1]
						easydb = remoteEasyDb.getDisplayname()
						instance = remoteEasyDb.getInstance()

					requestSorts[instance] = req.json_data.sort

					if req.json_data.objecttypes and req.json_data.objecttypes.length == 0
						console.debug "Skip no objecttypes:", idx, easydb
						promise = CUI.resolvedPromise("skipped")

					# else if req.json_data.search and req.json_data.search.length != opts.json_data.search.length
					# 	console.debug "Skip no search length unequal:", idx, easydb, req.json_data.search,opts.json_data.search
					# 	promise = CUI.resolvedPromise("skipped")
					else
						# console.debug "Search:", idx, easydb, req
						promise = @api.search(req)

					promises.push(promise)
					# promise.always =>
					# 	if idx == 0
					# 		console.warn("local", req, arguments)
					# 	else
					# 		console.warn(@__easydbs[idx-1].getServerUrl(), req, arguments)

			CUI.whenAll(promises)
			.done =>
				responses = arguments

				data =
					count: 0
					limit: 0
					offset: 0
					countByInstance: {}
					__instances: []


				have_data = false

				# console.debug "getting all responses", responses

				for response, idx in responses
					if response.args[0] == "skipped"
						continue

					if response.state == 'rejected'
						continue

					have_data = true

					if idx == 0
						data = response.args[0]
						data.__instances = ["local"]
						continue

					easydb = @__easydbs[idx-1]
					instance = easydb.getInstance()

					responseData = response.args[0]

					if instance != "local"
						event =
							info:
								instance: instance
								version:
									schema: ez5.schema.CURRENT.version
									save_data: Search.storeDataVersion
								request: reqs[idx].json_data
								response:
									count: responseData.count
									timings:
										request_time: responseData.request_time
										client: responseData.clientTook

						if debugData?.search
							try
								sd = debugData.search.getStoreData()
								if sd?.SearchInput?.query?.length
									easydb = @getRemoteEasydbByInstance(instance)
									if easydb.version("5.94")
										event.type = "CONNECTOR_SEARCH"
										promises.push(EventPoller.saveEvent(event, easydb))
									event.type = "CONNECTOR_REMOTE_SEARCH"
									promises.push(EventPoller.saveEvent(event))
									console.log("EVENT LOG")

					data.__instances.push(instance)

					if responseData.objecttypes
						if not data.objecttypes
							data.objecttypes = []

						for ot in responseData.objecttypes
							data.objecttypes.push(ot+'@'+instance)

					for k in ["count", "limit", "offset"]
						if responseData[k]
							data[k] = data[k] + responseData[k]

					if not data.countByInstance
						data.countByInstance = {}
					data.countByInstance[instance] = responseData["count"]

#					if resdata.offset != undefined
						 # this should be the same across all requests
#						data.offset = resdata.offset

					if responseData.objects
						if not data.objects
							data.objects = []

						for obj in responseData.objects
							data.objects.push(obj)
							obj._generated_rights = {}

							# set instance in _objecttype "_fields"
							if obj._fields
								for k, values of obj._fields
									if k.indexOf('@') > -1
										parts = k.split('@')
										k2 = parts[0]
										for value, idx in values
											values[idx] = value + '@' + parts[1]
										obj._fields[k2] = values
										delete(obj._fields[k])

							Connector.addInstanceToObject(obj, easydb)

					if responseData.aggregations
						if not data.aggregations
							data.aggregations = {}

						for key, info of responseData.aggregations

							if key.startsWith('_eas')
								if not data.aggregations[key]
									data.aggregations[key] = terms: []

								for term in responseData.aggregations[key].terms
									term_ok = false

									for _term in data.aggregations[key].terms
										if _term.term == term.term
											_term.count += term.count
											term_ok = true
											break

									if not term_ok
										data.aggregations[key].terms.push(CUI.util.copyObject(term, true))

								responseData.aggregations[key].terms.sort (a, b) =>
									CUI.util.compareIndex(b.count, a.count)

								continue


							switch key
								when '_objecttype', '_mask', '_result_table_masks'

									if not data.aggregations[key]
										data.aggregations[key] = terms: []

									for term in responseData.aggregations[key].terms
										term.term = term.term + '@' + instance
										term._instance = instance

										data.aggregations[key].terms.push(term)

								when '_asset'
									if not data.aggregations[key]
										data.aggregations[key] = terms: []

									terms = data.aggregations[key].terms
									for term in responseData.aggregations[key].terms
										aTerm = terms.filter((_term) -> _term.term == term.term)[0]
										if not aTerm
											aTerm =
												term: term.term
												count: 0
											terms.push(aTerm)
										aTerm.count += term.count
								else
									if key.startsWith('_pool')
										if not data.aggregations[key]
											data.aggregations[key] = count: 0, linked_objects: []
										else
											data.aggregations[key].count = data.aggregations[key].count +
												responseData.aggregations[key].count

										for linked_object in responseData.aggregations[key].linked_objects
											linked_object._instance = instance
											linked_object._id = linked_object._id + '@' + instance
											for p in linked_object._path
												p._id = p._id + '@' + instance
											data.aggregations[key].linked_objects.push(linked_object)
									else
										data.aggregations[key] = info


				if not have_data
					console.warn "Connector received no data.", opts
					dfr.reject(responses)
					return
				# console.debug "return:", data
				# console.error "wrap id completed:", Connector.wrap
				@__sort(data, requestSorts)
				return dfr.resolve(data)

			dfr.promise()
		return

	__sort: (data, requestSorts) ->
		getSortDirection = (instance = "local", level) ->
			sorts = requestSorts[instance]
			if sorts.length == 1
				return sorts[0].order
			sort = sorts.filter((_sort) -> _sort._level == level)[0]
			return sort?.order or "ASC"

		compare = (objectOne, objectTwo, level = 0) ->
			valueOne = objectOne._sort[level]
			valueTwo = objectTwo._sort[level]

			if not valueOne and not valueTwo
				return 0

			compareValue = CUI.util.compareIndex(valueOne, valueTwo)
			if compareValue == 0
				return compare(objectOne, objectTwo, ++level)

			sortDirection = getSortDirection(objectOne._instance, level)
			if sortDirection == "DESC"
				return -compareValue

			return compareValue

		data.objects.sort((objectOne, objectTwo) =>
			objectTwo._sort = objectTwo._sort?.filter((val) -> val != null)
			objectOne._sort = objectOne._sort?.filter((val) -> val != null)

			if not objectOne._sort and not objectTwo._sort
				return 0

			if not objectOne._sort
				return -1

			if not objectTwo._sort
				return 1

			return compare(objectOne, objectTwo)
		)

	@wrap: 0
	@global_counter: 0

	@addInstanceToObject: (obj, easydb, level = 0) ->
		instance = easydb.getInstance()

		if not obj
			return

		if level == 0
			obj._instance = instance

		if obj._objecttype
			if obj[obj._objecttype]
				obj[obj._objecttype + '@' + instance] = obj[obj._objecttype]
				delete(obj[obj._objecttype])
			obj._objecttype = obj._objecttype + '@' + instance
			obj._mask = obj._mask + '@' + instance

		if obj._tags and CUI.util.isArray(obj._tags)
			for tag in obj._tags
				tag._id = "#{tag._id}@#{instance}"

		if obj.class and obj.extension
			# EAS object? -> Add remote URL if the URL is not fully qualified
			if obj.technical_metadata # top level (not version)
				obj._id = obj._id + '@' + instance

			for k in ["url", "download_url"]
				if not CUI.util.isString(obj[k])
					continue

				if obj[k].startsWith("/")
					obj[k] = easydb.getServerUrl()+obj[k]

		for k, v of obj
			if CUI.isArray(v)
				if k.startsWith("_nested:")
					delete obj[k]
					obj[k+"@"+instance] = v
				for nested in v
					Connector.addInstanceToObject(nested, easydb, level + 1)
			else if CUI.isPlainObject(v)
				Connector.addInstanceToObject(v, easydb, level + 1)

		return

	@getSessionInfoForEvent: ->
		info =
			easydb: ez5.session.getEasydbName()
			instance: ez5.session.getInstanceGlobalName()

		if ez5.session.user
			info.user_displayname = ez5.session.user.getDisplayText()
			info.user_id = ez5.session.user.getId()
			info.user_displayname = ez5.session.user.getDisplayText()
			info.user_email = ez5.session.user.getPrimaryEmail()

		return info


ez5.connector = new Connector()

# store the original api functions, so that we
# can call "init" multiple times safely
ez5.connector.api.search = ez5.api.search
ez5.connector.api.suggest = ez5.api.suggest
ez5.connector.api.eas = ez5.api.eas

# wrap eagerly so calls during token negotiation can wait, not downgrade to local
ez5.connector.wrapServerRequestApi()

ez5.load_defaults =>
	# fire-and-forget; load_defaults must not wait on token negotiation
	ez5.connector.load()
	return

