#pragma rtGlobals=3		// Use modern global access method and strict wave access.
#pragma version = 1.41
#pragma IgorVersion = 6.2
#pragma ModuleName = PearlElog

// author: matthias.muntwiler@psi.ch
// Copyright (c) 2013-17 Paul Scherrer Institut

// Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//   http://www.apache.org/licenses/LICENSE-2.0

/// @file
/// @brief interface for writing ELOG entries with Igor graphs as attachment.
/// @ingroup ArpesPackage
///
///
/// the functions in this module support the following ELOG features:
/// - submit new entries and replies to existing entries.
/// - text field, list box, and check box attributes.
/// - attach any Igor graph to ELOG.
/// - configurable logbook templates for logbooks that share the same configuration.
/// - common server configurations available on the ELOG command line
///   (hostname, port, SSL, username, password, sub-directory).
/// - not specific to the configuration at PEARL.
///   PEARL code is concentrated in the elog_init_pearl_templates() function.
/// - the configuration of the ELOG server and logbooks
///   as well as the most recently used attributes are persisted in the preference file.
///
/// usage:
/// 1. the administrator of the ELOG server creates logbook templates
///    according to the configuration of the logbooks.
///    the templates are written in Igor code.
/// 2. the user opens logbooks via the _Open ELOG panel_ menu item.
///    before first use, select a template and enter a name for the logbook.
///    the new logbook is written to the preference file,
///    and can afterwards be opened directly.
/// 3. if the server requires a user name and password,
///    click the login button.
/// 4. edit the message, attributes and attachments as necessary, and submit to ELOG.
/// 5. log out before saving the experiment to clear the password.
///
/// @attention the user name and password are stored in the global data tree of an experiment.
/// it is not possible to handle passwords safely in Igor.
/// they can be read by anyone having access to an open Igor experiment or a saved experiment file
/// (unless the password is reset before saving).
/// therefore:
/// - use a password for the ELOG server which is different from your other passwords.
/// - clear the password (logout button in the panel) before saving an experiment.
///
/// elog command line
///@verbatim
/// elog -h <hostname> [-p port] [-d subdir]
///                              Location where elogd is running
///     -l logbook               Name of logbook
///     -s                       Use SSL for communication
///     [-v]                     For verbose output
///     [-w password]            Write password defined on server
///     [-u username password]   User name and password
///     [-f <attachment>]        Up to 50 attachments
///     -a <attribute>=<value>   Up to 50 attributes
///     [-r <id>]                Reply to existing message
///     [-q]                     Quote original text on reply
///     [-e <id>]                Edit existing message
///     [-x]                     Suppress email notification
///     [-n 0|1|2]               Encoding: 0:ELcode,1:plain,2:HTML
///     -m <textfile>] | <text>
///@endverbatim
///
///
/// @author matthias muntwiler, matthias.muntwiler@psi.ch
///
/// @copyright 2013-17 Paul Scherrer Institut @n
/// Licensed under the Apache License, Version 2.0 (the "License"); @n
///   you may not use this file except in compliance with the License. @n
///   You may obtain a copy of the License at
///   http://www.apache.org/licenses/LICENSE-2.0

/// @namespace PearlElog
/// @brief interface for writing ELOG entries with Igor graphs as attachment.
///
/// PearlElog is declared in @ref pearl-elog.ipf.

static strconstant package_name = "pearl_elog"
static strconstant package_path = "root:packages:pearl_elog:"

/// main function to initialize ELOG and to open an ELOG panel.
///
/// this function takes care of all necessary initialization, configuration, and preferences.
/// if a panel exists, it will be moved to the front.
///
/// @param	logbook	name of the logbook
/// 			if empty, the user is prompted to select or create a logbook by elog_prompt_logbook().
///
///
function pearl_elog(logbook)
	string logbook
		
	if (init_package() == 0)
		load_prefs()
		string templates = list_logbooks(templates=1)
		if (ItemsInList(templates) < 1)
			elog_init_pearl_templates()
		endif
	endif
	
	if (strlen(logbook) == 0)
		logbook = elog_prompt_logbook()
	endif
	
	string win_name = logbook + "ElogPanel"
	if (strlen(logbook) > 0)
		if (strlen(WinList(win_name, ";", "")) > 0)
			DoWindow /F $win_name
		else
			win_name = PearlElogPanel(logbook)
			STRUCT WMWinHookStruct s
			s.eventCode = 0
			s.winName = win_name
			elog_panel_hook(s)
		endif
	endif
end

/// save preferences and recent values before Igor opens a new experiment.
static function IgorBeforeNewHook(igorApplicationNameStr)
	string igorApplicationNameStr
	save_prefs()
	cleanup_temp_files()
	return 0
end

/// save preferences and recent values before Igor quits.
static function IgorQuitHook(igorApplicationNameStr)
	string igorApplicationNameStr
	save_prefs()
	cleanup_temp_files()
	return 0
end

/// initialize the package and reload preferences after an experiment is loaded.
static function AfterFileOpenHook(refNum,file,pathName,type,creator,kind)
	Variable refNum,kind
	String file,pathName,type,creator
	if( (kind >= 1) && (kind <= 2))
		init_package(clean=1)
		load_prefs()
	endif
	return 0
end

static constant kdfRoot = 0
static constant kdfVolatile = 1
static constant kdfPersistent = 2
static constant kdfTemplates = 3

/// get the package, logbook, or template datafolder.
///
/// @param	name		name of logbook or template, or empty string for respective parent folder.
///
/// @param	category		parameter category:
///			@arg		kdfRoot			package root
///			@arg 		kdfVolatile		volatile
///			@arg 		kdfPersistent		persistent
///			@arg 		kdfTemplates		template
///
/// @returns data folder reference
///
static function /df get_elog_df(name, category)
	string name
	variable category
	
	dfref df_package = $package_path
	dfref df_persistent = df_package:persistent
	dfref df_volatile = df_package:volatile
	
	switch(category)
		case kdfRoot:
			dfref df_parent = df_package
			break
		case kdfPersistent:
			dfref df_parent = df_persistent
			break
		case kdfTemplates:
			dfref df_parent = df_persistent:templates
			break
		case kdfVolatile:
			dfref df_parent = df_volatile
			break
		default:
			Abort "get_elog_df: undefined data folder category."
	endswitch
	
	if ((strlen(name) > 0) && (category >= 1))
		if (category == kdfTemplates)
			dfref df_logbooks = df_parent
		else
			dfref df_logbooks = df_parent:logbooks
		endif
		dfref df_logbook = df_logbooks:$name
		return df_logbook
	else
		return df_parent
	endif
end

/// initialize the package data folder.
///
/// the data folder is initialized with a default, local configuration without any logbooks.
/// the server configuration should be set in the preferences.
///
/// @param clean		decides what to do if the package configuration exists.
///					@arg 0 (default)	keep existing configuration.
///					@arg 1			overwrite existing configuration.
///
static function init_package([clean])
	variable clean
	
	if (ParamIsDefault(clean))
		clean = 0
	endif
	
	dfref savedf = getdatafolderdfr()
	dfref df_root = get_elog_df("", kdfRoot)
	if ((clean == 0) && (DataFolderRefStatus(df_root) == 1))
		return 1
	endif
	
	setdatafolder root:
	newdatafolder /o/s packages
	newdatafolder /o/s $package_name
	dfref df_package_root = getdatafolderdfr()
	newdatafolder /o/s volatile
	dfref df_volatile = getdatafolderdfr()
	newdatafolder /o logbooks
	setdatafolder df_package_root
	newdatafolder /o/s persistent
	dfref df_persistent = getdatafolderdfr()
	newdatafolder /o logbooks
	newdatafolder /o templates
	
	// common configuration
	setdatafolder df_persistent
	string /g elog_path = "c:\\program files (x86)\\ELOG\\elog.exe"
	string /g hostname = "localhost"
	variable /g port = 0 // 0 = unspecified (default)
	variable /g ssl = 0 // 0 = plain text (incl. passwords), 1 = secure connection
	string /g subdir = ""
	variable /g loglevel = 4

	setdatafolder savedf
	return 0
end

/// setup PEARL template logbooks.
///
/// template logbooks for PEARL.
///
/// @remark this function is specific to the setup at PEARL.
///
function elog_init_pearl_templates()
	dfref savedf = getdatafolderdfr()

	dfref df_root = get_elog_df("", kdfRoot)
	dfref df_persistent = get_elog_df("", kdfPersistent)
	dfref df_templates = get_elog_df("", kdfTemplates)
	
	// Experiments template
	setdatafolder df_templates
	newdatafolder /o/s Experiments
	
	// attributes (persistent)
	// available attributes
	string /g attributes = "author;project;p-group;sample;source;task;technique;file;valid;"
	// controls corresponding to attributes
	// prefix determines the control type: sv_ = setvariable (string), pm_ = popup menu, cb = check box
	string /g controls = "sv_author;sv_project;sv_pgroup;sv_sample;pm_source;pm_task;pm_technique;sv_file;cb_valid;"
	// attributes with fixed options, value item declares the options string
	string /g options = "source=sources;task=tasks;technique=techniques"
	// attributes which must be defined
	string /g required_attributes = "author;project;p-group;sample;source;task;technique;valid"

	// option lists
	string /g sources = "Manual Entry;PShell;Scienta Data;SScan Data;Prosilica Data;OTF Data;Beamline Status;LEED Data;QMS Data;Matrix Data;Igor Pro;Other"
	string /g tasks = "Measurement;Optimization;Analysis;Sample Preparation;Sample Storage;Comment;Development;Maintenance;Test;Other"
	string /g techniques = "XPS;UPS;XPD;XAS;XMCD;PhD;ARUPS;STM;STS;LEED;AES;QMS;MBE;Sputter/Anneal;Test;Other"

	// Calculations template
	setdatafolder df_templates
	newdatafolder /o/s Calculations
	
	// attributes (persistent)
	// available attributes
	string /g attributes = "author;project;p-group;sample;program;revision;machine;job;experiment;source path;result path;valid"
	// controls corresponding to attributes
	// prefix determines the control type: sv_ = setvariable (string), pm_ = popup menu, cb = check box
	string /g controls = "sv_author;sv_project;sv_pgroup;sv_sample;pm_program;sv_revision;pm_machine;sv_job;sv_experiment;sv_sourcepath;sv_resultpath;cb_valid"
	// attributes with fixed options, value item declares the options string
	string /g options = "program=programs;machine=machines"
	// attributes which must be defined
	string /g required_attributes = "author;project;sample"

	// option lists
	string /g programs = "PMSCO;EDAC;MSC;SSC;MUFPOT;DMSUP;Other"
	string /g machines = "PC;VM;Ra;Merlin;llcx;Other"

	// System template
	setdatafolder df_templates
	newdatafolder /o/s System
	
	// attributes (persistent)
	// available attributes
	string /g attributes = "author;type;system;source;file"
	// controls corresponding to attributes
	// prefix determines the control type: sv_ = setvariable (string), pm_ = popup menu, cb = check box
	string /g controls = "sv_author;pm_type;pm_system;pm_source;sv_file"
	// attributes with fixed options, value item declares the options string
	string /g options = "type=types;system=systems;source=sources"
	// attributes which must be defined
	string /g required_attributes = "author;type;system"

	// option lists
	string /g types = "Installation;Repair;Maintenance;Test;Commissioning;Bakeout;Incident;Cool-down;Warm-up;Storage;Other"
	string /g systems = "Vacuum;Control System;BL;XA;XP;SA;SP;T;LL;Monochromator;Carving;Scienta;STM;PC-Scienta;PC-Matrix;PC-Console;PC-Console-Win;PC-XP;EPS;LAC;Desiccator;Other"
	string /g sources = "Manual Entry;PShell;Scienta Data;SScan Data;Prosilica Data;OTF Data;Beamline Status;LEED Data;QMS Data;Matrix Data;Igor Pro;Other"

	setdatafolder savedf
	return 0
end

/// initialize volatile variables.
///
/// create and initialize all volatile variables for the configured notebooks.
/// values of existing variables are not changed.
///
/// this function must be called after new logbooks have been configured,
/// specifically by elog_create_logbook() and load_prefs().
///
static function init_volatile_vars()
	dfref savedf = GetDataFolderDFR()
	
	dfref df_volatile_root = get_elog_df("", kdfVolatile)
	dfref df_volatile_parent = df_volatile_root:logbooks

	string logbooks = list_logbooks()
	string logbook
	variable nlb = ItemsInList(logbooks)
	variable ilb
	
	SetDataFolder df_volatile_root
	if (exists("temp_graph_files") != 2)
		string /g temp_graph_files = ""
	endif

	for (ilb = 0; ilb < nlb; ilb += 1)
		logbook = StringFromList(ilb, logbooks)
		
		SetDataFolder df_volatile_parent
		if (DataFolderExists(logbook))
			SetDataFolder $logbook
		else
			NewDataFolder /o/s $logbook
		endif
		
		if (exists("username") != 2)
			string /g username = ""
		endif
		if (exists("password") != 2)
			string /g password = ""
		endif
		if (exists("msg_id") != 2)
			variable /g msg_id = 0
		endif
		if (exists("att_list") != 1)
			make /n=(0,3) /t /o attach_list
			make /n=(0,3) /i /o attach_sel
		endif
		if (exists("url") != 2)
			string /g url = ""
		endif
	endfor
	
	SetDataFolder savedf
	return 0
end

/// @var persistent:loglevel
/// @brief filter history messages by log level.
///
/// messages are printed to the history only if their level exceeds this setting.
/// this is a global variable in the "persistent" folder of the package configuration.
///
/// @arg 0 none. do not print any messages. no messages have this level.
/// @arg 1 critical. severe error with possible data corruption.
/// @arg 2 error. a function did not complete successfully.
/// @arg 3 warning. everything worked fine, but some attention of the user is required.
/// @arg 4 info. status message which may be useful to the normal user.
/// @arg 5 debug. status message which may be useful to the developer.
///

/// create a new logbook.
///
/// create a new empty logbook or duplicate from a template.
///
/// @param name				name of the new logbook.
///							if the logbook exists, the existing logbook folder is killed
///							and replaced by a new one.
///							this may fail if a window is still open.
///
/// @param template			name of the template.
///							if empty string, a new empty logbook is created.
///
function elog_create_logbook(name, [template])
	string name
	string template

	if (ParamIsDefault(template))
		template = ""
	endif

	dfref savedf = getdatafolderdfr()
	dfref df_root = get_elog_df("", kdfRoot)
	dfref df_persistent_root = get_elog_df("", kdfPersistent)
	dfref df_persistent_parent = df_persistent_root:logbooks
	dfref df_volatile_root = get_elog_df("", kdfVolatile)
	dfref df_volatile_parent = df_volatile_root:logbooks

	setdatafolder df_persistent_parent
	if (CheckName(name, 11) != 0)
		setdatafolder savedf
		Abort "invalid logbook name"
		return -1
	endif
	
	if (strlen(template) > 0)
		dfref df_template = get_elog_df(template, kdfTemplates)
		dfref df_existing = get_elog_df(name, kdfPersistent)
		if (DataFolderRefStatus(df_existing))
			KillDataFolder /Z df_existing
		endif
		DuplicateDataFolder df_template, df_persistent_parent:$name
	else
		NewDataFolder /o/s df_persistent_parent:$name
		
		// ELOG logbook name
		string /g logbook = name
		// attributes (persistent)
		// available attributes
		string /g attributes = ""
		// controls corresponding to attributes
		// prefix determines the control type: sv_ = setvariable (string), pm_ = popup menu, cb = check box
		string /g controls = ""
		// attributes with fixed options, value item declares the options string
		string /g options = ""
		// attributes which must be defined
		string /g required_attributes = ""
	endif

	// usage data (persistent)
	setdatafolder get_elog_df(name, kdfPersistent)
	string /g recent = ""
	string /g recent_message = ""

	init_volatile_vars()
	
	setdatafolder savedf
	return 0
end

/// set global module configuration parameters
///
function elog_config([elog_path, hostname, port, subdir])
	string elog_path
	string hostname
	variable port
	string subdir
	
	dfref df = get_elog_df("", kdfPersistent)
	
	if (!ParamIsDefault(elog_path))
		svar /sdfr=df g_elog_path = elog_path
		g_elog_path = elog_path
	endif
	if (!ParamIsDefault(hostname))
		svar /sdfr=df g_hostname = hostname
		g_hostname = hostname
	endif
	if (!ParamIsDefault(port))
		nvar /sdfr=df g_port = port
		g_port = port
	endif
	if (!ParamIsDefault(subdir))
		svar /sdfr=df g_subdir = subdir
		g_subdir = subdir
	endif
end

/// set username and password for login to a logbook
///
/// the username and password are stored (in plain text) in global strings under the selected logbook folder.
/// this is necessary for sending data to the ELOG server.
///
/// call elog_logout() to clear the password variables and to avoid unintended use of your credentials.
///
/// @warning
/// igor does not have a built-in mechanism to protect passwords.
/// user names and passwords are stored in plain text in the data folder tree.
/// as such they are saved to experiment files and preferences.
///
/// @param	logbook			name of the target logbook.
///
function elog_login(logbook, username, password)
	string logbook
	string username
	string password

	dfref df = get_elog_df(logbook, kdfVolatile)
	svar /sdfr=df g_username=username
	svar /sdfr=df g_password=password
	g_username = username
	g_password = password
end

/// clear username and password of a logbook or all logbooks.
///
/// the username and password are stored (in plain text) in global strings under the selected logbook folder.
/// this function resets the username and password strings.
///
/// @param	logbook			name of the target logbook.
///							if empty, the passwords of all logbooks are cleared.
///
function elog_logout(logbook)
	string logbook

	dfref df = get_elog_df(logbook, kdfVolatile)
	if (strlen(logbook) > 0)
		svar /z /sdfr=df g_username=username
		svar /z /sdfr=df g_password=password
		if (svar_exists(g_username))
			g_username = ""
		endif
		if (svar_exists(g_password))
			g_password = ""
		endif
	else
		dfref df2 = df:logbooks
		variable nlb = CountObjectsDFR(df2, 4)
		variable ilb
		string slb
		for (ilb = 0; ilb < nlb; ilb += 1)
			slb = GetIndexedObjNameDFR(df2, 4, ilb)
			if (strlen(slb) > 0)
				elog_logout(slb)
			endif
		endfor
	endif
end

/// save persistent package data to the preferences file.
///
/// saves everything under the persistent folder of the package.
///
static function save_prefs()
	dfref saveDF = GetDataFolderDFR()

	dfref df = get_elog_df("", kdfPersistent)
	if (DataFolderRefStatus(df) == 1)
		string fullPath = SpecialDirPath("Packages", 0, 0, 0)
		fullPath += package_name
		NewPath/O/C/Q tempPackagePrefsPath, fullPath
		fullPath += ":preferences.pxp"
		SetDataFolder df
		SaveData /O /Q /R fullPath
		KillPath/Z tempPackagePrefsPath
	endif
	
 	SetDataFolder saveDF
end

/// load persistent package data from the preferences file.
///
/// the preferences file is an Igor packed experiment file in a special preferences folder
static function load_prefs()
	dfref saveDF = GetDataFolderDFR()

	variable result = -1
	init_package()
	setdatafolder get_elog_df("", kdfPersistent)
	
	string fullPath = SpecialDirPath("Packages", 0, 0, 0)
	fullPath += package_name

	GetFileFolderInfo /Q /Z fullPath
	if (V_Flag == 0) // Disk directory exists?
		fullPath += ":preferences.pxp"
		GetFileFolderInfo /Q /Z fullPath
		if (V_Flag == 0) // Preference file exist?
			LoadData /O /R /Q fullPath
			init_volatile_vars()
			result = 0
		endif
	endif
	
 	SetDataFolder saveDF
	return result
end

/// get a list of configured logbooks or templates.
///
/// this is list of data folder names under persistent:logbooks (or persistent:templates).
/// the function does not check whether the folders contain valid data.
///
/// @param	templates	select whether logbooks (0, default) or templates (1) are returned.
///
/// @return semicolon-separated list of logbooks
///
static function /s list_logbooks([templates])
	variable templates
	
	if (ParamIsDefault(templates))
		templates = 0
	endif
	
	dfref df_persistent = get_elog_df("", kdfPersistent)
	if (templates)
		dfref df_logbooks = df_persistent:templates
	else
		dfref df_logbooks = df_persistent:logbooks
	endif
	string logbooks = ""
	
	variable nlb = CountObjectsDFR(df_logbooks, 4)
	variable ilb
	string slb
	for (ilb = 0; ilb < nlb; ilb += 1)
		slb = GetIndexedObjNameDFR(df_logbooks, 4, ilb)
		if (strlen(slb) > 0)
			logbooks = AddListItem(slb, logbooks)
		endif
	endfor
	
	return SortList(logbooks, ";", 16)
end

/// validate attributes
///
/// @returns 0 if all required attributes are present and enumerated items are correct.
/// non-zero if a violation is detected.
///
/// @todo
/// function currently not implemented, always returns 0
function elog_validate_attributes(logbook, attributes)
	
	string logbook	// name of the logbook (as in igor folder name)
	string attributes	// key=value list of attributes, semicolon separated
	
	variable result = 0
	return result
end

/// create a new entry in ELOG
///
/// this is the main function to create a new entry in a logbook.
///
/// @param	logbook			name of the target logbook.
///
/// @param	attributes		key=value list of attributes, semicolon separated.
///
/// @param	message		free text part of the entry.
///
/// @param	encoding		encoding of message, 0:ELcode, 1:plain (default), 2:HTML.
///
/// @param	graphs			names of graph windows to be added as attachments, semicolon separated.
///
/// @param	replyto			existing message ID (> 1) to follow up on.
///							0 or default: start new thread.
///
/// @return					ID number of the new entry (> 0), or error code (< 0).
///			@arg -1:			failed to save temporary message file.
///			@arg -2:			invalid/missing command line.
///			@arg -3:			invalid/missing attributes.
///			@arg -4:			elog returned error
///
function elog_create_entry(logbook, attributes, message, [encoding, graphs, replyto])
	string logbook
	string attributes
	string message
	variable encoding
	string graphs
	variable replyto
	
	if (ParamIsDefault(encoding))
		encoding = 1
	endif
	if (ParamIsDefault(graphs))
		graphs = ""
	endif
	if (ParamIsDefault(replyto))
		replyto = 0
	endif
	
	dfref savedf = getdatafolderdfr()
	dfref df_general = get_elog_df("", kdfPersistent)
	dfref df_volatile = get_elog_df(logbook, kdfVolatile)
	
	variable result = 0
	nvar /sdfr=df_volatile msg_id
	nvar /sdfr=df_general loglevel
	
	if (elog_validate_attributes(logbook,attributes) != 0)
		if (loglevel >= 2)
			print "ELOG: failed to validate attributes."
		endif
		result = -3
	endif

	string cmd = prepare_command_line(logbook)
	if (strlen(cmd) == 0)
		if (loglevel >= 2)
			print "ELOG: failed to prepare command line."
		endif
		result = -2
	endif

	if (replyto >= 1)
		cmd += " -r " + num2str(replyto)
	endif
	cmd += " -n " + num2str(encoding)

	variable nattr = ItemsInList(attributes, ";")
	variable iattr
	string sattr
	for (iattr = 0; (iattr < nattr) && (result == 0); iattr += 1)
		sattr = StringFromList(iattr, attributes, ";")
		if (strlen(StringFromList(1, sattr, "=")) > 0)
			sattr = ReplaceString("%", sattr, "")
			cmd += " -a \"" + sattr + "\""
		endif
	endfor

	if (result == 0)
		string cmd_graphs = prepare_graph_attachments(graphs)
		cmd += " " + cmd_graphs
	endif
	
	if ((result == 0) && (strlen(message) > 0))
		string messagefile = create_message_file(message)
		if (strlen(messagefile) > 0)
			cmd += " -m \"" + messagefile + "\""
			cmd += " > elog.log"
			if (loglevel >= 5)
				print cmd
			endif
			string cmd_file_path = create_cmd_file(cmd)
			if (strlen(cmd_file_path) > 0)
				ExecuteScriptText cmd_file_path
				variable id = parse_result()
				if (id > 0)
					msg_id = id
					if (loglevel >= 4)
						print "ELOG: sent message " + num2str(id)
					endif
				else
					if (loglevel >= 2)
						print "ELOG: sending message failed."
					endif
					result = -4
				endif
			else
				result = -2
			endif
		else
			if (loglevel >= 2)
				print "ELOG: failed to create temporary message file."
			endif
			result = -1
		endif
	endif
	
	setdatafolder savedf
	return result
end

/// add one or more graphs to an existing ELOG entry
///
/// @param	logbook	name of the target logbook
/// @param	id		identification number of the existing entry
/// @param	graphs	names of graph windows to be added as attachments, semicolon separated
///
/// @warning this will delete all existing attachments of the entry!
function elog_add_attachment(logbook, id, graphs)
	string logbook
	variable id // existing entry ID
	string graphs // names of graph windows to be added as attachments, semicolon separated
	
	dfref savedf = getdatafolderdfr()
	dfref df_general = get_elog_df("", kdfPersistent)
	dfref df_volatile = get_elog_df(logbook, kdfVolatile)
	
	variable result = 0
	nvar /sdfr=df_volatile msg_id
	nvar /sdfr=df_general loglevel
	
	string cmd = prepare_command_line(logbook)
	if (strlen(cmd) == 0)
		result = -2 // error: invalid/missing command line
	endif

	cmd += " -e " + num2str(id)

	if (result == 0)
		string cmd_graphs = prepare_graph_attachments(graphs)
		if (strlen(cmd_graphs) == 0)
			result = -3 // error: invalid/missing graphs
		endif
	endif
	
	if (result == 0)
		cmd += " " + cmd_graphs
		cmd += " > elog.log"
		string cmd_file_path = create_cmd_file(cmd)
		if (strlen(cmd_file_path) > 0)
			ExecuteScriptText cmd_file_path
			id = parse_result()
			if (id > 0)
				msg_id = id
				if (loglevel >= 4)
					print "ELOG: attached graphs to message " + num2str(id)
				endif
			else
				if (loglevel >= 2)
					print "ELOG: failed to attach graphs."
				endif
				result = -4 // error: elog returned error
			endif
		else
			result = -2 // error: invalid command line
		endif
	endif
	
	setdatafolder savedf
	return result
end

/// format the ELOG command and essential address arguments.
///
/// the following arguments are included (from global variables) if applicable:
/// host name, port, SSL, sub-dir, username, password
/// the result string does not include leading or trailing space
///
/// @param	logbook	name of the target logbook
static function /s prepare_command_line(logbook)
	string logbook
	
	dfref df_general = get_elog_df("", kdfPersistent)
	dfref df_persistent = get_elog_df(logbook, kdfPersistent)
	dfref df_volatile = get_elog_df(logbook, kdfVolatile)
	
	svar /sdfr=df_general elog_path
	svar /sdfr=df_general hostname
	nvar /sdfr=df_general port
	nvar /sdfr=df_general ssl
	svar /sdfr=df_general subdir
	nvar /sdfr=df_general loglevel
	svar /sdfr=df_volatile username
	svar /sdfr=df_volatile password

	string cmd
	cmd = "\"" + elog_path + "\""
	if (loglevel >= 5)
		cmd += " -v"
	endif
	cmd += " -h " + hostname
	if ((nvar_exists(port)) && (port > 0))
		cmd += " -p " + num2str(port)
	endif
	if ((svar_exists(subdir)) && (strlen(subdir) > 0))
		cmd += " -d " + subdir
	endif
	cmd += " -l \"" + logbook + "\""
	if ((nvar_exists(ssl)) && (ssl != 0))
		cmd += " -s"
	endif
	//cmd += " -w " + password
	if (svar_exists(username) && svar_exists(password) && (strlen(username) > 0) && (strlen(password) > 0))
		cmd += " -u " + username + " " + password
	endif
	
	if (loglevel >= 5)
		print cmd
	endif
	
	return cmd
end

/// format the URL for display to the user
///
///
/// @param	logbook	name of the target logbook
///
static function /s format_url(logbook)
	string logbook
	
	dfref df_general = get_elog_df("", kdfPersistent)
	
	svar /sdfr=df_general hostname
	nvar /sdfr=df_general port
	nvar /sdfr=df_general ssl
	svar /sdfr=df_general subdir

	string cmd = ""
	if ((nvar_exists(ssl)) && (ssl != 0))
		cmd += "https://"
	else
		cmd += "http://"
	endif
	cmd += hostname
	if ((nvar_exists(port)) && (port > 0))
		cmd += ":" + num2str(port)
	endif
	if ((svar_exists(subdir)) && (strlen(subdir) > 0))
		cmd += "/" + subdir
	endif
	cmd += "/" + logbook
	
	return cmd
end

/// prepare screenshots of graph windows for attachments
///
/// prepares the attachment files from Igor graph windows
/// and returns the arguments to the elog command to attach the files.
/// file names are returned without path.
/// the result string does not include leading or trailing space.
///
/// @param	graphs	names of graph windows to be added as attachments, semicolon separated
///
static function /s prepare_graph_attachments(graphs)
	string graphs // names of graph windows to be added as attachments, semicolon separated
	
	string cmd = ""
	variable ngraphs = ItemsInList(graphs, ";")
	variable igraph
	string sgraph
	string graph_path
	for (igraph = 0; igraph < ngraphs; igraph += 1)
		sgraph = StringFromList(igraph, graphs, ";")
		graph_path = create_graph_file(sgraph, igraph)
		if (strlen(graph_path) > 0)
			cmd += " -f \"" + graph_path + "\""
		endif
	endfor
	
	return cmd
end

static function /s get_timestamp(sep)
	string sep
	Variable now = DateTime
	string dat = ReplaceString("-", Secs2Date(DateTime, -2), "")
	string tim = ReplaceString(":", Secs2Time(DateTime, 3), "")
	return dat + sep + tim
end

/// save the message to a temporary text file
///
/// the file is saved to the Temporary directory returned by igor's SpecialDirPath function
/// under the file name "elog_temp_message.txt".
/// the function returns the name of the file (excluding path!)
///
/// @note percent characters (%) cannot be passed to elog.
/// they are removed silently from the message.
///
/// @param message			text message to save to the file.
/// @return (string)			name of the created file.
///							empty string if unsuccessful.
///
/// @version 1.41				the return value has changed from full path to file name only
///							due to the limited length of the command line (1024 bytes).
///
static function /s create_message_file(message)
	string message
	
	message = ReplaceString("%", message, "")
	string path = SpecialDirPath("Temporary", 0, 1, 0)
	variable len = strlen(path)
	string filename
	
	if (numtype(len) == 0)
		filename = "elog_temp_message.txt"
		path += filename
		variable f1
		Open f1 as path
		fprintf f1, message
		Close f1
	else
		filename = ""
	endif
	
	return filename
end

/// save a graph to a temporary graphics file
///
/// the file is saved to the Temporary directory returned by igor's SpecialDirPath function.
/// the file name contains a time stamp and the specified file index to make it unique.
/// the function returns the name of the file (excluding path!)
///
/// the full path is added to the temp_graph_files global list.
/// a hook function will delete the files listed there when igor quits.
///
/// @param graphname		object name of the graph to save.
/// @param fileindex			incrememtal index of the file within one submission.
///							the file name is made unique by a time stamp and this file index.
///							submissions within the same second must have a unique file index.
/// @return (string)			name of the created file.
///							empty string if unsuccessful.
///
/// @version 1.41				the return value has changed from full path to file name only
///							due to the limited length of the command line (1024 bytes).
///
static function /s create_graph_file(graphname, fileindex)
	string graphname
	variable fileindex
	
	dfref df_volatile_root = get_elog_df("", kdfVolatile)
	svar /sdfr=df_volatile_root temp_graph_files

	string path = SpecialDirPath("Temporary", 0, 1, 0)
	string ts = get_timestamp("_")
	variable len = strlen(path)
	string filename

	if (numtype(len) == 0)
		filename = "elog_" + ts + "_" + num2str(fileindex) + ".png"
		path += filename
		SavePICT /B=72 /E=-5 /M /O /W=(0,0,8,6) /WIN=$graphname /Z as path
		if (v_flag == 0)
			temp_graph_files = AddListItem(path, temp_graph_files, ";", inf)
		else
			filename = ""
		endif
	else
		filename = ""
	endif
	
	return filename
end

/// write the command line to a file.
///
/// the command script changes the working directory to the Temporary directory.
/// it also deletes a previous elog.log file.
///
/// @note somewhere the command line (even inside command files) is limited to 1024 bytes.
/// for this reason all files should now be in the Temporary directory assigned by igor.
///
static function /s create_cmd_file(cmd)
	string cmd
	
	dfref df_general = get_elog_df("", kdfPersistent)
	nvar /sdfr=df_general loglevel

	if (strlen(cmd) >= 1024)
		if (loglevel >= 2)
			print "ELOG: command line too long (add fewer attachments)."
		endif
		return ""
	endif
	
	string work_path = SpecialDirPath("Temporary", 0, 1, 0)
	variable len = strlen(work_path)
	if (numtype(len) == 0)
		string cmdx
		string cmd_path = work_path + "elog_temp_cmd.bat"
		
		variable f1
		Open f1 as cmd_path
		cmdx = "c:\r\n"
		fprintf f1, cmdx
		cmdx = "cd \"" + work_path + "\"\r\n"
		fprintf f1, cmdx
		cmdx = "del elog.log"
		fprintf f1, cmdx + "\r\n"
		fprintf f1, cmd
		Close f1
	else
		cmd_path = ""
	endif
	
	return cmd_path
end

static function /s get_log_path()
	string path = SpecialDirPath("Temporary", 0, 1, 0)
	variable len = strlen(path)
	if (numtype(len) == 0)
		path += "elog.log"
	else
		path = ""
	endif
	
	return path
end

/// delete temporary files created by the ELOG module.
///
/// this deletes all temporary graph files that are referenced by the volatile temp_graph_files list.
/// temp_graph_files is a semicolon-delimited string.
/// items are added by create_graph_file().
///
/// this function should be called before a new experiment is loaded or igor quits.
///
static function cleanup_temp_files()
	dfref df_volatile_root = get_elog_df("", kdfVolatile)
	if (DataFolderRefStatus(df_volatile_root))
		svar /sdfr=df_volatile_root /z temp_graph_files
		if (SVAR_Exists(temp_graph_files))
			variable nfi = ItemsInList(temp_graph_files)
			variable ifi
			string sfi
			for (ifi = 0; ifi < nfi; ifi += 1)
				sfi = StringFromList(ifi, temp_graph_files)
				DeleteFile /Z sfi
			endfor
			temp_graph_files = ""
		endif
	endif
	return 0
end

static strconstant elog_success_msg = "Message successfully transmitted"
static strconstant elog_parse_id = "ID=%u"

/// parse the result file from an elog invokation.
///
/// @returns the ID of the generated message,
/// or a value <= 0 if an error occurred.
///
static function parse_result()
	dfref df_general = get_elog_df("", kdfPersistent)
	nvar /sdfr=df_general loglevel

	string path = get_log_path()
	string line = ""
	string output = ""
	variable success = 0
	variable id = -1
	string part1 = ""
	string part2 = ""
	
	variable len = strlen(path)
	if (numtype(len) == 0)
		variable f1
		Open /R/Z f1 as path
		if (v_flag == 0)
			do
				FReadLine f1, line
				if (strlen(line) > 0)
					part1 = StringFromList(0, line, ",")
					part2 = ReplaceString(" ", StringFromList(1, line, ","), "")
					success = cmpstr(part1, elog_success_msg) == 0
					if (success)
						sscanf part2, elog_parse_id, id
					endif
				else
					break
				endif
				output += line
			while(!success)
			Close f1
		endif
	endif
	if (loglevel >= 5)
		print output
	endif
	
	return id
end

/// prompt to open or create a logbook
///
function /s elog_prompt_logbook()
	string logbooks = list_logbooks(templates=0)
	logbooks = AddListItem("(new)", logbooks)
	string templates = list_logbooks(templates=1)
	templates = AddListItem("(none)", templates)
	
	string logbook = StringFromList(0, logbooks)
	string template = StringFromList(0, logbooks)
	string name = ""
	string username = ""
	string password = ""
	
	prompt logbook, "logbook", popup logbooks
	prompt template, "template", popup templates
	prompt name, "new logbook name"

	doprompt "select logbook", logbook, template, name
	if (!v_flag)
		if (cmpstr(logbook, "(new)") == 0)
			elog_create_logbook(name, template=template)
			logbook = name
		endif
	else
		logbook = ""
	endif
	return logbook
end

/// prompt the user for login to a logbook
///
function elog_prompt_login(logbook)
	string logbook
	
	string logbooks = list_logbooks(templates=0)

	string username = ""
	string password = ""
	
	prompt logbook, "logbook", popup logbooks
	prompt username, "user name"
	prompt password, "password (blank to log out)"

	doprompt "log in to logbook", logbook, username, password
	if (!v_flag)
		elog_login(logbook, username, password)
	endif
	
	return v_flag
end

/// open a new panel for submitting data to ELOG.
///
/// this function creates only the panel but not the necessary data folders.
/// call init_package() and load_prefs() once before creating panels.
///
/// @param	logbook	name of the target logbook
///
function /s PearlElogPanel(logbook)
	string logbook

	dfref savedf = getdatafolderdfr()
	dfref df_general = get_elog_df("", kdfPersistent)
	dfref df_persistent = get_elog_df(logbook, kdfPersistent)
	dfref df_volatile = get_elog_df(logbook, kdfVolatile)

	string win_name = logbook + "ElogPanel"
	string win_title = "ELOG " + logbook
	
	NewPanel /K=1 /N=$win_name /W=(600,200,1200,700) as win_title
	win_name = s_name
	ModifyPanel /w=$win_name cbRGB=(52224,52224,65280)

	svar /sdfr=df_persistent attributes
	svar /sdfr=df_persistent controls
	svar /sdfr=df_persistent options
	wave /t /sdfr=df_volatile attach_list
	wave /sdfr=df_volatile attach_sel
	svar /sdfr=df_volatile url
	
	variable iattr
	variable nattr = ItemsInList(attributes, ";")
	string s_attr
	string s_control
	string s_option
	string persistent_path = GetDataFolder(1, df_persistent)
	string volatile_path = GetDataFolder(1, df_volatile)
	string options_path
	string variable_path
	variable ypos = 2
	variable height = 0
	
	for (iattr = 0; iattr < nattr; iattr += 1)
		s_attr = StringFromList(iattr, attributes, ";")
		s_control = StringFromList(iattr, controls, ";")
		strswitch(s_control[0,1])
			case "sv":
				SetVariable $s_control, win=$win_name, pos={0,ypos}, size={300,16}, bodyWidth=230
				SetVariable $s_control, win=$win_name, title=s_attr, value= _STR:""
				SetVariable $s_control, win=$win_name, userdata(attribute)=s_attr
				ypos += 18
				break
			case "pm":
				options_path = persistent_path + StringByKey(s_attr, options, "=", ";")
				PopupMenu $s_control, win=$win_name, pos={0,ypos}, size={300,21}, bodyWidth=230
				PopupMenu $s_control, win=$win_name, title=s_attr
				PopupMenu $s_control, win=$win_name, mode=1, popvalue="Test", value= #options_path
				PopupMenu $s_control, win=$win_name, userdata(attribute)=s_attr
				ypos += 23
				break
			case "cb":
				CheckBox $s_control, win=$win_name, pos={70,ypos}, size={300,14}
				CheckBox $s_control, win=$win_name, title=s_attr, value= 1
				CheckBox $s_control, win=$win_name, userdata(attribute)=s_attr
				ypos += 17
				break
		endswitch
	endfor
	ypos = max(ypos, 80)
	
	TitleBox t_attach, win=$win_name, pos={308,5}, size={70,14}, title="Attachments", frame=0
	height = ypos - 21 - 4
	ListBox lb_attach, win=$win_name, pos={308,21}, size={264,height}
	ListBox lb_attach, win=$win_name, listWave=attach_list
	ListBox lb_attach, win=$win_name, mode=1, selWave=attach_sel, selRow=-1
	ListBox lb_attach, win=$win_name, widths={20,160,80}
	ListBox lb_attach, win=$win_name, help={"Choose graphs to attach to the message."}

	Button b_attach_top, win=$win_name, pos={420,2}, size={40,18}, title="top"
	Button b_attach_top, win=$win_name, fcolor=(56576,60928,47872)
	Button b_attach_top, win=$win_name, proc=PearlElog#bp_attach_top
	Button b_attach_top, win=$win_name, help={"Select top graph for attachment."}
	Button b_attach_all, win=$win_name, pos={460,2}, size={40,18}, title="all"
	Button b_attach_all, win=$win_name, fcolor=(56576,60928,47872)
	Button b_attach_all, win=$win_name, proc=PearlElog#bp_attach_allnone
	Button b_attach_all, win=$win_name, help={"Select all graphs for attachment."}
	Button b_attach_none, win=$win_name, pos={500,2}, size={40,18}, title="none"
	Button b_attach_none, win=$win_name, fcolor=(56576,60928,47872)
	Button b_attach_none, win=$win_name, proc=PearlElog#bp_attach_allnone
	Button b_attach_none, win=$win_name, help={"Deselect all attachments."}
	Button b_save_graphs, win=$win_name, pos={540,2}, size={40,18}, title="save"
	Button b_save_graphs, win=$win_name, fcolor=(56576,60928,47872)
	Button b_save_graphs, win=$win_name, proc=PearlElog#bp_save_graphs
	Button b_save_graphs, win=$win_name, help={"Save selected graphs as PNG bitmap files."}
	Button b_attach_up, win=$win_name, pos={576,20}, size={20,20}, title="\\W517"
	Button b_attach_up, win=$win_name, fcolor=(56576,60928,47872)
	Button b_attach_up, win=$win_name, proc=PearlElog#bp_attach_updown
	Button b_attach_up, win=$win_name, help={"Move selected graph up."}
	Button b_attach_dw, win=$win_name, pos={576,40}, size={20,20}, title="\\W523"
	Button b_attach_dw, win=$win_name, fcolor=(56576,60928,47872)
	Button b_attach_dw, win=$win_name, proc=PearlElog#bp_attach_updown
	Button b_attach_dw, win=$win_name, help={"Move selected graph down."}

	ypos += 246-160
	Button b_submit,win=$win_name, pos={70,ypos},size={46,20},proc=PearlElog#bp_submit,title="Submit"
	Button b_submit,win=$win_name, help={"Submit form data to ELOG (new entry)."}
	Button b_submit,win=$win_name, fcolor=(56576,60928,47872)
	Button b_clear,win=$win_name, pos={120,ypos},size={46,20},proc=PearlElog#bp_clear,title="Clear"
	Button b_clear,win=$win_name, help={"Clear the form fields"}
	Button b_clear,win=$win_name, fcolor=(56576,60928,47872)

	ypos += 272-246
	variable_path = volatile_path + "msg_id"
	SetVariable sv_id,win=$win_name, pos={51,ypos},size={119,16},bodyWidth=77
	SetVariable sv_id,win=$win_name, title="ID",value=$variable_path
	SetVariable sv_id,win=$win_name, help={"ID of last submitted message, or message to attach or reply to."}

	TitleBox t_host, win=$win_name, pos={170,ypos+4}, size={112.00,14.00}, frame=0
	TitleBox t_host, win=$win_name, variable=url

	ypos += 270-272
	Button b_attach,win=$win_name, pos={170,ypos},size={48,20},proc=PearlElog#bp_attach,title="Attach"
	Button b_attach,win=$win_name, help={"Attach the selected graph to an existing ELOG entry (correct ID required)."}
	Button b_attach,win=$win_name, fcolor=(56576,60928,47872)
	Button b_reply,win=$win_name, pos={220,ypos},size={48,20},proc=PearlElog#bp_submit,title="Reply"
	Button b_reply,win=$win_name, help={"Submit form data to ELOG as a reply to an existing message (correct ID required)."}
	Button b_reply,win=$win_name, fcolor=(56576,60928,47872)
	Button b_login,win=$win_name, pos={550,ypos},size={46,20},proc=PearlElog#bp_login,title="Login"
	Button b_login,win=$win_name, help={"Enter user name and password."}
	Button b_login,win=$win_name, fcolor=(56576,60928,47872)
	Button b_logout,win=$win_name, pos={550,ypos},size={46,20},proc=PearlElog#bp_logout,title="Logout"
	Button b_logout,win=$win_name, help={"Clear user name and password."}
	Button b_logout,win=$win_name, fcolor=(56576,60928,47872), disable=3

	SetWindow $win_name, hook(elogPanelHook)=PearlElog#elog_panel_hook
	SetWindow $win_name, userdata(logbook)=logbook

	ypos += 160-270
	TitleBox t_message,win=$win_name, pos={10,ypos},size={58,16},fixedSize=1,frame=0,anchor=RT,title="Message"
	DefineGuide UGH0={FT,ypos},UGV0={FL,70},UGH1={FB,-52},UGV1={FR,-2}
	NewNotebook /F=0 /N=Message /OPTS=3 /W=(115,404,345,341)/FG=(UGV0,UGH0,UGV1,UGH1) /HOST=# 
	Notebook kwTopWin, defaultTab=20, statusWidth=0, autoSave=0
	Notebook kwTopWin fSize=10, fStyle=0, textRGB=(0,0,0)
	RenameWindow #,Message
	string nb_name = win_name + "#Message"
	SetActiveSubwindow ##

	// restore recently used attributes and message
	svar /z /sdfr=df_persistent recent
	if (svar_exists(recent) && (strlen(recent) > 0))
		set_panel_attributes(win_name, recent)
	endif
	svar /z /sdfr=df_persistent recent_message
	if (svar_exists(recent_message) && (strlen(recent_message) > 0))
		set_panel_message(win_name, recent_message)
	endif
	Notebook $nb_name selection={startOfFile,startOfFile}, findText={"",1}
	
	setdatafolder savedf
	return win_name
end

static function elog_panel_hook(s)
	STRUCT WMWinHookStruct &s

	Variable hookResult = 0

	switch(s.eventCode)
		case 0: // activate
			string logbook = GetUserData(s.winName, "", "logbook")
			if (strlen(logbook) > 0)
				dfref df_volatile = get_elog_df(logbook, kdfVolatile)
				svar /sdfr=df_volatile url
				url = format_url(logbook)
				update_attach_items(logbook)
			endif
			break
		case 6: // resize
			// move bottom-aligned controls when the window is resized
			variable b_top = s.winRect.bottom + 4
			Button b_submit,pos={70,b_top}
			Button b_clear,pos={120,b_top}
			TitleBox t_host, pos={170,b_top+4}
			b_top += 24
			Button b_attach,pos={170,b_top}
			Button b_reply,pos={220,b_top}
			Button b_login, pos={550,b_top}
			Button b_logout, pos={550,b_top}
			b_top += 2
			SetVariable sv_id,pos={51,b_top}
			break
	endswitch

	return hookResult		// 0 if nothing done, else 1
end

static constant kAttachColSel = 0
static constant kAttachColTitle = 1
static constant kAttachColName = 2

/// update the list of attachments
static function update_attach_items(logbook)
	string logbook

	dfref savedf = getdatafolderdfr()
	dfref df_volatile = get_elog_df(logbook, kdfVolatile)
	wave /t /sdfr=df_volatile attach_list
	wave /sdfr=df_volatile attach_sel

	if (!waveexists(attach_list))
		return -1
	endif
	string names = WinList("*", ";", "WIN:1;VISIBLE:1")
	names = SortList(names, ";", 16)

	// remove closed graphs
	variable i
	variable k
	variable n = DimSize(attach_list, 0)
	string s
	for (i = n-1; i >= 0; i -= 1)
		s = attach_list[i][kAttachColName]
		if (WhichListItem(s, names) < 0)
			DeletePoints /M=0 i, 1, attach_list, attach_sel
		endif
	endfor
	
	// add new graphs
	n = ItemsInList(names)
	for (i = 0; i < n; i += 1)
		s = StringFromList(i, names)
		FindValue /text=s /txop=4 /z attach_list
		if (v_value < 0)
			k = DimSize(attach_list, 0)
			Redimension /n=(k+1,3) attach_list, attach_sel
			//InsertPoints /M=0 k, 1, attach_list, attach_sel
			attach_list[k][kAttachColSel] = ""
			attach_list[k][kAttachColTitle] = ""
			attach_list[k][kAttachColName] = s
			attach_sel[k][kAttachColSel] = 32
			attach_sel[k][kAttachColTitle] = 0
			attach_sel[k][kAttachColName] = 0
		endif
	endfor

	// update titles
	n = DimSize(attach_list, 0)
	for (i = n-1; i >= 0; i -= 1)
		s = attach_list[i][kAttachColName]
		getwindow /z $s, wtitle
		if (v_flag == 0)
			attach_list[i][kAttachColTitle] = s_value
		else
			attach_list[i][kAttachColTitle] = s
		endif
	endfor
	
	setdatafolder savedf
	return 0
end

/// move an attachment item in the list of attachments
static function move_attach_item(logbook, item, distance)
	string logbook
	variable item
	variable distance
	
	dfref savedf = getdatafolderdfr()
	dfref df_volatile = get_elog_df(logbook, kdfVolatile)
	wave /t /sdfr=df_volatile attach_list
	wave /sdfr=df_volatile attach_sel
	variable n = DimSize(attach_list, 0)
	variable dest = item + distance
	
	if ((item >= 0) && (item < n) && (dest >= 0) && (dest < n))
		string name = attach_list[item][kAttachColName]
		variable sel = attach_sel[item][kAttachColSel]
		DeletePoints /M=0 item, 1, attach_list, attach_sel
		InsertPoints /M=0 dest, 1, attach_list, attach_sel
		attach_list[dest][kAttachColName] = name
		update_attach_items(logbook)
		attach_sel[dest][kAttachColSel] = sel
	endif
end

/// button procedure for the attachment up and down buttons
static function bp_attach_updown(ba) : ButtonControl
	STRUCT WMButtonAction &ba

	switch( ba.eventCode )
		case 2: // mouse up
			string logbook = GetUserData(ba.win, "", "logbook")
			ControlInfo /w=$ba.win lb_attach
			variable row = v_value
			dfref df = $s_datafolder
			wave /t /sdfr=df attach_list = $s_value
			if (cmpstr(ba.ctrlName, "b_attach_up") == 0)
				// up button
				if (row >= 1)
					move_attach_item(logbook, row, -1)
					ListBox lb_attach, win=$ba.win, selRow=(row-1)
				endif
			else
				// down button
				if (row < DimSize(attach_list, 0) - 1)
					move_attach_item(logbook, row, +1)
					ListBox lb_attach, win=$ba.win, selRow=(row+1)
				endif
			endif
			break
		case -1: // control being killed
			break
	endswitch

	return 0
end

/// button procedure for the Submit and Reply buttons
static function bp_submit(ba) : ButtonControl
	STRUCT WMButtonAction &ba

	switch( ba.eventCode )
		case 2: // mouse up
			string logbook = GetUserData(ba.win, "", "logbook")
			string attributes
			string message
			string graphs
			attributes = get_panel_attributes(ba.win)
			message = get_panel_message(ba.win)
			graphs = get_panel_graphs(ba.win)

			variable id
			if (cmpstr(ba.ctrlName, "b_reply") == 0)
				// Reply button
				ControlInfo /w=$ba.win sv_id
				id = v_value
			else
				// Submit button
				id = 0
			endif

			if ((elog_validate_attributes(logbook, attributes) == 0) && (strlen(message) > 0))
				variable result
				result = elog_create_entry(logbook, attributes, message, graphs=graphs, replyto=id)
				if (result == 0)
					dfref df = get_elog_df(logbook, kdfPersistent)
					svar /sdfr=df recent
					recent = attributes
					svar /sdfr=df recent_message
					recent_message = message
				else
					abort "Submission failed. Error code " + num2str(result) + "."
				endif
			else
				abort "Submission failed due to missing/invalid attribute."
			endif
			break
		case -1: // control being killed
			break
	endswitch

	return 0
end

/// select top graph window for attachment
static function bp_attach_top(ba) : ButtonControl
	STRUCT WMButtonAction &ba

	switch( ba.eventCode )
		case 2: // mouse up
			string graphs = WinName(0, 1, 1)
			set_panel_graphs(ba.win, graphs)
			break
		case -1: // control being killed
			break
	endswitch

	return 0
end

/// select/deselect all graph windows for attachment
static function bp_attach_allnone(ba) : ButtonControl
	STRUCT WMButtonAction &ba

	switch( ba.eventCode )
		case 2: // mouse up
			string logbook = GetUserData(ba.win, "", "logbook")
			dfref df_volatile = get_elog_df(logbook, kdfVolatile)
			wave /sdfr=df_volatile attach_sel
			if (cmpstr(ba.ctrlName, "b_attach_all") == 0)
				attach_sel[][kAttachColSel] = attach_sel[p][kAttachColSel] | 16
			else
				attach_sel[][kAttachColSel] = attach_sel[p][kAttachColSel] & ~16
			endif
			break
		case -1: // control being killed
			break
	endswitch

	return 0
end

static function bp_attach(ba) : ButtonControl
	STRUCT WMButtonAction &ba

	switch( ba.eventCode )
		case 2: // mouse up
			string logbook = GetUserData(ba.win, "", "logbook")
			string graphs
			graphs = get_panel_graphs(ba.win)
			
			variable id
			ControlInfo /w=$ba.win sv_id
			id = v_value

			// TODO : is there a way around this restriction?
			DoAlert /T="ELOG" 1, "This operation will replace all existing attachments. Do you want to continue?"

			if ((id > 0) && (v_flag == 1))
				variable result
				result = elog_add_attachment(logbook, id, graphs)
				if (result != 0)
					abort "Submission failed. Error code " + num2str(result) + "."
				endif
			else
				abort "Submission failed due to missing/invalid attribute."
			endif
			break
		case -1: // control being killed
			break
	endswitch

	return 0
end

static function bp_save_graphs(ba) : ButtonControl
	STRUCT WMButtonAction &ba

	switch( ba.eventCode )
		case 2: // mouse up
			string logbook = GetUserData(ba.win, "", "logbook")
			string graphs = get_panel_graphs(ba.win)
			variable ngraphs = ItemsInList(graphs, ";")

			variable igraph
			string sgraph
			string graph_path
			for (igraph = 0; igraph < ngraphs; igraph += 1)
				sgraph = StringFromList(igraph, graphs, ";")
				graph_path = create_graph_file(sgraph, igraph)
				if (strlen(graph_path) > 0)
					print graph_path
				endif
			endfor
				
			break
		case -1: // control being killed
			break
	endswitch

	return 0
end

static function bp_clear(ba) : ButtonControl
	STRUCT WMButtonAction &ba
	
	switch( ba.eventCode )
		case 2: // mouse up
			set_panel_attributes(ba.win, "", clear=1)
			set_panel_message(ba.win, "")
			set_panel_graphs(ba.win, "")
			break
		case -1: // control being killed
			break
	endswitch

	return 0
end

static function bp_login(ba) : ButtonControl
	STRUCT WMButtonAction &ba
	
	switch( ba.eventCode )
		case 2: // mouse up
			string logbook = GetUserData(ba.win, "", "logbook")
			if (elog_prompt_login(logbook) == 0)
				Button b_login, win=$ba.win, disable=3
				Button b_logout, win=$ba.win, disable=0
			endif
			break
		case -1: // control being killed
			break
	endswitch

	return 0
end

static function bp_logout(ba) : ButtonControl
	STRUCT WMButtonAction &ba
	
	switch( ba.eventCode )
		case 2: // mouse up
			string logbook = GetUserData(ba.win, "", "logbook")
			elog_logout(logbook)
			Button b_login, win=$ba.win, disable=0
			Button b_logout, win=$ba.win, disable=3
			break
		case -1: // control being killed
			break
	endswitch

	return 0
end

static function /s get_default_panel_name()
	string windowname
	windowname = StringFromList(0, WinList("*ElogPanel*", ";", "WIN:64"), ";")
	return windowname
end

/// get a list of attributes from the fields of the ELOG panel.
///
/// @param	windowname		window name of the ELOG panel
///							if empty, use default name "PearlElogPanel"
///
/// @return	list of attributes to in the format <code>"key1=value1;key2=value2"</code>.
///
static function /s get_panel_attributes(windowname)
	string windowname
	
	if (strlen(windowname) == 0)
		windowname = get_default_panel_name()
	endif
	if (strlen(windowname) == 0)
		return ""
	endif

	string controls = ControlNameList(windowname, ";")
	string attributes = ""
	string control
	string attribute
	variable ico
	variable nco = ItemsInList(controls, ";")
	for (ico = 0; ico < nco; ico += 1)
		control = StringFromList(ico, controls, ";")
		attribute = GetUserData(windowname, control, "attribute")
		if (strlen(attribute) > 0)
			ControlInfo /w=$windowname $control
			switch(v_flag)
				case 2:	// checkbox
					attributes = ReplaceNumberByKey(attribute, attributes, v_value, "=", ";")
					break
				case 3:	// popupmenu
				case 5:	// setvariable
					attributes = ReplaceStringByKey(attribute, attributes, s_value, "=", ";")
					break
			endswitch
		endif
	endfor
	
	return attributes
end

/// set the fields of the ELOG panel
///
/// @param	windowname		window name of the ELOG panel
///							if empty, use default name "PearlElogPanel"
///
/// @param	attributes		list of attributes to set (format "key1=value1;key2=value2")
///
/// @param	clear			what to do if a key is missing in attributes?
///	@arg	0 (default)		leave the field unchanged
///	@arg	1				clear the field
///
static function /s set_panel_attributes(windowname, attributes, [clear])
	string windowname
	string attributes
	variable clear
		
	if (strlen(windowname) == 0)
		windowname = get_default_panel_name()
	endif
	if (strlen(windowname) == 0)
		return ""
	endif
	if (ParamIsDefault(clear))
		clear = 0
	endif
	
	string path
	
	string logbook = GetUserData(windowname, "", "logbook")
	dfref df_persistent = get_elog_df(logbook, kdfPersistent)
	string persistent_path = GetDataFolder(1, df_persistent)
	svar /sdfr=df_persistent options
	string options_path

	string controls = ControlNameList(windowname, ";")
	string control
	string attribute
	string value
	variable numval
	variable ico
	variable nco = ItemsInList(controls, ";")
	for (ico = 0; ico < nco; ico += 1)
		control = StringFromList(ico, controls, ";")
		attribute = GetUserData(windowname, control, "attribute")
		if (strlen(attribute))
			value = StringByKey(attribute, attributes, "=", ";")
			if (strlen(value) || clear)
				ControlInfo /w=$windowname $control
				switch(v_flag)
					case 2:	// checkbox
						numval = NumberByKey(attribute, attributes, "=", ";")
						if ((numtype(numval) != 0) && clear)
							numval = 0
						endif
						if (numtype(numval) == 0)
							CheckBox $control, value=numval, win=$windowname
						endif
						break
					case 3:	// popupmenu
						options_path = persistent_path + StringByKey(attribute, options, "=", ";")
						svar values = $options_path
						numval = WhichListItem(value, values, ";") + 1
						if (numval >= 1)
							PopupMenu $control, mode=numval, win=$windowname
						endif
						break
					case 5:	// setvariable
						SetVariable /z $control, value= _STR:value, win=$windowname
						break
				endswitch
			endif
		endif
	endfor
	
	return attributes
end

/// get the message field of the ELOG panel
///
/// @param	windowname		window name of the ELOG panel
///							if empty, use default name "PearlElogPanel"
///
/// @return	message text
///
static function /s get_panel_message(windowname)
	string windowname
	
	if (strlen(windowname) == 0)
		windowname = get_default_panel_name()
	endif
	if (strlen(windowname) == 0)
		return ""
	endif

	string nb = windowname + "#Message"
	notebook $nb selection={startOfFile, endOfFile}
	getselection notebook, $nb, 2
	
	return s_selection
end

/// set the message field of the ELOG panel
///
/// @param	windowname		window name of the ELOG panel
///							if empty, use default name "PearlElogPanel"
///
/// @param	message		message text that can be passed to the @c Notebook operation.
///
/// @return	original message (unchanged)
///
static function /s set_panel_message(windowname, message)
	string windowname
	string message
	
	if (strlen(windowname) == 0)
		windowname = get_default_panel_name()
	endif

	string nb = windowname + "#Message"
	notebook $nb selection={startOfFile, endOfFile},text=message
	
	return message
end

/// get the names of the graphs selected for attachment
///
/// @param windowname	panel window name
/// @returns a semicolon-separated list,
/// or the empty string if the selection is not valid.
///
static function /s get_panel_graphs(windowname)
	string windowname // panel window name
	
	dfref savedf = getdatafolderdfr()
	if (strlen(windowname) == 0)
		windowname = get_default_panel_name()
	endif
	if (strlen(windowname) == 0)
		return ""
	endif

	string logbook = GetUserData(windowname, "", "logbook")
	dfref df_volatile = get_elog_df(logbook, kdfVolatile)
	wave /t /sdfr=df_volatile attach_list
	wave /sdfr=df_volatile attach_sel
	string graphs = ""
	string windows = ""
	string graphname

	variable n = DimSize(attach_sel, 0)
	variable i
	for (i = 0; i < n; i += 1)
		if (attach_sel[i][kAttachColSel] & 16)
			graphname = attach_list[i][kAttachColName]
			windows = WinList(graphname,  ";", "WIN:1")
			if (ItemsInList(windows) == 1)
				graphs = AddListItem(graphname, graphs, ";", inf)
			endif
		endif
	endfor
	
	return graphs
end

/// update selection of graphs for attachment
///
/// @param windowname		panel window name. looks for default panel if empty.
///
/// @param graphs			semicolon-separated list of names of graph windows to select for attachment.
///
static function /s set_panel_graphs(windowname, graphs)
	string windowname
	string graphs

	if (strlen(windowname) == 0)
		windowname = get_default_panel_name()
	endif
	if (strlen(windowname) == 0)
		return ""
	endif

	string logbook = GetUserData(windowname, "", "logbook")
	update_attach_items(logbook)
	dfref df_volatile = get_elog_df(logbook, kdfVolatile)
	wave /t /sdfr=df_volatile attach_list
	wave /sdfr=df_volatile attach_sel

	variable n = DimSize(attach_sel, 0)
	variable i
	string graphname
	for (i = 0; i < n; i += 1)
		graphname = attach_list[i][kAttachColName]
		if (WhichListItem(graphname, graphs)>= 0)
			attach_sel[i][kAttachColSel] = 48
		else
			attach_sel[i][kAttachColSel] = 32
		endif
	endfor
end