class Easydb4Migration extends RootMenuApp

	@click: ->
		ez5.rootMenu.closeMenu()
		mig = new Easydb4Migration()
		mig.start()

	@label: ->
		"easydb4migration.button"

	@is_allowed: ->
		ez5.session.hasSystemRight("root", "plugin.easydb4migration.migration")

	@group: ->
		"za_other"

	@submenu: ->
		"plugins"

	__log: (type, args) =>
		msg = []
		for arg in args
			msg.push(arg+"")

		if not @__logentries
			@__logentries = [["type", "time", "msg"]]

		@__logentries.push([type, ez5.format_date_and_time(new Date(), true), msg.join(" ")])


	logwarn: (msg) =>
		console.info.apply(console, arguments)
		@__log("warn", arguments)

	loginfo: (msg) =>
		console.info.apply(console, arguments)
		@__log("info", arguments)

	logerror: (msg) =>
		console.error.apply(console, arguments)
		@__log("error", arguments)

	start: ->
		@history = []

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

		if not @__config.fylr_inst
			@__config.fylr_inst = ez5.session.getInstance().name

		if not @__config.fylr_url or
			not @__config.fylr_inst or
			not @__config.fylr_uid
				CUI.problem(text: $$("easydb4migration.check_config"))
				return

		l = CUI.parseLocation(@__config.fylr_url)
		if not l
			CUI.problem(text: $$("easydb4migration.check_config"))
			return

		if l.href.endsWith("/")
			l.href = l.href.slice(0, -1)

		@__config.fylr_url = l.href

		@__config.fylr_inst_uid = @__config.fylr_inst + '/' + @__config.fylr_uid

		@__plugins = Easydb4Migration.plugins.newPlugins(migration: @)
		@__tools = Easydb4Migration.tools.newPlugins(migration: @)

		@initSettings()

		@acquireLock()
		.done =>
			@showModal()

	getEasydb4ReferencePrefix: (column_name, source_name) ->
		if not CUI.util.isEmpty(@__settings.easydb4_reference_prefix)
			ref = @__settings.easydb4_reference_prefix+":"
		else
			ref = ""

		if column_name == "easydb4_reference"
			return source_name + ":"
		else
			return ref

	getEasydb4TableName: (tn) ->
		if not CUI.util.isEmpty(@__settings.easydb4_schema?.trim())
			schema = @__settings.easydb4_schema
		else
			schema = "public"

		return '"source.'+schema+"."+tn+'"'


	acquireLock: ->
		dfr = new CUI.Deferred()

		connect = new CUI.XHR
			timeout: 1000
			url: @__config.fylr_url+'/objectstore/acquire/'+@__config.fylr_inst_uid

		dfr.fail =>
			CUI.problem(text: "Unable to acquire lock")

		connect.start()
		.done (res) =>
			# console.debug "res:", res
			if res?.status == "open"
				dfr.resolve()
			else
				dfr.reject()

		.fail =>
			dfr.reject.apply(dfr, arguments)

		dfr.promise()

	releaseLock: ->
		connect = new CUI.XHR
			timeout: 1000
			url: @__config.fylr_url+'/objectstore/release/'+@__config.fylr_inst_uid

		connect.start()
		.fail =>
			CUI.problem(text: "Unable to release lock")

	debugResult: (result, max_width = 15) ->

		pad = (_s, idx, padchar=" ") =>
			s = ""+_s
			c = []
			width = Math.max(result.columns[idx].length, widths[idx])

			for i in [0...width]
				if i >= s.length
					c.push(padchar)
				else
					c.push(s[i])

			c.join("")

		widths = []
		for col, idx in result.columns
			if widths[idx] == undefined
				widths[idx] = col.length
			else if col.length > widths[idx]
				widths[idx] = col.length

		for row in result.rows or []
			for idx in [0...row.__len] by 1
				col = _row[idx]
				if col == null
					col = row[idx] = "<null>"

				if col.length > widths[idx]
					widths[idx] = col.length

		sum = 0
		for w, idx in widths
			widths[idx] = Math.min(w, max_width)
			sum += widths[idx]

		if sum < 80
			# add remaining space to the
			# last
			widths[widths.length-1] += 80-sum

		_row = []
		_row2 = []
		for col, idx in result.columns
			_row.push(pad(col, idx))
			_row2.push(pad("", idx, "-"))

		console.debug(_row.join(" | "))
		console.debug(_row2.join(" | "))

		imgs = result.columns.length == 1

		for row, idx in result.rows or []
			_row = []
			urls = []
			for idx in [0...row.__len] by 1
				col = row[idx]

			for col, idx in row
				_row.push(pad(col, idx))

			console.debug(_row.join(" | "))
		return


	# debug query
	dbq: (query, max_width = 15) ->
		@query(query)
		.done (result) =>
			@debugResult(result, max_width)
			return
		.fail (err) =>
			console.error(err.debug)
		"...querying..."

	getFileRootUrl: ->
		@__config.fylr_url+'/objectstore/file/'+@__config.fylr_inst_uid + '/'

	query: (query) ->

		dfr = new CUI.Deferred()

		b64DecodeUnicode = (str) =>
		    decodeURIComponent(atob(str).split('').map((c) ->
		        '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
		    ).join(''))

		connect = new CUI.XHR
			url: @getFileRootUrl()+'migrate.sqlite'
			url_data:
				query: query
		connect.start()
		.fail (err, status, debug) =>
			console.error("quey", query, "error", err, "status", status, "debug", debug, "xhr", connect)
			dfr.reject(err)
		.done (result) =>
			if not CUI.isArray(result?.rows) or
				not CUI.isArray(result?.columns)
					console.error("quey", query, "result", result, "xhr", connect)
					dfr.reject(debug: "No or unparsable data received.")
					return

			_rows = []

			for row in result.rows
				_row = {}
				_row.__len = row.length
				_rows.push(_row)

				for cell, idx in row

					if CUI.isString(cell) and not cell.startsWith("/9j/4AAQSkZJRg") # JPEG

						# decode base64
						_row[idx] = b64DecodeUnicode(cell)
					else
						_row[idx] = cell

					# store by name
					_row[result.columns[idx]] = _row[idx]


			result.rows = _rows

			dfr.resolve(result)

		dfr.promise()

	# loadSettings: ->
	# 	filename = @__info.attrs?.filename
	# 	if not filename
	# 		return

	# 	store = CUI.getLocalStorage("easydb4migration")
	# 	if settings[filename]?.settings
	# 		for k, v of @__settings
	# 			if settings[filename].hasOwnProperty(k)
	# 				@__settings[k] = v
	# 		console.debug("Loaded settings:", settings[filename])
	# 	return

	# saveSettings: ->
	# 	filename = @__info.attrs?.filename
	# 	if not filename
	# 		return

	# 	settings = CUI.getLocalStorage("easydb4migration")
	# 	if not settings
	# 		settings = {}

	# 	settings[filename] = @__settings
	# 	CUI.setLocalStorage("easydb4migration", settings)
	# 	console.debug("Saved settings:", settings)
	# 	return

	initSettings: ->
		@__settings =
			eas_version: null
			limit_main_tables: 100
			limit_collections: true
			skip_collection_with_more_objects_than: null
			export_basetypes: ["pool", "user", "group", "collection", "presentation", "tags"]
			workfolder_name: "name"
			workfolder_parent: "fk_father_id"
			where_default_table: ""
			manifest_url: @getFileRootUrl()+'manifest.json'
			easydb4_reference_prefix: ""
			easydb4_schema: ""
			post_process_plugin: undefined
			export_tool: undefined

		@__settings.target_lang = ez5.session.getConfigDatabaseLanguages()[0]?"de-DE"
		return @__settings

	showModal: ->

		fileReader = new CUI.FileReader
			onDone: (file) =>
				try
					map = JSON.parse(file.getResult()) or {}
					if map?.version != 1 # old format
						map =
							mapping: map
							version: 0

				if map.mapping?.source_table
					if map.mapping.eadb_frontend != @__info.attrs.eadb_frontend
						CUI.problem(markdown: true, text: "Mapping file for **"+@__mapping.eadb_frontend+"** not accepted.")
					else
						@__mapping = map.mapping
						if map.version >= 1
							@__settings = map.settings

						CUI.alert(markdown: true, text: "Mapping file for **"+@__mapping.eadb_frontend+"** accepted.")
						.done =>
							upload_mapping_btn.activate()
							if map.version >= 1
								update_modal()
							else
								@initMapping()
								@reloadSourceForm()

				else
					CUI.problem(text: "Mapping file not recognized.")

		fileUpload = new CUI.FileUpload
			url: @getFileRootUrl() + 'migrate.sqlite'

			onFail: (file) =>
				EAS.presentUploadError(file)

			onDone: (file) =>
				console.debug "file uploaded", arguments
				@query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
				.fail =>
					CUI.problem(text: "File not recognized as SQLITE v3.")

				.done (result) =>
					CUI.alert(markdown: true, text: "Sqlite accepted: **"+ez5.format_filesize(file.getFile().size)+"** with "+result.rows.length+" tables.")
					.done =>
						@initSettings()
						@__mapping = null
						@initMapping()
						update_modal()

			onAlways: =>
				file_upload_btn.stopSpinner()

			onAdd: (file, idx, count) =>
				file_upload_btn.startSpinner()
				connect = new CUI.XHR
					method: "DELETE"
					url: @getFileRootUrl()

				connect.start()
				.done =>
					console.debug "adding file", file, idx, count
				.fail =>
					file_upload_btn.stopSpinner()
					CUI.problem(text: "Unable to delete filestore.")


		fileUploadAny = new CUI.FileUpload
			url: @__config.fylr_url + '/objectstore/file/' + @__config.fylr_inst_uid + '/'

			add_filename_to_url: true

			onFail: (file) =>
				EAS.presentUploadError(file)

			onDone: (file) =>
				f = file.getFile()
				console.debug "file uploaded", file, f
				CUI.alert(markdown: true, text: "File: **"+f.name+"** ["+ez5.format_filesize(f.size)+"] uploaded.")
				.done =>
					update_modal()

		file_upload_btn = new CUI.FileUploadButton
			text: "Upload Sqlite"
			icon: "upload"
			multiple: false
			fileUpload: fileUpload

		file_upload_any_btn = new CUI.FileUploadButton
			text: "Upload File"
			icon: "upload"
			multiple: false
			fileUpload: fileUploadAny

		reload_basic_btn = new CUI.Button
			text: "Analyze"
			icon: "refresh"
			disabled: true
			onClick: =>
				update_modal()

		download_csv_btn = new CUI.Button
			text: "Download Datamodel"
			disabled: true
			onClick: =>
				@downloadDatamodel()
			icon: "download"

		reset_mapping_btn = new CUI.Button
			text: "Reset Mapping"
			icon: "reset"
			disabled: true
			onClick: =>
				CUI.toaster(text: "Resetting mapping & settings...")
				@__mapping = null
				@initMapping()
				@initSettings()
				update_modal()

		download_mapping_btn = new CUI.Button
			text: "Download Mapping"
			icon: "download"
			disabled: true
			onClick: (ev) =>
				@downloadMapping(ev)

		upload_mapping_btn = new CUI.FileUploadButton
			fileUpload: fileReader
			multiple: false
			text: "Upload Mapping"
			icon: "upload"
			disabled: true

		export_json_btn = new CUI.Button
			text: "Export JSON"
			icon: "download"
			disabled: true
			onClick: =>
				export_json_btn.startSpinner()

				# console.debug CUI.util.dump(@__settings)
				# console.debug CUI.util.dump(@__info.fylr_files)

				done_delete = 0
				deletes = []
				for file in @__info.fylr_files
					if file.name == "migrate.sqlite" or not file.name.endsWith(".json")
						continue

					connect = new CUI.XHR
						method: "DELETE"
						url: @getFileRootUrl()+file.name

					promise = connect.start()
					deletes.push(promise)

					promise.done =>
						done_delete++
						ez5.splash.show("easydb4migration.progress", payload: "Deleting "+done_delete+"/"+deletes.length+" files on FYLR.")

				if deletes.length > 0
					ez5.splash.show("easydb4migration.progress", payload: "Deleting "+done_delete+"/"+deletes.length+" files on FYLR.")
				CUI.when(deletes)
				.fail =>
					export_json_btn.stopSpinner()
					CUI.problem(text: "Error deleting files on FLYR.")
					ez5.splash.hide()
					export_json_btn.stopSpinner()

				.done =>
					@exportJSON()
					.always =>
						export_json_btn.stopSpinner()
						update_modal()

		openJsonImporterModal = new CUI.Button
			text: "Open JSON Importer"
			disabled: true
			onClick: =>
				(new JSONImporter(manifestUrl: @__settings.manifest_url)).show()

		debug_sqlite_btn = new CUI.Button
			text: "Sqlite Browser"
			icon: "search"
			disabled: true
			onClick: =>
				@sqliteBrowser()

		update_modal = =>

			show_content = =>

				if @__info.sqlite_tables.length == 0
					# no sqlite file or buggy
					content = new CUI.Label
						text: "Upload sqlite file from easydb4."
						multiline: true
						center: true

					download_csv_btn.disable()
					download_mapping_btn.disable()
					reload_basic_btn.disable()
					reset_mapping_btn.disable()
					upload_mapping_btn.disable()
					debug_sqlite_btn.disable()
					export_json_btn.disable()
					openJsonImporterModal.disable()

					mod.setContent(content)
					return

				fylr_url =  @getFileRootUrl()

				content = []

				ul = CUI.dom.element("UL")
				content.push("Files on FYLR:")
				content.push(ul)

				@__info.fylr_files.sort (a, b) =>
					a.name.localeCompare(b.name)

				for file in @__info.fylr_files
					a = CUI.dom.element("A")

					a.target = "_blank"
					a.href = fylr_url+file.name
					a.textContent = file.name+" ["+ez5.format_filesize(file.size)+"]"

					li = CUI.dom.element("LI")
					if not file.name.endsWith(".json") or
						file.name == "manifest.json" or
						file.name.startsWith(@__info.attrs.EASYDB_DEFAULT_TABLE+"-")
							strong = CUI.dom.element("STRONG")
							strong.appendChild(a)
							li.appendChild(strong)
					else
						li.appendChild(a)

					ul.appendChild(li)

				# List of tables
				pre = CUI.dom.element("PRE")
				info = []
				info.push("Attrs:")
				for k, v of @__info.attrs
					info.push(k+": "+v)

				info.push("")
				info.push("Object tables:")
				for table in @__info.tables
					if table.internal
						continue

					info.push(table.target_name+" ("+table.count+")")

				info.push("")
				info.push("")
				info.push("Problems:")
				if @__info.problems.length
					info.push(@__info.problems.join("\n"))
				else
					info.push("None found.")

				info.push("")

				pre.textContent = info.join("\n")
				content.push(pre)

				download_csv_btn.enable()
				download_mapping_btn.enable()
				reload_basic_btn.enable()
				reset_mapping_btn.enable()
				upload_mapping_btn.enable()
				debug_sqlite_btn.enable()
				export_json_btn.enable()
				openJsonImporterModal.enable()

				eas_options = [
					text: "- no eas -"
					value: null
				]

				for eas_version in @__info.eas_versions
					eas_options.push
						text: eas_version
						value: eas_version

				post_process_opts = [
					text: "- none -"
					value: null
				]

				@__plugin_by_name = {}

				for plugin in @__plugins
					post_process_opts.push
						text: plugin.name()
						value: plugin.name()

					if @__info.attrs.eadb_frontend == plugin.name() and
						@__settings.post_process_plugin == undefined
							@__settings.post_process_plugin = plugin.name()

					@__plugin_by_name[plugin.name()] = plugin

				post_process_opts.sort (a, b) ->
					a.text.localeCompare(b.text)


				export_tool_opts = [
					text: "- none -"
					value: null
				]

				for tool in @__tools
					export_tool_opts.push
						text: tool.name()
						value: tool.name()

				limit_options = [
					text: "no limit: "+@__info.table_by_name[@__info.attrs.EASYDB_DEFAULT_TABLE]?.count
					value: null
				,
					text: "10"
					value: 10
				,
					text: "100"
					value: 100
				,
					text: "1000"
					value: 1000
				]

				source_table_options = []
				for table in @__info.tables
					if table.is_source
						txt = table.source_name
						if table.editlink
							txt = txt + " [editlink]"

						txt += " -> "+table.target_name_full

						source_table_options.push
							text: txt
							value: table.source_name

				source_table_options.sort (a, b) ->
					a.text.localeCompare(b.text)

				basetype_options = [
					text: "Users"
					value: "user"
				,
					text: "Groups"
					value: "group"
				,
					text: "Pools"
					value: "pool"
				,
					text: "Collection"
					value: "collection"
				,
					text: "Presentation"
					value: "presentation"
				,
					text: "Tags"
					value: "tags"
				]

				fields = [
					form:
						label: "FYLR."
					type: CUI.Output
					name: "fylr_url"
					data: @__config
				,
					form:
						label: "Backup origin"
					type: CUI.Output
					data: @__info.attrs
					name: "SERVER_NAME"
				,
					form:
						label: "Backup filename"
					type: CUI.Output
					data: @__info.attrs
					name: "filename"
				,
					form:
						label: "Backup timestamp"
					type: CUI.Output
					data: @__info.attrs
					name: "timestamp"
				,
					form:
						label: "easydb 4"
					type: CUI.Output
					data: @__info.attrs
					name: "eadb_frontend"
				# ,
				# 	form:
				# 		label: "Manifest URL"
				# 	type: CUI.Input
				# 	name: "manifest_url"
				# 	readonly: true
				,
					form:
						label: "Include EAS"
					type: CUI.Select
					options: eas_options
					name: "eas_version"
				,
					form:
						label: "Include Basetypes"
					type: CUI.Options
					name: "export_basetypes"
					min_checked: 0
					horizontal: false
					options: basetype_options
				,
					form:
						label: "Limit Main Tables:"
					type: CUI.Select
					name: "limit_main_tables"
					options: limit_options
				,
					form:
						label: "Limit collections to available objects:"
					type: CUI.Checkbox
					name: "limit_collections"
				,
					form:
						label: "Skip collection with objects > n:"
					type: CUI.NumberInput
					name: "skip_collection_with_more_objects_than"
				,
					form:
						label: "WHERE clause for: "+@__info.attrs.EASYDB_DEFAULT_TABLE
					type: CUI.Input
					textarea: true
					name: "where_default_table"
				,
					form:
						label: "Prefix for easydb4_reference"
					type: CUI.Input
					name: "easydb4_reference_prefix"
				,
					form:
						label: "Schema of easydb4_tables"
					type: CUI.Input
					placeholder: "public"
					name: "easydb4_schema"
				,
					form:
						label: "Post Process"
					type: CUI.Select
					name: "post_process_plugin"
					options: post_process_opts
				,
					form:
						label: "Use Export Tool"
					type: CUI.Select
					name: "export_tool"
					options: export_tool_opts
				,
					form:
						label: "Export Language Basetypes & Loca Default"
					type: CUI.Select
					options: (value: lang for lang in ez5.session.getConfigDatabaseLanguages())
					name: "target_lang"
				]

				form = new CUI.Form
					data: @__settings
					fields: fields

				@__sourceTableForm = new CUI.Form
					data: @__settings
					padded: true
					fields: [
						form:
							label: "Source Table"
						type: CUI.Select
						maximize_horizontal: true
						name: "source_table_name"
						onDataChanged: =>
							@reloadSourceForm()
						options: source_table_options
					]

				@initMapping()

				@__sourceTableForm.start()

				@__formLayout = new CUI.HorizontalLayout
					left:
						content: new CUI.VerticalLayout
							maximize: true
							center:
								content: form.start()
					right:
						content: new CUI.VerticalLayout
							maximize: true
							center:
								content: content
						flexHandle:
							state_name: "easydb4migration-hl-right"
							label:
								text: "Info"
							closed: true
							closable: true
							hidden: false
					center:
						content: [
							@__sourceTableForm
							@renderSourceForm()
						]

				if mod.isDestroyed()
					return

				mod.setContent(@__formLayout)
				return

			@getBasicInfo()
			.fail (err) =>
				console.error("basic info failed:", err)
				CUI.problem(text: "Failed to analyze: "+(err?.debug or JSON.stringify(err, "   ")))
			.always =>
				show_content()


		mod = new CUI.Modal
			cancel: true
			fill_space: "both"
			class: "ez5-easydb-migration-suite"
			onCancel: =>
				@releaseLock()
				# close even if the lock release failed, so we can re-open to re-acquire
				# if another windows has played with the log.
				return
			pane:
				title: "Easydb 4 Migration Suite"
				padded: true
				footer_left: [
					file_upload_btn
					file_upload_any_btn
					reload_basic_btn
					download_csv_btn
					download_mapping_btn
					reset_mapping_btn
					upload_mapping_btn
					debug_sqlite_btn
				]
				footer_right: [
					export_json_btn
					openJsonImporterModal
				]

		mod.show()
		update_modal()

		return

	reloadSourceForm: ->
		form = @renderSourceForm()

		@__formLayout.replace([
			@__sourceTableForm
			form
		], "center")
		return

	getTableByName: (name) ->
		if not name
			return null

		enrich = (table) ->
			if table._column_by_name
				return table
			table._column_by_name = {}
			for col in table.columns
				if not col.kind
					col.kind = "column"
				table._column_by_name[col.name] = col
			return table

		tb = switch name
			when "basetype:user"
				name: "basetype:user"
				columns: [
					name: "login"
				,
					name: "_new_primary_email"
				,
					name: "displayname"
				,
					name: "reference"
				,
					name: "first_name"
				,
					name: "last_name"
				,
					name: "phone"
				,
					name: "postal_code"
				,
					name: "house_number"
				,
					name: "address_supplement"
				,
					name: "street"
				,
					name: "town"
				,
					name: "country"
				,
					name: "remarks"
				,
					name: "_password"
				]
			else
				ez5.schema.CURRENT._table_by_name[name]

		if not tb
			return

		enrich(tb)

	initMapping: ->
		if not @__mapping
			@__mapping =
				source_table: {}

		@__mapping.eadb_frontend = @__info.attrs.eadb_frontend

		for source_table in @__info.tables
			if not source_table.is_source
				continue

			tb_mapping = @__mapping.source_table[source_table.source_name]

			if not tb_mapping # or not @getTableByName(tb_mapping.target_table_name)
				tb_mapping = @__mapping.source_table[source_table.source_name] =
					target_table_name: null

				target_table = @getTableByName(source_table.target_name_full)
				tb_mapping.target_table_name = target_table?.name

			target_table = @getTableByName(tb_mapping.target_table_name)
			if not target_table
				tb_mapping.target_table_name = undefined

			if target_table and not tb_mapping.source_column
				tb_mapping.source_column = {}

				for col in source_table.columns
					if not col.source_name
						continue

					if col.target_name
						target_col = target_table._column_by_name[col.target_name]
						if target_col
							target_col_name = target_col.name
							if target_col.type == "text_l10n" or
								target_col.type == "text_l10n_oneline"
									# add language
									target_col_name = target_col_name + "#" + @__settings.target_lang
						else
							target_col_name = null
					else
						target_col_name = null

					# console.debug "source:", col.source_name, "target: ", target_col_name, col, target_table

					tb_mapping.source_column[col.source_name] =
						target_column_name: target_col_name

		console.debug "Mapping:", @__mapping
		console.debug "Info:", @__info
		console.debug "Settings:", @__settings
		return

	getSettings: ->
		@__settings

	renderSourceForm: ->
		source_table = @__info.table_by_name[@__settings.source_table_name]
		if not source_table
			return null

		tb_mapping = @__mapping.source_table[source_table.source_name]

		target_table = @getTableByName(tb_mapping.target_table_name)

		target_table_opts = [
			text: "- not mapped -"
			value: null
		,
			text: "basetype:user"
			value: "basetype:user"
		]

		tables = ez5.schema.CURRENT.tables.slice(0)

		tables.sort (a, b) ->
			a.name.localeCompare(b.name)

		for ot in tables
			if ot.owned_by
				info = " [editlink]"
			else
				info = ""

			target_table_opts.push
				text: ot.name+info
				value: ot.name

		target_column_opts = [
			text: ""
			value: null
		]

		if target_table
			have_uplink = false
			for col in source_table.columns
				if col.uplink
					have_uplink = true
					break

			if target_table.owned_by and not have_uplink
				target_column_opts.push
					text: "- uplink -"
					value: "__UPLINK__"

			tcols = []
			for col in target_table.columns.slice(0)
				if col.kind != "column"
					continue
				tcols.push(col)

			tcols.sort (a, b) ->
				a.name.localeCompare(b.name)

			for col in tcols

				if col.type
					txt = col.name+" ["+col.type+"]"
				else
					txt = col.name

				if col.type == "text_l10n" or
					col.type == "text_l10n_oneline"
						for lang in ez5.session.getConfigDatabaseLanguages()
							target_column_opts.push
								text: col.name+" ["+col.type+"#"+lang+"]"
								value: col.name+"#"+lang
				else
					target_column_opts.push
						text: txt
						value: col.name

				if col.type == "daterange"
					target_column_opts.push
						text: col.name+" [daterange#from]"
						value: col.name+"#from"

					target_column_opts.push
						text: col.name+" [daterange#to]"
						value: col.name+"#to"


		# console.debug "source:", source_table, "target:", target_table, "mapping:", CUI.util.dump(tb_mapping)

		rows = []

		for col in source_table.columns
			if not col.source_name
				continue

			target_type = col.target_type

			if col.uplink
				target_column = new CUI.Label(text: "Uplink")
				target_type = ""
			else if col.parent_id
				target_column = new CUI.Label(text: "Parent")
				target_type = ""
			else if col.pool_id
				target_column = new CUI.Label(text: "Pool")
				target_type = ""
			else if col.tag
				target_column = new CUI.Label(text: "Tag")
				target_type = ""
			else if target_table

				target_column = new CUI.Select
					options: target_column_opts
					name: "target_column_name"
					data:
						target_column_name: tb_mapping.source_column[col.source_name]?.target_column_name or null
						source_name: col.source_name

					onDataChanged: (data) =>
						if not tb_mapping.source_column[data.source_name]
							tb_mapping.source_column[data.source_name] = {}
						tb_mapping.source_column[data.source_name].target_column_name = data.target_column_name
						if not data.target_column_name
							tb_mapping.source_column[data.source_name].not_mapped = true
						else
							delete(tb_mapping.source_column[data.source_name].not_mapped)

				target_column.start()

			opts = col.source_options.slice(0)
			if col.source_linkprio
				opts.push("linkprio: "+col.source_linkprio)

			rows.push
				source: col.source_name
				source_type: col.source_type
				source_options: opts.join(", ")
				target: col.target_name
				target_column: target_column
				target_type: target_type


		data =
			target_table_name: target_table?.name or null

		# console.debug "target table:", target_table, data

		form = new CUI.Form
			data: data
			padded: true
			fields: [
				form:
					label: "Target Table"
				type: CUI.Select
				name: "target_table_name"
				maximize_horizontal: true
				options: target_table_opts
				onDataChanged: =>
					console.debug "update with data:", CUI.util.dump(data), source_table.source_name
					@__mapping.source_table[source_table.source_name].target_table_name = data.target_table_name
					delete(@__mapping.source_table[source_table.source_name].source_column)

					@initMapping()
					@reloadSourceForm()
			]

		tb = new CUI.Table
			maximize: true
			columns: [
				name: "source"
			,
				name: "source_type"
			,
				name: "source_options"
			,
				name: "target"
			,
				name: "target_type"
			,
				name: "target_column"
			]
			rows: rows

		[form.start(), tb]

	sqliteBrowser: ->

		sel_data = table: null

		history_pos = 0

		unpack = (str) ->
		    bytes = []
		    for i in [0...str.length]
		        char = str.charCodeAt(i)
		        bytes.push(char >>> 8, char & 0xFF)
		    return bytes

		show_result = (result) =>
			fields = []
			for col, idx in result.columns
				fields.push
					type: CUI.Output
					form:
						label: col
					name: idx+""

			data = rows: []
			for _row in result.rows
				row = {}
				for idx in [0..._row.__len] by 1
					cell = _row[idx]
					if cell == null
						row[idx+""] = new CUI.EmptyLabel(text: "null")
						continue

					if not CUI.isString(cell)
						row[idx+""] = cell
						continue

					if cell.startsWith("/9j/4AAQSkZJRg") # JPEG signature
						img = document.createElement('img')
						img.width = 100
						img.src = "data:image/jpeg;base64," + cell
						row[idx+""] = img
					else if cell?.length > 50 and idx > 0
						row[idx+""] = cell.substr(0, 50)
					else
						try
							row[idx+""] = JSON.stringify(JSON.parse(cell), null, "   ")
						catch
							row[idx+""] = cell

				data.rows.push(row)

			console.debug("Sqlite Browser:", data)

			CUI.dom.empty(output_table_div)

			dt = new CUI.DataTable
				maximize: true
				fields: fields
				data: data
				new_rows: "none"
				name: "rows"
				chunk_size: 25
				footer_right: new CUI.Label(text: data.rows.length+" records.")

			dt.start()
			vl.replace(dt, "center")


		tb_opts = [
			text: "- Select Table -"
			value: null
		]

		for table in @__info.sqlite_tables
			tb_opts.push
				text: table
				value: table

		tb_select = new CUI.Select
			options: tb_opts
			data: sel_data
			name: "table"
			onDataChanged: =>
				inp_select.storeValue("SELECT * FROM \""+sel_data.table+"\" LIMIT 100")
				inp_select.displayValue()
				# inp_select.getElement().value = "SELECT * FROM \""+sel_data.table+"\" LIMIT 100"
				run_input()
		.start()

		inp_select = new CUI.Input
			maximize: true
			data: sel_data
			textarea: true
			name: "query"
			onKeyup: (inp, ev) =>
				if not ev.ctrlKey()
					history_pos = 0
					return


				switch ev.keyCode()
					when 13 # return
						run_input()
						return
					when 38 # cursor up
						if history_pos < @history.length
							history_pos++
					when 40 # cursor down
						if history_pos > 0
							history_pos--
					else
						return

				ev.preventDefault()

				if history_pos == 0
					inp_select.setValue("")

				console.debug "history:", @history

				sql = @history[@history.length - history_pos]
				console.debug ev.keyCode(), history_pos, sql
				if not sql
					return
				inp_select.setValue(sql)


		.start()

		go_btn = new CUI.Button
			text: "Go"
			icon: "fa-caret-right"
			onClick: =>
				run_input()

		run_input = =>
			sql = sel_data.query.trim()
			if sql.length == 0
				return

			@history.push(sql)
			inp_select.disable()
			run_query(sql)
			.always =>
				inp_select.enable()
				inp_select.focus()

		run_query = (query) =>
			go_btn.startSpinner()
			@query(query)
			.always =>
				go_btn.stopSpinner()
			.done (result) =>
				show_result(result)
			.fail (err) =>
				lb = new CUI.Label
					text: err?.debug or err?.error or "Unknown error"
					multiline: true
					centered: true
				vl.replace(lb, "center")

		hl = new CUI.HorizontalLayout
			left:
				content: tb_select
			center:
				content: inp_select
			right:
				content: go_btn

		output_table_div = CUI.dom.element("DIV")

		vl = new CUI.VerticalLayout
			maximize: true
			top:
				content: hl

		new CUI.Modal
			cancel: true
			fill_space: "both"
			pane:
				title: "Sqlite Browser"
				padded: true
				content: vl
		.show()

	sanitizeTablename: (tn) ->
		_tn = tn.toLowerCase() # .replace(/__/g, "_")

		if _tn.startsWith("_")
			return _tn.substr(1)

		return _tn

	sanitizeColumnname: (cn) ->
		if cn in @track_create_fields
			return cn.replace(/easydb_/, "easydb4_")

		if cn.match(/^[0-9]/)
			cn = "c"+cn

		return @sanitizeTablename(cn)

	# first letter capialized
	displayname: (cn) ->
		names = []
		for part in cn.split("_")
			name = part.substr(0,1).toUpperCase()+part.substr(1)
			name = name.replace(/Easydb/g,"easydb")
			names.push(name)

		return names.join(" ")

	getBasicInfo: ->

		@__info = info =
			sqlite_tables: []
			eas_versions: []
			min_eas_version: undefined
			tables: []
			table_by_name: {}
			table_by_id: {}
			pool_tables: []
			tag_tables: []
			problems: []
			base_tables: {}
			attrs: {}
			editlinks: {}
			user_by_ref: {} # this is filled in export_users
			group_by_id: {} # this is filled in export_groups

		dfr = new CUI.Deferred()

		ez5.splash.show("easydb4migration.analysing", progress: "...")

		dfr.always =>
			ez5.splash.hide()

		get_sqlite_tables = =>
			@query("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
			.done (result) =>
				info.sqlite_tables = (row.name for row in result.rows)


		get_editlinks = =>
			@query("""
			SELECT a.id as mask_id, a.name, b.* FROM "source.eadb_masks" a
			   LEFT JOIN "source.eadb_attrs" b ON
			    (b.referer='MA_'||a.id AND b.keystr IN ('link_field_id', 'table_id'))
			WHERE a.type='editlinks'"""
			)
			.done (result) =>
				attr_by_mask = {}
				for row in result.rows
					if not attr_by_mask[row.referer]
						attr_by_mask[row.referer] =
							name: row.name
							mask_id: row.mask_id

					attr_by_mask[row.referer][row.keystr] = parseInt(row.value)

				for referer, _info of attr_by_mask
					if not info.editlinks[_info.table_id]
						info.editlinks[_info.table_id] = []
					info.editlinks[_info.table_id].push(_info)

				return
				console.debug "editlinks: ", result, attr_by_mask


		# SELECT *, (select table_name || '.' || name from "source.eadb_columns" where id=value) FROM "source.eadb_attrs" where referer='MA_2386' and keystr='link_field_id' LIMIT 100

		get_eas_versions = =>
			@query("SELECT DISTINCT file_version FROM \"file\"")
			.done (result) =>
				info.eas_versions = (row.file_version for row in result.rows)
				min_eas_version = undefined

				for eas_version in info.eas_versions
					v = parseInt(eas_version)
					if isNaN(v)
						continue

					if min_eas_version == undefined or v < min_eas_version
						min_eas_version = v

				if min_eas_version != undefined
					info.min_eas_version = ""+min_eas_version

				return

		get_tables = =>

			@query("""
			SELECT a.table_name, a.id as column_id, a.name, a.type, a.linkprio, a.options,
				   b.id as table_id, b.screen_name, b.options as table_options
				     FROM "source.eadb_columns" a
				     LEFT JOIN "source.eadb_tables" b ON (b.name = a.table_name)
					 ORDER BY table_name, a.name
			""")

			.done (result) =>

				tn = null
				for row in result.rows
					if tn != row.table_name
						tn = row.table_name

						table_options = row.table_options?.split(",") or []

						switch tn
							when @__info.attrs.USER_TABLE_NAME
								target_name = "basetype:user"
							else
								target_name = @sanitizeTablename(tn)

						# if "internal" in table_options and tn.indexOf("__") > -1 or
						# 	tn.startsWith(@__info.attrs.EASYDB_DEFAULT_TABLE+"__")

						tb =
							editlink: false
							real_source_name: tn
							source_name: tn
							source_id: row.table_id
							hierarchical: "treesupport" in table_options
							pool: false
							target_name: target_name
							target_displayname: row.screen_name or row.table_name
							screen_name: row.screen_name
							columns: []
							_nested_tables: []
							_linked_tables: []

						editlinks = @__info.editlinks[row.table_id]

						if editlinks
							CUI.util.pushOntoArray("internal", table_options)

							parts = tn.split("__")
							tb.real_target_name = @sanitizeTablename(parts.pop())
							tb.target_name = tb.real_target_name
							tb.editlink = true
							tb.editlinks = editlinks

						if @__info.attrs.USER_TABLE_NAME == tn
							tb.columns.push
								real_source_name: "displayname"
								source_name: "displayname"
								target_name: "displayname"
								source_type: "text"
								source_options: []
						else
							if CUI.util.idxInArray(@getEasydb4TableName("eadb_changelog"), @__info.sqlite_tables) > -1
								tb.columns.push
									real_source_name: "__changelog__"
									source_name: "__changelog__"
									target_name: "easydb4_changelog"
									source_type: "text"
									source_options: []

						info.tables.push(tb)

					linkprio = undefined

					if row.linkprio == 1 # and tb.source_name == @__info.attrs.EASYDB_DEFAULT_TABLE
						if row.name.match(/admin.*group/)
							# stadt-hannover special case
							msg = "Ignoring linkprio == 1 on "+tb.source_name+"."+row.name
							info.problems.push(msg)
						else
							tb.pool = true
							linkprio = 1

					if row.linkprio == 2
						tb.tags = true
						linkprio = 2

					col =
						real_source_name: row.name
						source_name: row.name
						source_id: row.column_id
						target_name: @sanitizeColumnname(row.name)
						source_type: row.type
						source_options: row.options?.split(",") or []
						source_linkprio: linkprio

					if tn == @__info.attrs.USER_TABLE_NAME
						switch col.source_name
							when @__info.attrs.USER_LOGIN_COLUMN
								col.target_name = "login"
							when "password"
								col.target_name = "_password"
							when "email"
								col.target_name = "_new_primary_email"

					if row.type == "key"
						col.key = true

						if tn == @__info.attrs.USER_TABLE_NAME
							col.target_name = "reference"
						else
							col.target_name = "easydb4_reference"

						tb.key = col

					if row.name == "fk_father_id"
						tb.parent = col
						col.parent_id = true
						col.source_type = "parent"
						col.target_type = "number"

					tb.columns.push(col)


				# split multiple editlinks in two tables
				_idx = 0
				while _idx < info.tables.length

					table = info.tables[_idx]
					len = table.editlinks?.length
					if not len
						_idx += 1
						continue

					if len == 1
						table.link_field_id = table.editlinks[0].link_field_id
					else
						link_field_ids = (el.link_field_id for el in table.editlinks)

						# create an extra table for each editlink
						for editlink, idx in table.editlinks
							app = "_"+editlink.mask_id

							if idx == 0
								tb = table
							else
								tb = CUI.util.copyObject(table, true)
								info.tables.splice(_idx, 0, tb)
								_idx += 1

							for col in tb.columns
								if col.source_id in link_field_ids and col.source_id != editlink.link_field_id
									# we don't map this, it belongs to the another editlink
									col.target_name = null

							tb.source_name = tb.real_source_name + app
							tb.target_name = tb.real_target_name + app
							tb.link_field_id = editlink.link_field_id

					_idx += 1


				for table in info.tables
					info.table_by_name[table.source_name] = table
					info.table_by_id[table.source_id] = table

				for table in info.tables
					idx = 0
					while idx < table.columns.length
						col = table.columns[idx]

						if col.source_name.endsWith("_date_bis")

							date_col_name = col.source_name.substr(0, col.source_name.length-9)

							find = {}
							find[date_col_name] = true
							for k in ["date_bis", "date_von", "jahr_bis", "jahr_von"]
								find[date_col_name+"_"+k] = true

							for _col, _idx in table.columns
								if _col.source_name == date_col_name
									main_idx = _idx

								if find[_col.source_name]
									_col[k] = true
									delete(find[_col.source_name])

							if Object.keys(find).length > 0
								info.problems.push(table.source_name+"."+col.source_name+": Not enough date range columns found.")
							else
								table.columns.splice main_idx+1, 0,
									target_type: "daterange"
									real_source_name: date_col_name
									source_name: date_col_name+"_*"
									target_name: date_col_name+"_range"
									target_displayname: @displayname(date_col_name)
									source_options: []
								idx += 1

						if col.target_name
							col.target_displayname = @displayname(col.target_name)

						linked_table = null

						switch col.source_type
							when "parent"
								type = "number"
							when "date"
								type = "date"
							when "boolean"
								type = "boolean"
							when "easfile"
								type = "eas"
							when "time"
								type = "datetime"
							when "blob"
								type = "text"
							when "key"
								type = "number"
							when "text"
								type = "text_oneline"
							when "integer", "number"
								type = "number"
							when "currency"
								type = "integer.2"
							when "image"
								# old eas
								type = "number"
							else
								if col.source_type.startsWith("link_")
									tb_id = col.source_type.substr(5)

									linked_table = info.table_by_id[tb_id]

									if not linked_table
										info.problems.push(table.source_name+"."+col.source_name+": Table for "+col.source_type+" not found.")
									else if linked_table.source_name == info.attrs.USER_TABLE_NAME
										info.problems.push(table.source_name+"."+col.source_name+": Unable to link to User Table: "+linked_table.source_name+".")
										type = "number"
									else if linked_table.source_name == info.attrs.GROUP_TABLE_NAME
										info.problems.push(table.source_name+"."+col.source_name+": Unable to link to Group Table: "+linked_table.source_name+".")
										type = "number"
									else if not table.editlink and linked_table.editlink
										info.problems.push(table.source_name+"."+col.source_name+": Unable to link to Editlink Table: "+linked_table.source_name+".")

									else
										col.source_link = linked_table
										type = "link: "+linked_table.target_name
										col.target_displayname = linked_table.target_displayname
								else
									msg = table.source_name+"."+col.source_name+": Unknown type: "+col.source_type
									info.problems.push(msg)
									type = "unknown"
									@logwarn(msg)

						if not col.target_type
							col.target_type = type

						if col.source_linkprio == 1
							if not linked_table
								info.problems.push(table.source_name+"."+col.source_name+": Table for linkprio == 1 not found.")
							else
								# table ids > 10,7 are "remote tables" in easydb4
								# and never used as pool tables
								if linked_table?.source_id < Math.pow(10,7)
									if linked_table not in info.pool_tables
										info.pool_tables.push(linked_table)
										linked_table.pool_table = true
									col.pool_id = true

						if col.source_linkprio == 2
							if not linked_table
								info.problems.push(table.source_name+"."+col.source_name+": Table for linkprio == 2 not found.")
							else
								CUI.util.pushOntoArray(linked_table, info.tag_tables)
								linked_table.tag_table = true
								# we export this table alongside with creating tags,
								# in some cases the easydb4 datamodel has multiple links into this
								# field, some set source_linkprio, others do not
								#
								# linked_table.internal = true
								col.tag = true

						idx++


				for table in info.tables

					if table.editlink
						table.internal = true

					if table.pool_table
						table.internal = true

					if table.target_name == "connector"
						table.internal = true

					if table.target_name == "customrender"
						table.internal = true

					if table.target_name == "automator"
						table.internal = true

					owned_by = null
					owned_by_col = null

					# Find non-editlink like table name "Workfolder2_Bilder"
					for col in table.columns

						# set pools on tables which don't have linkprio 1
						if col.source_link?.pool_table and not col.pool_id
							console.debug "col.source_link", table.real_source_name, col.source_link?.pool_table, col.source_link

							col.pool_id = true
							table.pool = true

						if info.attrs.WORKFOLDER2_DEFAULT_TABLE
							if col.source_link?.source_name == info.attrs.WORKFOLDER2_DEFAULT_TABLE
								table.editlink = true
								owned_by = col.source_link
								owned_by_col = col
								break
						if info.attrs.WORKFOLDER_SECOND_TABLE
							if col.source_link?.source_name == info.attrs.WORKFOLDER_SECOND_TABLE
								table.editlink = true
								owned_by = col.source_link
								owned_by_col = col
								break
						if info.attrs.POWERPOINT_TABLE_NAME
							if col.source_link?.source_name == info.attrs.POWERPOINT_TABLE_NAME
								table.editlink = true
								owned_by = col.source_link
								owned_by_col = col
								break

					if not table.editlink
						continue

					if not owned_by
						for col in table.columns
							if col.source_id == table.link_field_id
								owned_by = col.source_link
								owned_by_col = col
								break

					if owned_by
						owned_by_col.uplink = true
						table.uplink = owned_by_col
						table.owned_by = owned_by
						owned_by._nested_tables.push(table)
					else
						# console.debug "table:", table, "column:", col
						msg = "Owned by not found for assumed editlink table: "+table.source_name
						info.problems.push(msg)
						@logwarn(msg, table)

				# add easydb4 reference columns
				for table in info.tables
					table.columns.splice 0, 0,
						target_type: "string"
						target_name: "easydb4_reference"
						target_displayname: "easydb4 Referenz"
						source_options: [
							"unique"
						]

				for table in info.tables

					if info.attrs.POWERPOINT_TABLE_NAME
						if table.source_name == info.attrs.POWERPOINT_TABLE_NAME
							info.base_tables.presentation = table
							table.internal = true
							table.presentation = true

						if table.owned_by?.source_name == info.attrs.POWERPOINT_TABLE_NAME
							info.base_tables.presentation_objects = table
							info.attrs.presentation_objects = table.source_name
							table.internal = true
							table.presentation = true

					if table.source_name == info.attrs.USER_TABLE_NAME
						info.base_tables.user = table
						table.internal = true

					if table.source_name == info.attrs.GROUP_TABLE_NAME
						info.base_tables.group = table
						table.internal = true

					if info.attrs.WORKFOLDER2_DEFAULT_TABLE
						if table.source_name == info.attrs.WORKFOLDER2_DEFAULT_TABLE
							info.base_tables.workfolder = table
							table.internal = true
							table.workfolder = true

						if table.owned_by?.source_name == info.attrs.WORKFOLDER2_DEFAULT_TABLE
							info.base_tables.workfolder_objects = table
							info.attrs.workfolder_objects = table.source_name
							table.internal = true
							table.workfolder = true

					if info.attrs.WORKFOLDER_SECOND_TABLE
						if table.source_name == info.attrs.WORKFOLDER_SECOND_TABLE
							info.base_tables.workfolder2 = table
							table.internal = true
							table.workfolder = true

						if table.owned_by?.source_name == info.attrs.WORKFOLDER_SECOND_TABLE
							info.base_tables.workfolder2_objects = table
							info.attrs.workfolder2_objects = table.source_name
							table.internal = true
							table.workfolder = true


					if @__settings.workfolder2_table
						if table.source_name == @__settings.workfolder2_table
							info.base_tables.workfolder2 = table
							info.attrs.workfolder2 = table.source_name
							table.internal = true
							table.workfolder = true

						if table.owned_by?.source_name == @__settings.workfolder2_table
							info.base_tables.workfolder2_objects = table
							info.attrs.workfolder2_objects = table.source_name
							table.internal = true
							table.workfolder = true

					if info.attrs.EASYDB_DEFAULT_TABLE
						if table.source_name == info.attrs.EASYDB_DEFAULT_TABLE
							info.base_tables.easydb_default = table

				return

		get_attrs = =>

			keys = []
			for referer, keystrs of {
				DEFAULT_VALUE: [
					"USER_TABLE_NAME"
					"USER_LOGIN_COLUMN"
					"GROUP_TABLE_NAME"
					"GROUP_TABLE_DISPLAY_COLUMN_NAME"
					"EASYDB_DEFAULT_TABLE"
				]
				ST: [
					"WORKFOLDER_SECOND_TABLE"
					"WORKFOLDER2_DEFAULT_TABLE"
					"POWERPOINT_TABLE_NAME"
				]
				SYSTEM: [
					"eadb_frontend"
				]
				BACKUP: [
					"timestamp"
					"filename"
				]
				SERVER: [
					"SERVER_NAME"
				]
			}
				for keystr in keystrs
					keys.push("'" + referer + "__" + keystr + "'")

			@query("""SELECT keystr, value FROM "source.eadb_attrs" WHERE (referer || '__' || keystr) IN ("""+keys+""")""")
			.done (result) =>
				for row in result.rows
					info.attrs[row.keystr] = row.value

				for key, value of info.attrs
					if not value
						info.problems.push("Key: "+key+" not found, unable to migrate.")

				return


		get_eadb_links = =>
			@query("SELECT DISTINCT from_table_id, to_table_id FROM "+@getEasydb4TableName("eadb_links"))
				# SELECT DISTINCT from_table_id, to_table_id FROM "source.public.eadb_links"
				#   WHERE from_table_id = to_table_id OR to_table_id IN (
				#      SELECT value
			    #        FROM "source.eadb_attrs"
				#        WHERE referer IN (
				#          SELECT 'MA_'||id FROM "source.eadb_masks" WHERE type='multilink'
				# 	   )
	            #        AND keystr= 'table_id'
				#     )
				#     AND from_table_id IS NOT NULL
			#     SELECT DISTINCT
			#      (SELECT DISTINCT from_table_id FROM "source.public.eadb_links" WHERE
			#      to_table_id = value) AS from_table_id,
			#    value AS to_table_id
			#        FROM "source.eadb_attrs"
			# 	   WHERE referer IN (
			# 	  SELECT 'MA_'||id FROM "source.eadb_masks" WHERE type='multilink')
	        #     AND keystr= 'table_id' AND from_table_id IS NOT NULL

			.done (result) =>
				for row in result.rows
					from_table = info.table_by_id[row.from_table_id]
					to_table = info.table_by_id[row.to_table_id]

					if not from_table or not to_table
						info.problems.push("eadb_links: FROM or TO table not found: "+row.from_table_id+", "+row.to_table_id)
						continue

					if from_table.real_source_name != @__info.attrs.EASYDB_DEFAULT_TABLE
						@logwarn("eadb_links: FROM table not EASYDB_DEFAULT_TABLE: "+from_table.real_source_name+". Skipping.")
						continue

						# target_type = "number"
					target_type = "link:"+to_table.target_name

					nested_table =
						_linked_tables: []
						_nested_tables: []
						internal: true
						eadb_links: true
						eadb_links_from_table_id: row.from_table_id
						eadb_links_to_table_id: row.to_table_id
						real_source_name: "eadb_links:"+to_table.target_name
						source_name: "eadb_links:"+to_table.target_name
						owned_by: from_table
						target_table: to_table
						target_name: to_table.target_name+"_eadb_links"
						columns: [
							source_name: "to_id"
							source_options: ["not_null"]
							target_name: "lk_"+to_table.target_name+"_id"
							target_type: target_type
						,
							source_name: "remark"
							source_options: []
							target_name: "remark"
							target_type: "text"
						]

					info.tables.push(nested_table)

					info.table_by_name[nested_table.source_name] = nested_table

					if to_table.editlink
						info.problems.push("eadb_links: Merging eadb_links into existing editlink: "+from_table.real_source_name+" -> "+to_table.real_source_name+". eadb_links.remark will be ignored.")

						nested_table.columns.pop() # remove remark

						nested_table.merge_with_table = to_table
						nested_table.columns[0].target_name = "__UPLINK__"
						nested_table.columns[0].uplink = true
						nested_table.target_name = to_table.target_name

					from_table._nested_tables.push(nested_table)


		order_tables = =>

			# # ignore empty top level tables
			# tables = info.tables.splice(0)

			# for table in tables
			# 	if table.internal or table.count > 0
			# 		info.tables.push(table)


			for table in info.tables
				if (table.uplink and not table.workfolder and not table.presentation and not table.tag_table) or not table.internal
					table.is_source = true
				else if table.eadb_links
					table.is_source = true
				else if info.base_tables.user == table
					table.is_source = true
				else
					table.is_source = false

				root_table = table

				if root_table.owned_by
					path = [ root_table ]
					while root_table.owned_by
						root_table = root_table.owned_by
						path.push(root_table)

					path.reverse()

					# table.nested_target_key_name = "_nested:"+(tb.target_name for tb in path).join("__")
					table.target_name_full = (tb.target_name for tb in path).join("__")
				else
					table.target_name_full = table.target_name

				for col in table.columns
					if col.source_link and
						not col.uplink and
						not col.source_link.internal
							CUI.util.pushOntoArray(col.source_link, root_table._linked_tables)

			# order tables
			have_table_names = []
			need_count = 0

			for table in info.tables
				if table.internal
					continue

				need_count += 1

			find_next_tables = =>
				next_tables = []

				for table in info.tables
					if table.internal
						continue

					if table.source_name in have_table_names
						continue

					table.depending_on_table_names = []
					for tb in table._linked_tables
						if tb.source_name not in have_table_names
							table.depending_on_table_names.push(tb.source_name)
							break

					if table.depending_on_table_names.length == 0
						next_tables.push(table)

				for next_table in next_tables
					have_table_names.push(next_table.source_name)

				# console.warn("found another", next_tables.length, have_table_names.length, need_count)

				return next_tables.length

			not_found = []
			_loop = 0
			while true
				_loop++
				if _loop == 50
					console.error "recursion"
					break

				found = find_next_tables()
				if have_table_names.length == need_count
					break

				if found == 0
					for table in info.tables
						if table.internal
							continue

						if table.source_name in have_table_names
							continue

						not_found.push(table)

					warn = []

					for tb in not_found
						warn.push("- "+tb.source_name+" needs: "+tb.depending_on_table_names.join(', '))

					msg = "Unable to order tables for export:\n"+warn.join("\n")

					info.problems.push(msg)
					@logwarn(msg)
					break

			tables = info.tables.splice(0)

			by_name = {}

			for table in tables
				if table.internal
					info.tables.push(table)
				else
					by_name[table.source_name] = table

			for table_name in have_table_names
				info.tables.push(by_name[table_name])

			not_found.sort (a, b) =>
				if a.source_name == @__info.attrs.EASYDB_DEFAULT_TABLE
					return 1
				return 0

			@__info.deferred_tables = not_found

			for tb in not_found
				info.tables.push(tb)


			return

		get_fylr_files = =>
			connect = new CUI.XHR
				url: @getFileRootUrl()
			connect.start()
			.done (result) =>
				info.fylr_files = result.files
			.fail =>
				info.fylr_files = []

		count_tables = =>
			CUI.chunkWork.call @,
				timeout: 0
				items: info.tables
				chunk_size: 1
				call: (items) ->
					count_table(items[0])

		count_table = (tb) =>
			ez5.splash.show("easydb4migration.analysing", progress: "counting "+tb.real_source_name)

			if tb.real_source_name == @__info.attrs.EASYDB_DEFAULT_TABLE and
				@__settings.where_default_table.trim().length > 0
					tb.where = " WHERE "+@__settings.where_default_table.trim()+" "
			else
				tb.where = ""

			# console.debug 'SELECT COUNT(*) FROM "source.public.'+tb.real_source_name+'"'+tb.where
			if tb.eadb_links
				sel = 'SELECT COUNT(*) FROM '+@getEasydb4TableName("eadb_links")+' WHERE to_table_id='+tb.eadb_links_to_table_id
			else
				sel = 'SELECT COUNT(*) FROM '+@getEasydb4TableName(tb.real_source_name)+tb.where

			@query(sel)
			.done (result) =>
				tb.count = result.rows[0][0]

		ez5.splash.show("easydb4migration.analysing", progress: "fylr_files")
		get_fylr_files()
		.fail(dfr.reject)
		.done =>
			get_eas_versions()
			.fail(dfr.reject)
			.done =>
				ez5.splash.show("easydb4migration.analysing", progress: "sqlite_tables")
				get_sqlite_tables()
				.fail(dfr.reject)
				.done =>
					ez5.splash.show("easydb4migration.analysing", progress: "editlinks")
					get_editlinks()
					.fail(dfr.reject)
					.done =>
						ez5.splash.show("easydb4migration.analysing", progress: "attrs")
						get_attrs()
						.fail(dfr.reject)
						.done =>
							ez5.splash.show("easydb4migration.analysing", progress: "tables")
							get_tables()
							.fail(dfr.reject)
							.done =>
								ez5.splash.show("easydb4migration.analysing", progress: "eadb_links")
								get_eadb_links()
								.fail(dfr.reject)
								.done =>
									count_tables() # @__info.attrs.EASYDB_DEFAULT_TABLE)
									.fail(dfr.reject)
									.done =>
										ez5.splash.show("easydb4migration.analysing", progress: "order")
										order_tables()
										dfr.resolve(info)

		return dfr.promise()


	track_create_fields: [
		"easydb_insert_time"
		"easydb_insert_user"
		"easydb_owner"
		"easydb_update_time"
		"easydb_update_user"
	]

	resetMapping: ->

	downloadMapping: (ev) ->
		if ev.hasModifierKey()
			console.debug(CUI.util.dump(@__mapping))
		else
			map =
				mapping: @__mapping
				settings: @__settings
				version: 1

			CUI.FileReader.save("mapping-settings-easydb4-"+@__mapping.eadb_frontend+".json", JSON.stringify(map, null, "    "))

	downloadDatamodel: ->
		csv = [["# CSV DATAMODEL DUMP "+@__info.attrs.eadb_frontend+" "+ez5.format_date_and_time(new Date())]]
		csv.push([])
		csv.push([])

		if @__info.problems.length == 0
			csv.push[["# no import problems detected"]]
		else
			csv.push([["# Problems detected:"]])
			csv.push([["#"]])
			for problem in @__info.problems
				console.warn("Import Problem:", problem)
				csv.push([["# "+problem]])
			csv.push([])
			csv.push([])

		add_columns = (table, path) =>
			table_path = (tb.target_name for tb in path)

			for col in table.columns
				if col.uplink
					continue

				if col.key
					continue

				if col.parent_id
					continue

				if col.pool_id
					continue

				if col.tag
					continue

				if not col.target_name
					continue

				if table.owned_by and col.source_name in @track_create_fields
					continue

				if "unique" in col.source_options and col.target_type != "eas"
					unique = "true"
				else
					unique = ""

				if "not_null" in col.source_options
					not_null = "true"
				else
					not_null = ""

				csv.push([
					table_path.join(" > ")
					col.target_name
					col.target_type
					unique
					not_null
					col.target_displayname
				])

			for nested_table in table._nested_tables
				if nested_table.merge_with_table
					continue

				new_path = path.slice(0)
				new_path.push(nested_table)

				csv.push([ (tb.target_name for tb in new_path).join(" > "), "", "", "" ])
				add_columns(nested_table, new_path)

			return


		for table, idx in @__info.tables

			if table.internal
				continue

			csv.push(["### TABLE "+table.source_name])
			csv.push(["NAME", table.target_name])
			csv.push(["DISPLAYNAME:"+@__settings.target_lang, table.target_displayname])
			csv.push(["POOL", table.pool])
			csv.push(["HIERARCHICAL", table.hierarchical])
			csv.push(["TAGS", table.tags])
			csv.push(["SEARCH", table.source_name == @__info.attrs.EASYDB_DEFAULT_TABLE or table.pool])

			csv.push([])
			csv.push([])
			csv.push(["### COLUMNS "])
			csv.push(["TABLE", "NAME", "TYPE", "UNIQUE", "NOT_NULL", "DISPLAYNAME:"+@__settings.target_lang])

			add_columns(table, [ table ])

			csv.push([])
			csv.push([])


		_csv = new CUI.CSVData(rows: csv)
		CUI.FileReader.save("datamodel-easydb4-"+@__info.attrs.eadb_frontend+".csv", '\uFEFF'+_csv.toText())

		return

	fixLineEndings: (str) ->
		if not CUI.isString(str)
			return str

		str = str.replace(/\\r\\n/g, "\n") # this is left over in some easydb4, so we are nice and replace it
		str = str.replace(/\r\n/g, "\n")
		str = str.replace(/\r/g, "\n")
		return str.trim()

	mapObject: (row, table, obj_by_id={}) ->
		tb_mapping = @__mapping.source_table[table.source_name]
		obj = {}

		coalesce = ->
			for arg in arguments
				if not CUI.util.isEmpty(arg)
					return ""+arg
			return null

		if not tb_mapping?.source_column
			return obj

		for col in table.columns

			if not col.source_name
				continue

			col_mapping = tb_mapping.source_column[col.source_name]

			if col.uplink
				# we need to map this for our nested links
				target_column_name = col.source_name
			else
				target_column_name = col_mapping?.target_column_name

			value = @fixLineEndings(row[col.real_source_name])

			if col.key
				obj.__key = value
				obj_by_id[value] = obj

			if col.parent_id

				if value
					target = @getTargetColumn(table.source_name, table.key.source_name)
					if not target
						msg = "Unable to find target for id lookup (easydb4_reference missing?): '"+table.source_name+"'.'"+table.key.source_name+"'"
						console.error(msg, value, table, col)
						throw(msg)
						# continue

					lookup = {}
					lookup[target.column.name] = @getEasydb4ReferencePrefix(target.column.name, table.source_name)+value
					obj["lookup:_id_parent"] = lookup
				else
					obj["_id_parent"] = null

				continue

			if not target_column_name and not col.pool_id and not col.tag
				continue

			if col.key
				obj[target_column_name] = @getEasydb4ReferencePrefix(target_column_name, table.source_name)+value
				continue

			if CUI.util.isEmpty(value)
				if col.pool_id
					# put object without pool in standard
					obj._pool = pool: {}
					obj._pool.pool["lookup:_id"] = reference: "system:standard"
				continue

			target = @getTargetColumn(table.source_name, col.real_source_name)
			unique = false

			if table.target_name.startsWith("basetype:")
				target_type = col.target_type
			else if target
				target_type = target.column.type

				if target.column._unique_keys?.length == 1
					if target.column._unique_keys[0].columns.length == 1
						unique = true
						for id, _obj of obj_by_id
							if _obj[target_column_name] == value
								value = value + " ["+row[table.key.source_name]+"]"
								@logwarn("Deduplicated value: "+value+" in "+table.real_source_name+"["+id+"]")
			else
				target_type = undefined


			if target_type == "eas"

				if not @__settings.eas_version
					# skip this
					obj["__skip_"+target_column_name] = value
					continue

				urls = @__eas_urls_by_id[value]

				if urls and (urls.original or urls._original)
					obj[target_column_name] = [{}]

					obj[target_column_name][0].preferred = true

					obj[target_column_name][0]["eas:filename"] = urls.original_filename

					url = urls.original or urls._original
					url = url.replace(/([^:]\/)\/+/g, "$1") # Replace all double slashes // https://stackoverflow.com/questions/24381480
					obj[target_column_name][0]["eas:url"] = url

					# @__config.fylr_url+"/proxy?url="+
					# 	encodeURIComponent(urls.original or urls._original)

					if urls.preview_url
						obj[target_column_name][0]["eas:preview:url"] = urls.preview_url.replace(/([^:]\/)\/+/g, "$1")

					obj[target_column_name][0]["_easydb4_eas_id"] = value

					# obj[col.target_name]["eas:preview:url"] = urls.preview_url
					# obj[col.target_name]["eas:preview:base64"] = urls.preview_data
					#
				else
					msg = "EAS export: "+table.real_source_name+":"+row[table.key.source_name]+", eas id: "+value+" request version '"+@__settings.eas_version+"' not found."

					@logwarn(msg)
					obj["__not_found_"+target_column_name] = value

				continue

			if target_type == "daterange" and target.key # this is a manual daterange mapping
				dstr = CUI.DateTime.format(value, "store")
				if dstr != null
					if not obj[target.column.name]
						obj[target.column.name] = {}
					obj[target.column.name][target.key] = dstr
					console.debug dstr, obj[target.column.name]
				continue

			# we cannot rely on automated mapping here, since the real_source_name is "datierung" but
			# the target is "datierung_range", so getTargetColumn does not return the correct info
			else if col.target_type == "daterange"
				sn = col.real_source_name

				from = null
				if not CUI.util.isEmpty(row[sn+"_date_von"])
					from = CUI.DateTime.format(row[sn+"_date_von"], "store")
				jahr_von = parseInt(row[sn+"_jahr_von"])
				if not from and not isNaN(jahr_von)
					from = CUI.DateTime.format(""+jahr_von, "store")
				if not from
					@logwarn(table.source_name+":"+row[table.key.source_name]+"["+col.source_name+"]: From date not recognized, this will be stored as 'null': date_von: "+row[sn+"_date_von"]+", jahr_von: "+row[sn+"_jahr_von"])

				to = null
				if not CUI.util.isEmpty(row[sn+"_date_bis"])
					to = CUI.DateTime.format(row[sn+"_date_bis"], "store")
				jahr_bis = parseInt(row[sn+"_jahr_bis"])
				if not to and not isNaN(jahr_bis)
					to = CUI.DateTime.format(""+jahr_bis, "store")
				if not to
					@logwarn(table.source_name+":"+row[table.key.source_name]+"["+col.source_name+"]: To date not recognized, this will be stored as 'null': date_bis: "+row[sn+"_date_bis"]+", jahr_bis: "+row[sn+"_jahr_bis"])

				if from and to
					if parseInt(from) > parseInt(to)
						@logwarn(table.source_name+":"+row[table.key.source_name]+"["+col.source_name+"]: From date bigger than To date: "+from+" > "+to+". Swapping dates.")
						[from, to] = [to, from]

					if from.endsWith("-01-01") and to.endsWith("-12-31") # assume a full year range
						# keep only the year
						from = from.substr(0, 4)
						to = to.substr(0, 4)

				# console.debug "from:", from, "to:", to, "|", row[sn+"_jahr_bis"], row[sn+"_jahr_bis"], row[sn+"_date_von"], row[sn+"_date_bis"]

				if from or to
					obj[target_column_name] =
						from: from
						to: to

				continue

			if target_type == "date"
				obj[col.target_name] = value: value
				continue

			# col_mapping.target is set in the preparation of text/int > linked object conversions
			if col_mapping.target
				# text/int > linked object

				_lobj = {}

				# we allow only text and text_oneline for this conversion, so
				# we can safely (and must) cast the value to string

				_lobj[col_mapping.target.other_column.name] = value+""

				lobj = {}
				lobj["lookup:_id"] = _lobj

				lobj2 =
					_objecttype: col_mapping.target.other_table.name
					_mask: "_all_fields"

				lobj2[col_mapping.target.other_table.name] = lobj

				obj[target_column_name] = lobj2

			else if target_column_name == '__UPLINK__'
				obj[col.source_name] = value
			else if col.uplink
				obj[target_column_name] = value
			else if target_type == "custom:base.custom-data-type-gnd.gnd"

				if col.source_link.real_source_name != "dnb_normdaten"
					@logwarn("Unable to migrate GND from "+col.source_link.real_source_name+".")
					continue

				# source is a field in dnb_normdaten, we map this
				# in post process at the end
				cobj = __gnd:
					source: col.source_link.real_source_name
					key: col.source_link.key.source_name
					value: value

				obj[target_column_name] = cobj


			else if target_type == "custom:base.custom-data-type-gazetteer.gazetteer"
				gobj = __gazetteer:
					gazId: value

				obj[target_column_name] = gobj

			else if target_type == "custom:base.custom-data-type-getty.getty"
				# __getty_subject
				# __getty_term
				gobj = {}
				gobj["__"+col.source_link.source_name] =
					value: value

				obj[target_column_name] = gobj

			else if col.source_link
				# this is a link to another table
				# we need the real target table

				if col.pool_id
					obj._pool = pool: {}
					obj._pool.pool["lookup:_id"] = reference: col.source_link.source_name+":"+value
				else if col.tag
					# this will be moved one up
					obj["top:_tags"] = [{}]
					obj["top:_tags"][0]["lookup:_id"] = reference: col.source_link.source_name+":"+value
				else

					target = @getTargetColumn(col.source_link.source_name, col.source_link.key.source_name)

					if not target
						# @loginfo("Target not found for: "+table.source_name+"."+target_column_name+" -> "+col.source_link.source_name+"."+col.source_link.key.source_name)
						# lookup not found
						obj["lookup:"+target_column_name] = __not_found_easydb4_reference: col.source_link.source_name+":"+value
						continue

					# the linked objecttype might differ from the source
					# we assume that the target column uses the same reference field for id
					# as the source column

					target_column = ez5.schema.CURRENT._table_by_name[tb_mapping.target_table_name]?._column_by_name[target_column_name]
					target_link_table = ez5.schema.CURRENT._table_by_id[target_column?._foreign_key?.referenced_table?.table_id]
					if target_link_table
						ref = @getEasydb4ReferencePrefix(target.column.name, col.source_link.source_name)+value
					else
						ref = value+""

					if not target_link_table
						@logwarn("Unable to map source type link to "+table.source_name+"."+col.source_link.source_name+" > "+tb_mapping.target_table_name+"."+target_column_name+"["+target_column?.type+"]")
						switch target_type
							when "number", "integer.2"
								number = parseInt(ref)
								if isNaN(number)
									obj[target_column_name] = 0
								else
									obj[target_column_name] = number
							else
								obj[target_column_name] = ref
					else
						# console.error "migrartion: target:", table, target_column_name, target_column, target_link_table

						lobj =
							_objecttype: target_link_table.name
							_mask: "_all_fields"

						lobj[target_link_table.name] = {}
						lobj[target_link_table.name]["lookup:_id"] = {}
						lobj[target_link_table.name]["lookup:_id"][target.column.name] = ref

						obj[target_column_name+":source_name"] = col.source_link.source_name
						obj[target_column_name] = lobj

			else
				switch target_type
					when "boolean"
						if CUI.util.isTrue(value) or value == "Y" or value == "t"
							obj[target_column_name] = true
						else
							obj[target_column_name] = false
					when "number", "integer.2"
						number = parseInt(value)
						if isNaN(number)
							obj[target_column_name] = 0
						else
							obj[target_column_name] = number
					when "text", "text_oneline", "string"
						obj[target_column_name] = value+""
					when "datetime" # this should already have the correct format
						obj[target_column_name] = value: value
					when "text_l10n", "text_l10n_oneline"
						if target.key
							lang = target.key
						else
							lang = @__settings.target_lang

						if not obj[target.column.name]
							obj[target.column.name] = {}
						obj[target.column.name][lang] = value
					when "custom:base.custom-data-type-link.link"
						obj[target_column_name] = url: value
					else
						console.error("Unsupported target type: "+target_type, target, col_mapping, table, row,col, value)
						throw "Unsupported target type: "+target_type+" ("+table.source_name+"."+col.source_name+")"

		# if table.source_name == "user"
		# 	console.debug "map:", row, row._depth, CUI.util.dump(obj)

		if row._depth != undefined
			obj["top:_depth"] = row._depth

		return obj

	# move all keys, starting with "__" to top level
	mergeTopLevel: (top_level, obj) ->
		#
		obj_key = top_level._basetype or top_level._objecttype

		if not obj_key
			console.error("mergeTopLevel: No object key found:", top_level_obj)
			return obj

		if not top_level[obj_key]
			top_level[obj_key] = {}

		for k, v of obj
			if k.startsWith("top:")
				top_level[k.substr(4)] = v
			else
				top_level[obj_key][k] = v

		return

	exportJSON: ->

		master_dfr = new CUI.Deferred()
		master_dfr2 = new CUI.Deferred()

		master_dfr.fail (err) =>
			msg = "Export failed: "+JSON.stringify(err?.debug or err?.error or err)

			@logerror(msg, arguments)
			CUI.problem(text: msg)

		@loginfo("Started export JSON.")

		for problem, idx in @__info.problems
			@logwarn("Problem #"+idx+":"+problem)

		master_dfr.done =>
			@loginfo("JSON export done.")
			store_log()
			.done(master_dfr2.resolve)
			.fail(master_dfr2.reject)

		master_dfr.fail =>
			@loginfo("JSON export failed.")
			store_log()
			.always(master_dfr2.reject)

		ldap_sso_users = []
		collection_user_refs = []

		store_log = =>
			@storeTextFile("log-export-"+(@__info.attrs.eadb_frontend or "unknown")+".csv", new CUI.CSVData(rows: @__logentries).toText(delimiter: "\t", always_quote: false))
			.always =>
				delete(@__logentries)

		manifest =
			source: @__info.attrs.eadb_frontend
			batch_size: 1000
			eas_type: "url"
			has_passwords: true # easydb4 always returns user passwords (md5 hash)
			payloads: []
			__update_payloads: []

		# payload split generates multiple payloads split by depth and chunk size
		payload_split = (filename, chunk_size, json) =>

			# console.error "payload split", filename, chunk_size, json

			promises = []
			basename = ez5.bareBasename(filename)
			ext = ez5.extension(filename)
			switch json.import_type
				when "collection"
					okey = "collections"
				else
					console.error("payload_split: unsupported import_type:", json.import_type)
					return CUI.rejectedPromise()

			json_chunk =
				import_type: json.import_type

			json_chunk[okey] = []

			offset = 0

			write_file = =>
				console.debug "write file:", json_chunk[okey].length
				if json_chunk[okey].length == 0
					return

				if depth != undefined
					dep = "-depth-"+depth
				else
					dep = ""

				to = offset + json_chunk[okey].length
				promises.push(payload(basename+"-"+(offset+1)+"-"+to+dep+"."+ext, json_chunk))

				offset = offset + json_chunk[okey].length
				json_chunk[okey] = []
				return

			idx = 0
			depth = undefined
			while idx < json[okey].length
				obj = json[okey][idx]
				if obj._depth != undefined
					if depth != obj._depth
						write_file()
						depth = obj._depth

				json_chunk[okey].push(obj)
				if json_chunk[okey].length == chunk_size
					write_file()
				idx++

			write_file()
			return CUI.when(promises)


		payload = (filename, json, update=false) =>
			if filename in manifest.payloads
				console.error("Unable to add same payload twice:", filename)
				return CUI.rejectedPromise()

			if update
				manifest.__update_payloads.push(filename)
			else
				manifest.payloads.push(filename)

			dfr3 = new CUI.Deferred()

			if @__settings.post_process_plugin
				plugin = @__plugin_by_name[@__settings.post_process_plugin]
				if not plugin
					console.error("Unable to find plugin:", @__settings.post_process_plugin)
				else

					promises = []
					filename_idx = CUI.util.idxInArray(filename, manifest.payloads)

					post_promise = plugin.payload(filename, json, update, ((_filename, json, use_filename_idx) =>
						if use_filename_idx == undefined
							use_filename_idx = filename_idx

						# push this before use_filename_idx
						ext = ez5.extension(_filename)
						filename_uniq = ez5.bareBasename(_filename)+"_"+(manifest.payloads.length+1)
						if not CUI.util.isEmpty(ext)
							filename_uniq = filename_uniq + "." + ext

						manifest.payloads.splice(use_filename_idx, 0, filename_uniq)
						promise = @storeFile(filename_uniq, json)
						promises.push(promise)
						return promise
					), filename_idx)

					if post_promise != undefined
						@loginfo("Post process: "+@__settings.post_process_plugin+": "+filename)

					CUI.decide(post_promise)
					.fail(dfr3.reject)
					.done (opts = {}) =>
						CUI.when(promises)
						.fail(dfr3.reject)
						.done =>
							if opts.dont_store
								CUI.util.removeFromArray(filename, manifest.payloads)
								dfr3.resolve()
								return

							@storeFile(filename, json)
							.fail(dfr3.reject)
							.done(dfr3.resolve)

			else
				@storeFile(filename, json)
				.fail(dfr3.reject)
				.done(dfr3.resolve)

			dfr3.promise()

		update_payload = (filename, json) =>
			payload(filename, json, true)

		master_dfr.done =>
			# save manifest
			ez5.splash.done("easydb4migration.progress", payload: "manifest.json")
			for payload in manifest.__update_payloads
				manifest.payloads.push(payload)

			delete(manifest.__update_payloads)
			@storeFile("manifest.json", manifest)

		master_dfr.fail =>
			ez5.splash.hide()

		sort_by_depth = (items, id_key, parent_id_key) =>
			# sets the level in each item in relation to
			# the depth from parent_id
			find_item_by_id = (id) =>
				for item in items
					if item[id_key] == id
						return item

			find_path = (item, path=[]) ->
				if not parent_id_key
					return path

				parent_id = item[parent_id_key]
				if not parent_id
					return path

				parent_item = find_item_by_id(parent_id)
				if not parent_item
					console.debug("Unable to find parent for item.") #  item, path, id_key, parent_id_key)
					return path

				path.push(parent_item)
				return find_path(parent_item, path)

			for item in items
				item._depth = find_path(item).length

			items.sort (a, b) ->
				if a._depth < b._depth
					-1
				else if a._depth > b._depth
					1
				else
					CUI.util.compareIndex(a[id_key], b[id_key])

			return

		export_pool = (pool) =>

			dfr = new CUI.Deferred()

			for col in pool.columns
				if col.source_name == "name"
					name_col = col

			if not name_col
				dfr.reject("Pool column 'name' not found. Table: "+pool.source_name)
				return dfr.promise()

			id_key = pool.key.source_name
			if pool.parent
				id_parent_key = pool.parent.source_name

			@query("SELECT * FROM "+@getEasydb4TableName(pool.source_name)+" ORDER BY \""+pool.key.source_name+"\"")
			.fail(dfr.reject)
			.done (result) =>
				# console.debug "pools:", result

				sort_by_depth(result.rows, id_key, id_parent_key)
				pools = []

				for row in result.rows
					_pool =
						_basetype: "pool"
						_depth: row._depth
						pool:
							_version: 1
							reference: pool.source_name+":"+row[id_key]
							name: {}

					_pool.pool.name[@__settings.target_lang] = row[name_col.source_name]

					if id_parent_key and row[id_parent_key]
						_pool.pool["lookup:_id_parent"] = reference: pool.source_name+":"+row[id_parent_key]
					else
						if pool != @__info.pool_tables[0]
							_pool.pool.name[@__settings.target_lang] += " ["+pool.source_name+"]"

						_pool.pool["lookup:_id_parent"] = reference: "system:root"

					pools.push(_pool)

				return dfr.resolve(pools)

			return dfr.promise()

		export_pools = =>
			if "pool" not in @__settings.export_basetypes
				return CUI.resolvedPromise()

			dfr = new CUI.Deferred()

			# start with pools
			if @__info.pool_tables.length == 0
				dfr.resolve()
				return dfr.promise()

			ez5.splash.show("easydb4migration.progress", payload: "pools")

			CUI.chunkWork.call @,
				items: @__info.pool_tables
				chunk_size: 1
				call: (items, idx) =>
					dfr2 = new CUI.Deferred()
					export_pool(items[0])
					.fail(dfr2.reject)
					.done (pools) =>
						payloads = []
						pools_per_depth = []
						for pool, idxP in pools
							pools_per_depth.push(pool)
							if idxP < pools.length - 1 and pools[idxP+1]._depth > pool._depth
								# next payload depth is bigger, output current depth
								payloads.push(payload("basetype_pool-"+idx+"-depth-"+pool._depth+".json", import_type: "pool", pools: pools_per_depth))
								pools_per_depth = []
						if pools_per_depth.length > 0
							payloads.push(payload("basetype_pool-"+idx+"-depth-"+pools_per_depth[0]._depth+".json", import_type: "pool", pools: pools_per_depth))
						CUI.when(payloads)
						.fail(dfr2.reject)
						.done(dfr2.resolve)
					return dfr2.promise()
			.fail(dfr.reject)
			.done(dfr.resolve)

			return dfr.promise()

		export_tags = =>
			if "tags" not in @__settings.export_basetypes
				return CUI.resolvedPromise()

			dfr = new CUI.Deferred()

			# if @__info.tag_tables.length == 0
			# 	dfr.resolve()
			# 	return dfr.promise()

			ez5.splash.show("easydb4migration.progress", payload: "tags")

			taggroups = []
			promises = []

			# each tag table becomes a group
			for tag_table in @__info.tag_tables

				for col in tag_table.columns
					if col.source_name == "name"
						name_col = col

				if not name_col
					dfr.reject("Tag column 'name' not found.")
					return dfr.promise()

				taggroup =
					taggroup:
						reference: "easydb4:"+tag_table.source_name
						shortname: tag_table.source_name
						type: "checkbox"
						displayname: {}
					_tags: []

				taggroup.taggroup.displayname[@__settings.target_lang] = tag_table.target_displayname

				taggroups.push(taggroup)

				do (taggroup, tag_table) =>

					promise = @query("SELECT * FROM "+@getEasydb4TableName(tag_table.source_name)+" ORDER BY \""+tag_table.key.source_name+"\"")
					promise.done (result) =>
						for row in result.rows
							tag =
								tag:
									reference: tag_table.source_name+":"+row[tag_table.key.source_name]
									displayname: {}
									displaytype: "search"
									is_default: false
									enabled: true
									type: "individual"

							tag.tag.displayname[@__settings.target_lang] = row[name_col.source_name]
							taggroup._tags.push(tag)
						return

					promises.push(promise)

			CUI.when(promises)
			.fail(dfr.reject)
			.done =>
				payload("basetype_tags.json", import_type: "tags", tags: taggroups)
				dfr.resolve()

			return dfr.promise()


		send_ldap_sso_users = =>
			users = []
			for user in ldap_sso_users
				if user.user.reference in collection_user_refs
					# console.debug "user has a collection:", user
					users.push(user)

			@loginfo("LDAP/SSO users with collections: "+users.length)

			payload("basetype_user_ldap_sso.json", import_type: "user", users: users)
			.done =>
				# we need to move this payload before collections + presentations
				# we move this after basetype_user
				idx = manifest.payloads.indexOf("basetype_user.json")
				if idx == -1
					@logwarn("basetype_user.json not found, unable to move basetype_user_ldap_sso.json.")
				else
					payload_file = manifest.payloads.pop()
					manifest.payloads.splice(idx+1, 0, payload_file)


		export_users = =>
			if "user" not in @__settings.export_basetypes
				return CUI.resolvedPromise()

			dfr = new CUI.Deferred()

			group_table = @__info.base_tables.group

			if not group_table
				dfr.reject("No group table found.")
				return dfr.promise()

			table = @__info.base_tables.user
			if not table
				dfr.reject("No user table found.")
				return dfr.promise()

			ez5.splash.show("easydb4migration.progress", payload: "users")

			user_by_email = {}
			user_by_login = {}
			easydb_logins = []

			@__info.user_by_ref = {}

			@query("SELECT * FROM "+@getEasydb4TableName(table.source_name)+" ORDER BY \""+table.key.source_name+"\"")
			.done (result) =>
				easydb_users = result.rows

				@query("SELECT * FROM "+@getEasydb4TableName("eadb_user_cache")+" ORDER BY id")
				.done (result2) =>

					cache_users = []
					for row2 in result2.rows
						if not CUI.util.isString(row2.user_record)
							continue

						rec = JSON.parse(row2.user_record)
						if rec.logintype not in ["LoginShib2", "LoginLDAP"]
							continue

						# CUI.util.removeFromArray(null, easydb_users, (user) ->
						# 	if user.login != rec.login
						# 		return false

						# 	warning = "User '"+user.login+"' already an easydb user. Migrating to LDAP/SSO."
						# 	@logwarn(warning)
						# 	return true
						# )

						rec.login = rec.user_id
						cache_users.push(rec)

					# console.debug "easydb users:", easydb_users
					# console.debug "cached users:", cache_users

					process_users(easydb_users, false)
					.fail(dfr.reject)
					.done =>
						easydb_logins = Object.keys(user_by_login)
						process_users(cache_users, true)
						.fail(dfr.reject)
						.done(dfr.resolve)

			process_users = (rows, eadb_user_cache) =>

				user_by_id = {}
				users = []

				for row in rows
					uid = row[table.key.source_name]

					user =
						_basetype: "user"
						_groups: []
						user:
							_version: 1
							reference: table.source_name+":"+uid
							remarks: ""

					user_by_id[uid] = user

					obj = @mapObject(row, table)
					@mergeTopLevel(user, obj)

					if user.user.login
						login = user.user.login?.toLocaleLowerCase()

						if user.user.login == "root"
							user.user.login = "root_easydb4"

						if user_by_login[login]

							if eadb_user_cache and login in easydb_logins
								warning = "User '"+user.user.login+"' already an easydb user. Skipping."
								@logwarn(warning)
								continue

							# change login
							new_login = user.user.login + user_by_login[login]
							warning = "User '"+user.user.login+"' changed login to: '"+new_login+"'"
							@logwarn(warning)

							user.user.login = new_login
							user_by_login[login] += 1
						else
							user_by_login[login] = 1

						# we prefer login as reference
						user.user.reference = table.source_name+":login:"+user.user.login

						@__info.user_by_ref[user.user.reference] = user
					else
						user.user.login = "easydb4:"+uid
						@logwarn("User '"+uid+"' has not login. The login was set to '"+user.user.login+"'.")

					if eadb_user_cache
						switch row.logintype
							when "LoginLDAP"
								user.user.type = "ldap"
							when "LoginShib2"
								user.user.type = "sso"
					else
						if row.use_ldap_auth
							info = "User '"+user.user.login+"' has use_ldap_auth set, creating as with type: ldap."
							@loginfo(info)
							user.user.type = "ldap"
						else
							user.user.type = "easydb"


					email = user.user._new_primary_email?.trim().toLocaleLowerCase()

					if email?.length > 0
						if not CUI.EmailInput.regexp.exec(email)
							warning = "User '"+user.user.login+"' uses a borked email. Email '"+email+"' for this user was ignored."
							@logwarn(warning)
							user.user.remarks += warning+"\n"
						else
							other_user = user_by_email[email]
							if other_user
								warning = "User '"+other_user.user.login+"' uses the same email. Email '"+email+"' for this user was ignored."
								@logwarn(warning)
								other_user.user.remarks += "User '"+user.user.login+"' uses same email.\n"
								user.user.remarks += warning+"\n"
							else
								user_by_email[email] = user

								user._emails = [
									email: email
									use_for_login: false
									use_for_email: true
									send_email: false
									send_email_include_password: false
									send_email_welcome_now: false
									is_primary: true
									needs_confirmation: false
								]

					# we set user._emails instead
					delete(user.user._new_primary_email)

					if user.user._password
						user._password_insecure_hash = user.user._password
						user._password_insecure_hash_method = "md5"
						# fylr needs a combination of hash method + hash value
						user._password_hash = "md5:" + user.user._password
						delete(user.user._password)

					# console.debug "mapped user object", user, obj
					users.push(user)

				dfr2 = new CUI.Deferred()

				if eadb_user_cache
					# for group_id in user.groups or []
					# 	user._groups.push
					# 		_basetype: "group"
					# 		group:
					# 			"lookup:_id":
					# 				reference: group_table.source_name+":"+group_id
					#
					# save for later
					ldap_sso_users = users
					dfr2.resolve()
				else
					@query("""SELECT * FROM """+@getEasydb4TableName("eadb_links")+"""
					       WHERE
						     to_table_id = """+group_table.source_id+""" AND
						     from_table_id = """+table.source_id+
							' AND to_id IN (SELECT id FROM '+@getEasydb4TableName(group_table.source_name)+')')
					.fail(dfr.reject)
					.done (result) =>

						for row in result.rows
							uid = row.from_id # user id
							user = user_by_id[uid]

							if not user
								@logwarn("User not found:", uid)
								continue

							user._groups.push
								_basetype: "group"
								group:
									"lookup:_id":
										reference: group_table.source_name+":"+row.to_id

						payload("basetype_user.json", import_type: "user", users: users)
						.done(dfr2.resolve)
						.fail(dfr2.reject)

				return dfr2.promise()

			return dfr.promise()


		export_groups = =>
			if "group" not in @__settings.export_basetypes
				return CUI.resolvedPromise()

			dfr = new CUI.Deferred()

			table = @__info.base_tables.group
			if not table
				dfr.reject("No group table found.")
				return dfr.promise()

			ez5.splash.show("easydb4migration.progress", payload: "groups")

			@query("SELECT * FROM "+@getEasydb4TableName(table.source_name)+" ORDER BY \""+table.key.source_name+"\"")
			.done (result) =>
				groups = []
				@__info.group_by_id = {}

				for row in result.rows
					name = row[@__info.attrs.GROUP_TABLE_DISPLAY_COLUMN_NAME]
					group_id = row[table.key.source_name]

					group =
						_basetype: "group"
						__name: name
						group:
							_version: 1
							reference: table.source_name+":"+group_id
							displayname: {}

					group.group.displayname[@__settings.target_lang] = name

					@__info.group_by_id[group_id] = group

					groups.push(group)

				payload("basetype_group.json", import_type: "group", groups: groups)
				.fail(dfr.reject)
				.done(dfr.resolve)
				return

			return dfr.promise()

		export_collections = =>
			if "collection" not in @__settings.export_basetypes
				return CUI.resolvedPromise()

			table = @__info.base_tables.workfolder
			if not table
				@logwarn("No workfolder table found.")

			table2 = @__info.base_tables.workfolder2
			if not table2
				@loginfo("No WORKFOLDER_SECOND_TABLE configured.")

			# console.debug "Object Ids Per Table", object_ids_per_table

			CUI.chunkWork.call @,
				items: [
					[table, @__info.base_tables.workfolder_objects]
					[table2, @__info.base_tables.workfolder2_objects]
				]
				chunk_size: 1
				call: (items, idx) =>
					if not items[0][0] # no table
						return

					export_collection(items[0][0], items[0][1], idx)


		export_collection = (table, objects_table, source_idx) =>
			ez5.splash.show("easydb4migration.progress", payload: "collections ["+table.source_name+"]")

			@loginfo("Export Collection: "+table.source_name)

			# Structure in easydb 4
			#
			# personal folders
			#   fk_father = null, group_right = null, easydb_ower = user_<login>
			#
			# public folder
			#   group_rights = -1
			#
			# group folder
			#   group_rights = <group id>

			user_table = @__info.base_tables.user
			login_column = @__info.attrs.USER_LOGIN_COLUMN

			dfr = new CUI.Deferred()

			@query("SELECT * FROM "+@getEasydb4TableName(table.source_name)+" ORDER BY \""+table.key.source_name+"\"")
			.fail(dfr.reject)
			.done (result) =>

				collections = []
				collection_by_id = {}
				collection_by_uid = {}

				collections_by_group_rights = {}

				for col in objects_table.columns
					if not col.source_link
						continue

					if col.source_link.source_name != table.source_name # @__info.attrs.EASYDB_DEFAULT_TABLE
						bild_col = col
						bild_mapping = @getTargetColumn(col.source_link.source_name, col.source_link.key.source_name)
						break

				if not bild_col or not bild_mapping
					console.error("Cannot find link field for collection:", bild_col, objects_table)
					dfr.reject("Cannot find link field in export_collections, easydb4_reference missing?")
					return

				if bild_col.source_link.source_name != @__info.attrs.EASYDB_DEFAULT_TABLE
					name_appendix = " ["+bild_col.source_link.source_name+"]"
				else
					name_appendix = ""

				# get_collection = (group_rights) =>

				# 	collection = collections_by_group_rights[group_rights]
				# 	if collection
				# 		return collection

				# 	if group_rights == "-1" # public
				# 		reference = "easydb4migration:public:"+idx
				# 		displayname =
				# 			"de-DE": "easydb4 Öffentliche Mappen"+name_appendix
				# 			"en-US": "easydb4 Public Folders"+name_appendix
				# 	else
				# 		gname = @__info.group_by_id[group_rights]?.__name or group_rights+""
				# 		reference = "easydb4migration:group:"+idx+":"+group_rights
				# 		displayname =
				# 			"de-DE": "easydb4 Gruppen Mappe "+gname+name_appendix
				# 			"en-US": "easydb4 Public Folder "+gname+name_appendix

				# 	collection = collections_by_group_rights[group_rights] =
				# 		_basetype: "collection"
				# 		__reference: reference
				# 		__parent_reference: null
				# 		collection:
				# 			_version: 1
				# 			"lookup:_id_parent":
				# 				reference: ez5.session.user.getCollectionReference()
				# 			reference: reference
				# 			type: "workfolder"
				# 			children_allowed: true
				# 			objects_allowed: true
				# 			displayname: displayname

				# 	# console.error "creating collection:", collection

				# 	collections.push(collection)
				# 	return collection

				# console.debug "received rows", result.rows.length

				for row, idx in result.rows
					name = row[@__settings.workfolder_name].trim()+name_appendix

					collection_id = row[table.key.source_name]
					# console.debug "running row", idx, name, collection_id

					collection =
						_basetype: "collection"
						_objects: []
						__bild_ids: []
						__reference: table.source_name+":"+collection_id
						__parent_reference: null
						collection:
							_version: 1
							reference: table.source_name+":"+collection_id
							type: "workfolder"
							children_allowed: true
							objects_allowed: true
							displayname: {}


					collection_by_id[collection_id] = collection

					if row.group_rights
						if row.group_rights == "-1"
							gname = "public"
						else
							gname = @__info.group_by_id[row.group_rights]?.__name or row.group_rights+""

						name = name + " ["+gname+"]"

					collection.collection.displayname[@__settings.target_lang] = name

					parent_id = row[@__settings.workfolder_parent]
					if parent_id
						colref = table.source_name+":"+parent_id
						collection.__parent_reference = colref
					else
						owner = row.easydb_owner
						if owner?.startsWith("user_")
							ref = user_table.source_name+":"+login_column+":"+owner.substr(5)
							if not @__info.user_by_ref[ref]
								colref = ez5.session.user.getCollectionReference()
								@loginfo("User of private collection '"+name+"' not found: "+ref+" setting user to: "+colref)
							else
								colref = "user:ref:"+ref
								collection_user_refs.push(ref)

						else
							@logwarn("No easydb_owner found for collection, using migration user: "+name)
							colref = ez5.session.user.getCollectionReference()

					collection.collection["lookup:_id_parent"] =
						reference: colref

					uid = colref+name

					if collection_by_uid[uid]
						# change name
						collection.collection.displayname[@__settings.target_lang] = name+" ("+collection_by_uid[uid]+")"
						collection_by_uid[uid] += 1
					else
						collection_by_uid[uid] = 1

					collections.push(collection)

				sort_by_depth(collections, "__reference", "__parent_reference")

				# console.error("objects table:", objects_table)

				sel = 'SELECT * FROM '+@getEasydb4TableName(objects_table.source_name)+'
				    WHERE
					   "' + bild_col.source_name + '" IS NOT NULL AND
					   "' + objects_table.uplink.source_name + '" IS NOT NULL
				    ORDER BY "'+objects_table.key.source_name+'"'

				@query(sel)
				.fail(dfr.reject)
				.done (result) =>

					missing_count = bild_ids: [], collection: []

					for row in result.rows
						collection_id = row[objects_table.uplink.source_name]
						bild_id = row[bild_col.source_name]
						collection = collection_by_id[collection_id]

						if not collection
							CUI.util.pushOntoArray(collection_id, missing_count.collection)
							continue

						cn = collection.collection.displayname[@__settings.target_lang]

						if bild_id in collection.__bild_ids
							@logwarn("Collection '"+cn+"': Id found multiple times in collection, skipping: "+bild_id)
							continue

						collection.__bild_ids.push(bild_id)

						if bild_id < Math.pow(10,10)

							if @__settings.limit_collections and bild_id not in object_ids_per_table[ bild_col.source_link.real_source_name ]
								# easydb_default_object_keys
								missing_count.bild_ids.push(bild_id)
								# console.debug("Collection export: skipping not exported ", @__info.attrs.EASYDB_DEFAULT_TABLE, "ID:", bild_id)
								continue

							lobj =
								_objecttype: bild_mapping.table.name

							lobj[bild_mapping.column.name] = bild_col.source_link.real_source_name+":"+bild_id

							co = "lookup:_global_object_id": lobj

						else
							co = _global_object_id: bild_id+"@easydb4"

						collection._objects.push(co)

					if missing_count.bild_ids.length > 0
						@logwarn("Collection Objects not found: '"+bild_col.source_link.source_name+"': "+missing_count.bild_ids.length)

					if missing_count.collection.length > 0
						@logwarn("Collections not found: "+missing_count.collection.length)

					if @__settings.skip_collection_with_more_objects_than > 0
						@loginfo("Checking collection sizes. Will drop collection with more than "+@__settings.skip_collection_with_more_objects_than+" objects.")
						CUI.util.removeFromArray(null, collections, (col) =>
							if col._objects.length > @__settings.skip_collection_with_more_objects_than
								@loginfo("Skip collection "+col.collection.displayname[@__settings.target_lang]+", it has "+col._objects.length+" objects.")
						)

					json =
						import_type: "collection"
						collections: collections

					payload_split("basetype_collection-no"+(source_idx+1)+".json", 500, json)
					.fail(dfr.reject)
					.done(dfr.resolve)

				return

			return dfr.promise()


		export_presentations = =>
			if "presentation" not in @__settings.export_basetypes
				return CUI.resolvedPromise()

			dfr = new CUI.Deferred()

			table = @__info.base_tables.presentation
			if not table
				@logwarn("No presentation table found.")
				dfr.resolve()
				return dfr.promise()

			bilder_table = @__info.base_tables.easydb_default
			if not bilder_table
				dfr.reject("export presentations: No EASYDB_DEFAULT_TABLE found.")
				return dfr.promise()

			ez5.splash.show("easydb4migration.progress", payload: "presentations")

			user_table = @__info.base_tables.user
			login_column = @__info.attrs.USER_LOGIN_COLUMN

			@query("SELECT * FROM "+@getEasydb4TableName(table.source_name)+" ORDER BY \""+table.key.source_name+"\"")
			.fail(dfr.reject)
			.done (result) =>

				presentations = []

				presentation_by_id = {}
				presentation_by_uid = {}

				for row in result.rows
					# console.debug "presentations found:", row

					if CUI.util.isEmpty(row.name)
						name = "Ohne Namen"
					else
						name = row.name.trim()

					name += " [P]"

					presentation_id = row[table.key.source_name]

					presentation =
						_basetype: "collection"
						_objects: []
						__reference: table.source_name+":"+presentation_id
						__bild_ids: []
						collection:
							_version: 1
							reference: table.source_name+":"+presentation_id
							type: "presentation"
							children_allowed: true
							objects_allowed: true
							displayname: {}
							webfrontend_props:
								presentation:
									slides: [
										type: "start"
										data:
											title: name
											info: ""
									]
									settings:
										show_info: "no-info"

					presentation.collection.displayname[@__settings.target_lang] = name

					owner = row.easydb_owner
					if owner?.startsWith("user_")
						ref = user_table.source_name+":"+login_column+":"+owner.substr(5)
						if not @__info.user_by_ref[ref]
							colref = ez5.session.user.getCollectionReference()
							@logwarn("User of private presentation '"+name+"' not found: "+ref+" setting user to: "+colref)
						else
							colref = "user:ref:"+ref
							collection_user_refs.push(ref)
					else
						@logwarn("No easydb_owner found for collection, using migration user: "+name)
						colref = ez5.session.user.getCollectionReference()

					presentation.collection["lookup:_id_parent"] =
						reference: colref

					uid = colref+name

					if presentation_by_uid[uid]
						# change name
						presentation.collection.displayname[@__settings.target_lang] = name+" ("+presentation_by_uid[uid]+")"
						presentation_by_uid[uid] += 1
					else
						presentation_by_uid[uid] = 1

					presentations.push(presentation)
					presentation_by_id[presentation_id] = presentation

				missing_count = bild: 0, presentation: []


				objects_table = @__info.base_tables.presentation_objects

				if objects_table?.count > 0

					if objects_table.count == 0
						@logwarn("Skipping migration of table:", objects_table.source_name, ". No records found.")
						dfr.resolve()
						return

					for col in objects_table.columns
						if col.source_link?.source_name == @__info.attrs.EASYDB_DEFAULT_TABLE
							bild_col = col
							bild_mapping = @getTargetColumn(col.source_link.source_name, col.source_link.key.source_name)
							break

					if not bild_col or not bild_mapping
						console.error("Cannot find link field for EASYDB_DEFAULT_TABLE:", bild_col, objects_table)
						dfr.reject("Cannot find link field for EASYDB_DEFAULT_TABLE in export_presentations.")
						return

					sel = 'SELECT "' + objects_table.uplink.source_name + '" AS from_id, "' +
						bild_col.source_name + '" AS to_id, position FROM '+@getEasydb4TableName(objects_table.source_name)+'
					    WHERE
						   "' + bild_col.source_name + '" IS NOT NULL AND
						   "' + objects_table.uplink.source_name + '"  IS NOT NULL
					    ORDER BY "'+objects_table.key.source_name+'"'
				else
					bild_mapping = @getTargetColumn(bilder_table.source_name, bilder_table.key.source_name)

					sel = """SELECT * FROM """+@getEasydb4TableName("eadb_links")+""" WHERE from_table_id = """+
						table.source_id + " AND to_table_id = "+
						bilder_table.source_id+" ORDER BY from_id, id"

				# console.error("sel: ", sel)

				@query(sel)
				.fail(dfr.reject)
				.done (result) =>

					for row in result.rows
						presentation_id = row.from_id
						bild_id = row.to_id
						presentation = presentation_by_id[presentation_id]
						if not presentation
							CUI.util.pushOntoArray(presentation_id, missing_count.presentation)
							continue

						slides = presentation.collection.webfrontend_props.presentation.slides

						if bild_id < Math.pow(10,10)

							if @__settings.limit_collections and bild_id not in object_ids_per_table[ bilder_table.real_source_name ]
								missing_count.bild = missing_count.bild + 1
								continue

							lobj =
								_objecttype: bild_mapping.table.name

							lobj[bild_mapping.column.name] = bilder_table.real_source_name+":"+bild_id

							# client based lookup
							# gid = bilder_table.real_source_name+":"+bild_id+"@lookup:"+bild_mapping.table.name+"."+bild_mapping.column.name
							co = "lookup:_global_object_id": lobj

							# webfrontend_props use a different format
							co_p = "lookup:global_object_id": lobj
						else
							co = _global_object_id: bild_id+"@easydb4"
							co_p = global_object_id: co._global_object_id

						slide_idx = Math.floor(row.position / 2) + 1 # add one, we have a start slide

						if row.position % 2 == 0
							side = "left"
						else
							side = "right"

						slide = slides[slide_idx]

						# console.debug "row position:", presentation_id, bild_id, row.position, slide_idx, side, slide

						if not slide
							slide = slides[slide_idx] =
								type: "one"
								_side: side
								center: co_p # global_object_id: gid
						else
							if slide.center
								slide[slide._side] = slide.center
								delete(slide.center)
								slide.type = "two"

							# next image
							if slide[side]
								console.error("Side ", side, "already occupied.", slide)
							else
								slide[side] = co_p # global_object_id: gid

						if bild_id in presentation.__bild_ids
							# no dups in collection
							continue

						presentation.__bild_ids.push(bild_id)

						presentation._objects.push(co)

					if missing_count.bild > 0
						@logwarn("Presentation Objects not found:", missing_count.bild)

					if missing_count.presentation.length > 0
						@logwarn("Presentations not found:", missing_count.presentation.length)

					# remove empty slides
					for presentation in presentations
						filled_slides = []
						for slide in presentation.collection.webfrontend_props.presentation.slides
							if not slide
								continue
							delete(slide._side)
							filled_slides.push(slide)
						presentation.collection.webfrontend_props.presentation.slides = filled_slides

					payload_split("basetype_presentation.json", 500, import_type: "collection", collections: presentations)
					.fail(dfr.reject)
					.done(dfr.resolve)

					# console.debug "presentations:", presentations

			dfr.promise()

		table_idx = -1

		export_next_object_table = (dfr = new CUI.Deferred()) =>
			table_idx++

			if table_idx == @__info.tables.length
				dfr.resolve()
				return dfr.promise()

			table = @__info.tables[table_idx]
			if not table.internal and not table.target_editlink
				export_object_table(table)
				.fail(dfr.reject)
				.done =>
					export_next_object_table(dfr)
			else
				export_next_object_table(dfr)

			return dfr.promise()

		object_ids_per_table = {}

		export_object_table = (table) =>

			splash_text = table.source_name
			ez5.splash.show("easydb4migration.progress", payload: splash_text)

			tb_mapping = @__mapping.source_table[table.source_name]

			if not tb_mapping?.target_table_name
				@logwarn("Skipping migration of table:", table.source_name, ". No mapping defined.")
				return CUI.resolvedPromise()

			if table.count == 0
				@logwarn("Skipping migration of table:", table.source_name, ". No records found.")
				return CUI.resolvedPromise()

			fake_arr = []

			# every table with "pool" is considered a main table
			main_tables = [ @__info.attrs.EASYDB_DEFAULT_TABLE ]
			for t in @__info.tables
				if t.pool
					main_tables.push(t.source_name)

			if table.source_name in main_tables
				limited = true
				if @__settings.limit_main_tables > 0
					chunk_size = @__settings.limit_main_tables
					fake_arr[chunk_size-1] = true
				else
					chunk_size = 1000
					fake_arr[table.count-1] = true
			# else if table.count == 0
			# 	chunk_size = 1
			# 	fake_arr[0] = true
			else
				# limited = false
				# chunk_size = table.count
				limited = true
				chunk_size = 10000
				fake_arr[table.count-1] = true

			# console.debug "select:", limited, chunk_size, table.source_name

			object_ids_per_table[ table.real_source_name ] = []

			CUI.chunkWork.call @,
				items: fake_arr
				chunk_size: chunk_size
				call: (fake_items, offset, len) =>
					limit = fake_items.length

					if limited
						range = (offset+1)+"-"+Math.min(limit+offset, len)
					else
						range = ""

					dfr = new CUI.Deferred()
					get_objects_for_table(table, offset, limit)
					.fail(dfr.reject)
					.done (objects) =>
						# send objects to store

						payloads = []

						init_json = =>
							import_type: "db"
							objecttype: tb_mapping.target_table_name
							objects: []

						json = init_json()

						for obj, idx in objects

							object_ids_per_table[ table.real_source_name ].push(obj.__key)

							obj2 = {}
							obj2._objecttype = tb_mapping.target_table_name
							obj2._mask = "_all_fields"
							obj._version = 1

							@mergeTopLevel(obj2, obj)
							json.objects.push(obj2)

							if idx < objects.length - 1 and obj2._depth != objects[idx+1]["top:_depth"]
								# console.debug idx, objects.length, obj2._depth, objects[idx+1]["top:_depth"], json.objects.length
								# depth change in the next round, output payload
								payloads.push(payload(table.source_name+"-level-"+obj2._depth+(if range then "-"+range else "")+".json", json))
								json = init_json()


						if json.objects[0]?._depth != undefined
							depth = "-level-"+json.objects[0]._depth
						else
							depth = ""

						# console.debug "last payload:", depth

						deferred_sources = (tb.source_name for tb in (@__info.deferred_tables or []))

						# add self referencing table entries
						deferred_sources.push(table.target_name)

						# console.debug "table", table, deferred_sources

						# check for updates
						find_deferred = (obj, found=[]) =>
							for k, v of obj
								if k.endsWith(":source_name") and v in deferred_sources
									# defer this value
									col_name = k.substr(0, k.length-":source_name".length)
									found.push(col_name)
								else if CUI.isArray(v)
									for _obj in v
										find_deferred(_obj, found)
								else if CUI.isPlainObject(v)
									find_deferred(v, found)

							return found



						ref_target = @getTargetColumn(table.source_name, table.key.source_name)

						# console.debug "deferred sources:", deferred_sources, json, ref_target

						update_json = init_json()

						for obj in json.objects
							found = find_deferred(obj)
							if found.length == 0
								continue

							# push the complete object to the update queue
							upd_obj = CUI.util.copyObject(obj, true)
							_upd_obj = upd_obj[upd_obj._objecttype]

							lobj = {}
							lobj[ref_target.column.name] = _upd_obj[ref_target.column.name]

							_upd_obj._version = _upd_obj._version + 1
							_upd_obj["lookup:_id"] = lobj

							update_json.objects.push(upd_obj)

							# remove all deferred objects and nested from the object
							# also, remove all eas objects
							for k, v of obj[obj._objecttype]
								if k in found
									delete(obj[obj._objecttype][k])
									delete(obj[obj._objecttype][k+":source_name"])

								if v?[0]?["eas:url"]
									# remove all eas objects, so we dont upload them twice
									delete(obj[obj._objecttype][k])

								if k.startsWith("_nested:")
									delete(obj[obj._objecttype][k])

							# console.debug("found deferred:", table.source_name, found, upd_obj)

						filename = table.source_name+depth+(if range then "-"+range else "")+".json"
						payloads.push(payload(filename, json))

						if update_json.objects.length > 0
							payloads.push(update_payload("update-"+filename, update_json))

						CUI.when(payloads)
						.done =>
							@loginfo("Exported: "+table.source_name+": "+((range and range+"/"+len) or objects.length))
							dfr.resolve()
						.fail(dfr.reject)
					return dfr.promise()


		prepare_tables = =>
			dfr = new CUI.Deferred()

			# Find manually linked nested tables
			for nested_table in @__info.tables
				if nested_table.internal
					continue

				tb_mapping = @__mapping.source_table[nested_table.source_name]
				# check if the target table is a nested

				table_schema = @getTableByName(tb_mapping?.target_table_name)
				if not table_schema?.owned_by
					continue

				nested_table.target_editlink = true

				# find the parent table
				for table in @__info.tables
					parent_mapping = @__mapping.source_table[table.source_name]

					if table_schema.owned_by.other_table_name_hint == parent_mapping?.target_table_name
						parent_table = table
						break

				if not parent_table
					dfr.reject("Target editlink: Parent table not found for nested table.", nested_table)
					return dfr.promise()

				if not parent_table._target_nested_tables
					parent_table._target_nested_tables = []

				# console.error "MAGIC NESTED FOUND!", nested_table, parent_table, tb_mapping.source_column
				for col in nested_table.columns
					if tb_mapping.source_column[col.source_name]?.target_column_name != '__UPLINK__'
						continue

					if nested_table.uplink
						dfr.reject("Target editlink: "+table_schema.name+": More than one uplink column found.", nested_table)
						return dfr.promise()

					nested_table.uplink = col

				if not nested_table.uplink
					dfr.reject("Target editlink: "+table_schema.name+": No uplink column found.", table)
					return dfr.promise()

				parent_table._target_nested_tables.push(nested_table)

			# Find text to linked objects conversions
			promises = []
			objects_by_table = {}

			for table in @__info.tables
				for col in table.columns
					if col.source_type not in ["blob", "text", "integer"]
						continue

					target = @getTargetColumn(table.source_name, col.source_name)
					if target?.column.type != "link"
						continue

					for _col in target.other_table.columns
						if col.source_name.startsWith("easydb_")
							# skip internal fields
							continue

						if _col.type == "text" or
							_col.type == "text_oneline" or
							_col.type == "text_l10n_oneline" or
							_col.type == "text_l10n"
								target_col = _col
								break

					if not target_col
						@logwarn("text/int > linked object [text] conversion, no target column found.", target)
						continue

					@loginfo("text/int > linked object conversion:"+table.source_name+"."+col.source_name+"["+col.source_type+"] > "+table.real_source_name+"."+target_col.name+"["+target_col.type+"]")

					target.other_column = target_col

					@__mapping.source_table[table.source_name].source_column[col.source_name].target = target

					if not objects_by_table[target.other_table.name]
						objects_by_table[target.other_table.name] =
							target_column: target_col
							values: []

					do (target) =>
						promise = @query('SELECT DISTINCT TRIM("'+col.real_source_name+'") FROM '+@getEasydb4TableName(table.real_source_name))
						.done (result) =>
							if result.rows.length == 0
								return

							for row in result.rows
								CUI.util.pushOntoArray(row[0], objects_by_table[target.other_table.name].values)

						promises.push(promise)

			dfr = new CUI.Deferred()

			CUI.when(promises)
			.fail(dfr.reject)
			.done =>

				more_promises = []
				for target_table_name, info of objects_by_table
					if info.values.length == 0
						continue

					objects = []
					for value in info.values
						if CUI.util.isEmpty(value)
							continue

						obj = _version: 1

						obj[info.target_column.name] = value

						obj2 =
							_mask: "_all_fields"
							_objecttype: target_table_name

						obj2[obj2._objecttype] = obj
						objects.push(obj2)

					promise = payload(target_table_name+"-text-to-linked-objects.json",
						import_type: "db"
						objecttype: target_table_name
						objects: objects
					)
					more_promises.push(promise)

				CUI.when(more_promises)
				.fail(dfr.reject)
				.done(dfr.resolve)

			return dfr.promise()

		get_objects_for_table = (table, offset=0, limit=0, where="") =>

			splash_text = table.source_name+" "+(offset+1)+"-"+Math.min(table.count, offset+limit)+"/"+table.count
			ez5.splash.show("easydb4migration.progress", payload: splash_text)

			tb_mapping = @__mapping.source_table[table.source_name]

			dfr = new CUI.Deferred()

			if limit > 0
				off_limit = "LIMIT "+limit
			else
				off_limit = "LIMIT -1"

			off_limit += " OFFSET "+offset

			# for hierarchical tables we need to use a recursive select
			# which is sorted by depth
			if table.parent
				fk_father_id = '"' + table.parent.real_source_name + '"'

				tn = @getEasydb4TableName(table.real_source_name)

				sql = """
	WITH RECURSIVE ids AS (
	    SELECT id, 0 as level
	    FROM """ + tn + """ WHERE
	    """ + fk_father_id + """ IS NULL

	    UNION ALL

	    SELECT a.id, ids.level + 1
	    FROM """ + tn + """ a
	    JOIN ids ON (a.""" + fk_father_id + """ = ids.id)
	)
	SELECT level as _depth, b.* -- level, s.id, s.fk_father_id
	FROM ids
	JOIN """ + tn + """ b USING (id)
	ORDER BY ids.level, id """ + off_limit
			else

				if where.length > 0
					sqlWhere = table.where or ""
					if sqlWhere.length > 0
						sqlWhere += " AND ("+where+")"
					else
						sqlWhere = "WHERE "+where
				else
					sqlWhere = table.where or ""

				sql = "SELECT * FROM "+@getEasydb4TableName(table.real_source_name)+" "+sqlWhere+" ORDER BY \""+table.key.source_name+"\" "+off_limit

			# console.debug "sql:", table, sql

			@query(sql)
			.fail(dfr.reject)
			.done (result) =>
				changelog_by_id = {}
				# pull in changelog?
				map_changelog = @getTargetColumn(table.source_name, "__changelog__")

				# console.debug("Select: ", sql, "Found:", result.rows.length)

				export_table = =>
					objects = []
					obj_by_id = {}

					# if table.parent
					# 	sort_by_depth(result.rows, table.key.source_name, table.parent.source_name)

					for row in result.rows
						if map_changelog
							row.__changelog__ = JSON.stringify(entries: changelog_by_id[row[table.key.source_name]] or [])
							# console.debug "row:", row.__changelog__

						obj = @mapObject(row, table, obj_by_id)

						objects.push(obj)

					# console.debug "exported table", changelog_by_id, obj_by_id

					calls = []
					nested_tables = table._nested_tables.slice(0)
					if table._target_nested_tables
						for nt in table._target_nested_tables
							nested_tables.push(nt)

					for nested_table in nested_tables
						if nested_table == table
							dfr.reject("Nested table same as parent table.")
							return

						if nested_table.eadb_links
							if nested_table.merge_with_table
								# this will be handled in the merge_with_table
								continue

							do (nested_table) =>
								to_id_target = @getTargetColumn(nested_table.source_name, "to_id")
								remark_target = @getTargetColumn(nested_table.source_name, "remark")

								if not to_id_target
									@logwarn("Skipping: "+nested_table.source_name+". No mapping defined.")
									return

								nk = "_nested:"+to_id_target.table.name

								# console.debug nested_table.source_name, to_id_target, remark_target, nk, nested_table.nested_target_key_name

								calls.push =>
									dfr2 = new CUI.Deferred()

									if splash_text
										ez5.splash.show("easydb4migration.progress", payload: splash_text+" -- "+nested_table.target_table.real_source_name)
									get_target_ids = (tb) =>
										if object_ids_per_table[ tb.real_source_name ]
											return CUI.resolvedPromise(object_ids_per_table[ tb.real_source_name ])

										sel = 'SELECT "' + tb.key.real_source_name +
											'" FROM '+@getEasydb4TableName(tb.real_source_name)

										@query(sel)
										.done (result) =>
											object_ids_per_table[ tb.real_source_name ] = []
											for row in result.rows
												object_ids_per_table[ tb.real_source_name ].push(row[0])

											console.debug("Getting ids: "+tb.real_source_name+": "+result.rows.length)

									get_target_ids(nested_table.target_table)
									.fail(dfr2.reject)
									.done =>
										target_ids = object_ids_per_table[nested_table.target_table.real_source_name]

										sel2 = 'SELECT * FROM '+@getEasydb4TableName("eadb_links")+
											' WHERE from_id IS NOT NULL ' +
											' AND to_id IS NOT NULL ' +
											' AND from_table_id = '+ nested_table.owned_by.source_id +
											' AND to_table_id = ' + nested_table.target_table.source_id

										# console.debug sel2
										@query(sel2)
										.fail(dfr2.reject)
										.done (result) =>
											for row in result.rows
												obj = obj_by_id[row.from_id]

												if not obj
													continue


												target = @getTargetColumn(nested_table.target_table.source_name, nested_table.target_table.key.source_name)
												# console.debug("Target:", nested_table.target_table, target)

												if not target
													@logwarn("Target not found for: "+nested_table.target_table.source_name+" using standard  mapping.")
													target =
														column: name: "__not_mapped"
														table: name: nested_table.target_table.target_name


												if row.to_id not in target_ids and
													nested_table.owned_by.source_id != nested_table.target_table.source_id
														@loginfo("eadb_links: "+nested_table.owned_by.source_name+"["+row.from_id+"] -> "+nested_table.target_table.source_name+"["+row.to_id+"] is missing, not exporting.")
														continue

												obj2 = {}

												lobj = {}
												lobj["lookup:_id"] = {}

												# standard name, use standard value
												lobj["lookup:_id"][target.column.name] = @getEasydb4ReferencePrefix(target.column.name, nested_table.target_table.source_name)+row.to_id

												lobj2 = {}
												lobj2 =
													_objecttype: target.table.name
													_mask: "_all_fields"

												lobj2[lobj2._objecttype] = lobj

												if remark_target?.column
													obj2[remark_target?.column.name] = row.remark

												if to_id_target?.column
													obj2[to_id_target?.column.name] = lobj2
													obj2[to_id_target?.column.name+":source_name"] = lobj2._objecttype

												if not obj[nk]
													obj[nk] = []

												@pushOntoArray(obj2, obj[nk])

											dfr2.resolve()
											return

									return dfr2.promise()
							continue


						do (nested_table) =>

							target_mapping = @__mapping.source_table[nested_table.source_name]
							target_table_name = target_mapping?.target_table_name

							if not target_table_name
								@logwarn("Skipping nested table: '"+nested_table.source_name+"'. No mapping defined.")
								return

							calls.push =>

								if splash_text
									ez5.splash.show("easydb4migration.progress", payload: splash_text+" -- "+nested_table.real_source_name)

								eadb_link_infos = []
								promises = []
								for _nested_table in nested_tables
									if not _nested_table.merge_with_table
										continue

									_target_mapping = @__mapping.source_table[_nested_table.source_name]
									_target_table_name = _target_mapping?.target_table_name

									if target_table_name != _target_table_name
										continue

									if not _target_table_name
										@logwarn("Skipped merging nested table: '"+_nested_table.source_name+"' . No mapping defined.")
										continue

									sel = 'SELECT id, from_id, to_id, remark FROM '+@getEasydb4TableName("eadb_links")+' WHERE '+
										'from_table_id=' + _nested_table.eadb_links_from_table_id + ' AND ' +
										'to_table_id=' + _nested_table.eadb_links_to_table_id +
										' ORDER BY position '

									# console.debug target_table_name, _target_table_name, sel

									promise = @query(sel)
									.done (result) =>
										for row in result.rows
											eadb_link_infos.push(row)
										return

									promises.push(promise)

								dfr3 = new CUI.Deferred()
								CUI.when(promises)
								.fail(dfr3.reject)
								.done =>
									if Object.keys(obj_by_id).length > 0
										where = '"'+nested_table.uplink.source_name+'" IN ('+Object.keys(obj_by_id).join(',')+')'
									# console.debug nested_table.uplink.source_name, "in", Object.keys(obj_by_id).length
									return get_objects_for_table(nested_table, undefined, undefined, where)
									.done (nested_objects) =>
										# if eadb_link_infos.length
										# 	console.debug "eadb_links:", eadb_link_infos, nested_objects
										nk = "_nested:"+target_table_name
										# console.debug "nested_table", nested_table, nested_objects, nk, obj_by_id
										# console.debug "nested:", nk, nested_objects, nested_table, obj_by_id
										# unmatched=0
										for nested_object in nested_objects
											obj_id = nested_object[nested_table.uplink.source_name]
											obj = obj_by_id[obj_id]
											if not obj
												# unmatched++
												# if table.source_name != @__info.attrs.EASYDB_DEFAULT_TABLE
												# 	console.warn("Object for nested table not found:", obj_id, table, nested_table)
												# else
												# 	; # probably skipped by limit
												continue
											obj2 = CUI.util.copyObject(nested_object, true)
											delete(obj2[nested_table.uplink.source_name])
											if not obj[nk]
												obj[nk] = []
											@pushOntoArray(obj2, obj[nk])
										# console.debug "found", nested_objects.length, "unmatched", unmatched

										for nested_object in nested_objects
											for eadb_link_info in eadb_link_infos
												if eadb_link_info.to_id != nested_object.__key
													continue
												obj = obj_by_id[eadb_link_info.from_id]
												if not obj
													continue
												obj2 = CUI.util.copyObject(nested_object, true)
												delete(obj2[nested_table.uplink.source_name])
												if not obj[nk]
													obj[nk] = []
												obj2.__eadb_links_id = eadb_link_info.id
												obj2.__eadb_links_remark = eadb_link_info.remark
												if CUI.util.idxInArray(null, obj[nk], (_obj2) ->
													_obj2.__key == obj2.__key
												) == -1
													@pushOntoArray(obj2, obj[nk])
												# console.debug "obj2:", obj2
										return
									.fail(dfr3.reject)
									.done(dfr3.resolve)
								return dfr3.promise()

					CUI.chainedCall.apply(null, calls)
					.fail(dfr.reject)
					.done =>
						@postProcessGNDForTable(objects)
						.fail(dfr.reject)
						.done =>
							@postProcessGazetteerForTable(objects)
							.fail(dfr.reject)
							.done =>
								@postProcessGettyForTable(objects)
								.fail(dfr.reject)
								.done =>
									dfr.resolve(objects)
				# export_table END

				if not map_changelog
					export_table()
				else
					# console.error "pulling changelog...?", table, map_changelog, @

					@query("SELECT * FROM "+@getEasydb4TableName("eadb_changelog")+" WHERE table_id = "+table.source_id+" ORDER BY id")
					.fail(dfr.reject)
					.done (changelog_result) =>
						for change_row, idx in changelog_result.rows
							log_entry = {}
							for k in ["id", "zeitpunkt", "log_entry", "user_login", "user_name", "client_ip"]
								log_entry[k] = change_row[k]

							if not changelog_by_id[change_row.key_id]
								changelog_by_id[change_row.key_id] = []
							changelog_by_id[change_row.key_id].push(log_entry)

						export_table()
				return

			return dfr.promise()

		@__eas_urls_by_id = {}

		get_eas_ids = =>
			if not @__settings.eas_version
				return CUI.resolvedPromise()

			ez5.splash.show("easydb4migration.progress", payload: "getting eas ids")

			@query("""SELECT eas_id, file_version, url, original_filename FROM file a LEFT JOIN filestore b
			    ON (b.filestore_id = a.filestore_id)""")
			.done (result) =>
				for row in result.rows
					if not @__eas_urls_by_id[row.eas_id]
						@__eas_urls_by_id[row.eas_id] = original_filename: row.original_filename

					if @__settings.eas_version != "original" and row.file_version == "original"
						@__eas_urls_by_id[row.eas_id]._original = row.url

					if row.file_version == @__settings.eas_version
						@__eas_urls_by_id[row.eas_id].original = row.url

					if row.file_version == @__info.min_eas_version
						# pick smallest version for preview
						@__eas_urls_by_id[row.eas_id].preview_url = row.url

		if @__settings.export_tool
			for tool in @__tools
				if tool.name() == @__settings.export_tool
					tool.run(payload)
					.fail(master_dfr.reject)
					.done(master_dfr.resolve)
				break
		else
			go_on = =>
				get_eas_ids()
				.fail(master_dfr.reject)
				.done =>
					export_groups()
					.fail(master_dfr.reject)
					.done =>
						export_users()
						.fail(master_dfr.reject)
						.done =>
							export_pools()
							.fail(master_dfr.reject)
							.done =>
								export_tags()
								# .fail(master_dfr.reject)
								# .done(master_dfr.resolve)
								.fail(master_dfr.reject)
								.done =>
									prepare_tables()
									.fail(master_dfr.reject)
									.done =>
										export_next_object_table()
										.fail(master_dfr.reject)
										.done =>
											export_collections()
											.fail(master_dfr.reject)
											.done =>
												export_presentations()
												.fail(master_dfr.reject)
												.done =>
													send_ldap_sso_users()
													.fail(master_dfr.reject)
													.done(master_dfr.resolve)
				return

			if @__settings.post_process_plugin
				plugin = @__plugin_by_name[@__settings.post_process_plugin]
				CUI.decide(plugin.init())
				.done(go_on)
				.fail(master_dfr.reject)
			else
				go_on()


		return master_dfr2.promise()

	# push no duplicates
	pushOntoArray: (value, arr) ->
		value_s = JSON.stringify(value)
		for item in arr
			if JSON.stringify(item) == value_s
				@logwarn("Skipping duplicate nested entry: "+value_s)
				return false

		arr.push(value)
		return true

	# return objs for post processing
	#
	__collectRefs:	(key, obj, parent_key, parent_obj, objs = null) ->
		if objs == null
			objs = []

		if CUI.util.isArray(obj)
			for item in obj
				@__collectRefs(key, item, null, null, objs)

		if CUI.util.isPlainObject(obj)
			for k, v of obj
				if k == key
					objs.push
						obj: parent_obj
						key: parent_key
				else
					@__collectRefs(key, v, k, obj, objs)

		return objs


	# Copy and paste from Custom Data Type Gazetteer
	@ID_API_URL =
	@JSON_EXTENSION = ".json"

	postProcessGazetteerForTable: (objects) ->

		gazetteer_objs = @__collectRefs("__gazetteer", objects)

		if gazetteer_objs.length == 0
			return CUI.resolvedPromise()

		dfr = new CUI.Deferred()

		console.debug("Gazetteer search process started. #{gazetteer_objs.length} gazetteer objects.")
		chunkSize = 10
		CUI.chunkWork.call(@,
			items: gazetteer_objs
			chunk_size: 10
			call: (items, offset) ->

				# Progress
				offsetPlusChunk = offset + chunkSize
				if offsetPlusChunk > gazetteer_objs.length
						offsetPlusChunk = gazetteer_objs.length
				progressString = "#{offset + 1}-#{offsetPlusChunk} / #{gazetteer_objs.length}"
				message = "Searching Gazetteer data. Progress: " + progressString
				ez5.splash.show("easydb4migration.progress", payload: message)
				console.debug(message)
				##

				promises = []
				for item in items
					object = item.obj[item.key]
					promises.push(@__findAndAddGazetteerDataById(object))
				return CUI.whenAll(promises)
		).always(=>
			console.debug("Gazetteer search process finished.")
			return dfr.resolve()
		)

		return dfr.promise()

	__findAndAddGazetteerDataById: (object) ->
		deferred = new CUI.Deferred()

		gazId = object.__gazetteer.gazId
		delete object.__gazetteer
		object.gazId = gazId

		xhr = new CUI.XHR
			method: "GET"
			url: "https://gazetteer.dainst.org/doc/" + gazId + ".json"

		xhr.start().done((data) =>
			if not data
				console.warn("Empty response data for Gazetteer object.", gazId)
				return deferred.resolve()
			object.displayName = data.prefName.title
			object.otherNames = data.names

			fulltext = object.displayName
			if object.otherNames?.length > 0
				fulltext = object.otherNames.map((otherName) -> otherName.title).concat(fulltext)

			object._fulltext =
				text: fulltext
				string: object.gazId
			object._standard =
				text: object.displayName

			if data.prefLocation?.coordinates
				position =
					lng: data.prefLocation?.coordinates[0]
					lat: data.prefLocation?.coordinates[1]

				if CUI.Map.isValidPosition(position)
					object.position = position

			return deferred.resolve()
		).fail((err) =>
			@logwarn("Not possible to retrieve the Gazetteer data. ID: #{gazId}")
			object._fulltext = text: "Gazetteer #{gazId} not found."
			return deferred.resolve()
		)
		return deferred.promise()

	__processGetty: (objects, type) =>

		getty_objs = @__collectRefs("__"+type, objects)

		if getty_objs.length == 0
			return CUI.resolvedPromise()

		ids = []
		for gobj in getty_objs
			getty = gobj.obj[gobj.key]["__"+type]
			ids.push(getty.value)

		switch type
			when "getty_subject"
				sel0 = 'SELECT id, getty_subject_id, 0 as getty_term_id, easydb_getty_term_preferred_text as term FROM '+@getEasydb4TableName("getty_subject")+' WHERE id'
			when "getty_term"
				sel0 = 'SELECT a.id, b.getty_subject_id, a.getty_term_id, term_text as term FROM '+@getEasydb4TableName("getty_term")+' a LEFT JOIN '+@getEasydb4TableName("getty_subject")+' b ON (b.id = a.lk_getty_subject_id) WHERE a.id'
			else
				@logerror("Unknown __processGetty type: "+type)
				return CUI.rejectedPromise()

		sel = sel0 + ' IN ('+ids.join(",")+')'

		console.debug "select:", sel0

		q = @query(sel)

		q.done (result) =>
			getty_entries = {}

			for row in result.rows
				entry =
					__getty_subject_id: row.getty_subject_id
					__getty_term_id: row.getty_term_id
					__term: row.term

				getty_entries[row.id] = entry

			for gobj in getty_objs
				row_id = gobj.obj[gobj.key]["__"+type].value
				for k, v of getty_entries[row_id]
					gobj.obj[gobj.key][k] = v

			return

		return q


	postProcessGettyForTable: (objects) ->

		dfr = new CUI.Deferred()

		@__processGetty(objects, "getty_subject")
		.fail(dfr.reject)
		.done =>
			@__processGetty(objects, "getty_term")
			.fail(dfr.reject)
			.done(dfr.resolve)

		return dfr.promise()



	postProcessGNDForTable: (objects) ->

		gnd_objs = @__collectRefs("__gnd", objects)

		if gnd_objs.length == 0
			return CUI.resolvedPromise()

		dfr = new CUI.Deferred()

		ids = []
		for gobj in gnd_objs
			ids.push(gobj.obj[gobj.key].__gnd.value)

		sel = """SELECT *, COALESCE("800c", "800g", "800k", "800s", "800p", '') AS conceptName FROM """+@getEasydb4TableName("dnb_normdaten")+""" WHERE id IN ("""+ids.join(",")+""")"""

		@query(sel)
		.fail(dfr.reject)
		.done (result) =>
			gnd_entries = {}

			for row in result.rows

				entry =
					conceptName: row.conceptName
					conceptURI: "http://d-nb.info/gnd/"+row["001"]

				entry._fulltext =
					text: entry.conceptName
					string: entry.conceptURI

				gnd_entries[row.id] = entry


			for gobj in gnd_objs
				dnb_id = gobj.obj[gobj.key].__gnd.value
				for k, v of gnd_entries[dnb_id]
					gobj.obj[gobj.key][k] = v

			# console.error "post process", objects, gnd_objs, sel, result, ids

			dfr.resolve()


		return dfr.promise()


	getTargetColumn: (source_table_name, source_column_name) =>
		tb_mapping = @__mapping.source_table[source_table_name]
		if not tb_mapping?.target_table_name
			return null

		target_col = tb_mapping?.source_column?[source_column_name]
		if not target_col?.target_column_name
			return null

		target_schema = @getTableByName(tb_mapping.target_table_name)

		target_col_name = target_col?.target_column_name

		if not target_col_name
			return null

		parts = target_col_name.split("#")

		target_column = target_schema?._column_by_name[parts[0]]
		if not target_column
			return null

		if target_column._foreign_key
			table: target_schema
			other_table: ez5.schema.CURRENT._table_by_id[target_column._foreign_key.referenced_table.table_id]
			column: target_column
		else
			table: target_schema
			column: target_column
			key: parts[1]


	storeTextFile: (filename, txt) ->
		new CUI.XHR
			method: 'POST'
			url: @getFileRootUrl()+filename
			body: txt
		.start()

	storeFile: (filename, json) ->
		# @loginfo("Storing file: ", filename)
		# console.debug "storing file:", filename
		# console.debug filename, JSON.stringify(json).length

		new CUI.XHR
			method: 'POST'
			url: @getFileRootUrl()+filename
			json_data: json
			json_pretty: true
		.start()


ez5.session_ready ->
	ez5.rootMenu.registerApp(Easydb4Migration)
	Easydb4Migration.plugins = new CUI.PluginRegistry(class: Easydb4MigrationPlugin)
	Easydb4Migration.tools = new CUI.PluginRegistry(class: Easydb4MigrationTool)
