#! /bin/sh
set -e

# Introduction and mandatory help and version functions.
########################################################

readonly PROGNAME="${0##*/}"
readonly VERSION="1.13.8"

print_version () {
	cat << END
${PROGNAME} ${VERSION}
END
}

print_copyright_information () {
	cat << END
Copyright (C) 2024-2026, Étienne Mollier <emollier@emlwks999.eu>

This program is free software: you can redistribute it and/or modify it
under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or (at
your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
END
}

print_description () {
	cat << END
${PROGNAME} is a logbook and note taking helper.  It provides a way to
record seamlessly journal entries, along automated recording of the time
and date and all sorts of interesting information, like recorded
calendar(1) activities, or what could be playing on music players.
Invoked without arguments, ${PROGNAME} opens straight the log book to
create a new entry at the current time and date at the tail of the file.
Use cases include, but are not limited to: scratch pad for taking notes,
timesheet recording, or notes storage and retrieval facility; the killer
feature is that it may be all at once as the needs of the writer evolve
over time.

The "carnet" log book is a set of text files, one per week, made of
entries named "feuillets" separated by a time stamp, and specific
markers to separate days of the week; this avoids exploding the file
size of a long running log file typed for a while by a proficient
writer, while also avoiding the explosion of file numbers that could
appear with compulsive writers.

END
}

print_usage () {
	cat << END
Usage: $PROGNAME [-R|--reverse] [-d|--debug] [-h] [--help] [-v] [--version]
   or: $PROGNAME [-le|--list-entries] [-lf|--list-feuillets] [-ld|--list-dates]
   or: $PROGNAME [-lm|--list-modules] [-a|--activity] [--motd] [--edit-motd]
   or: $PROGNAME [-f|--feuillet [<TIMESTAMP> ... [-s|--search [<pattern> ...]]]]
   or: $PROGNAME [-ef|--edit-feuillet [<TIMESTAMP>]]
   or: $PROGNAME [-qf|--quick-feuillet [<TIMESTAMP> ...]]
   or: $PROGNAME [-r|--readonly [<pattern> ...]]
   or: $PROGNAME [-s|--search [<pattern> ...]]
   or: $PROGNAME [-t|--todo|--done [<status>]] [-lts|--list-todo-statuses]
   or: $PROGNAME [-m|--module <module> [<arguments> ...]]

END
}

print_help () {
	cat << END
Options:
	-a, --activity	print out a histogram of activity for each week.
	-d, --debug	print out debug information upon invocation.
	-ef, --edit-feuillet [<TIMESTAMP>]
			edit the entry pointed to by the time stamp in
			somewhat normalized format; if no time stamp is
			passed, it edits the register.txt file of the
			carnet.
	-f, --feuillet [<TIMESTAMP> ...] [-s, --search [<pattern> ...]]
			print out the entry pointed to by the time stamp
			in somewhat normalized format; the format is
			also highlighted to facilitate navigation
			between entries of carnet; if no time stamp is
			passed, it opens the register.txt file of the
			carnet; several time stamps can be passed, which
			will be printed out by order of apparition in
			the command arguments; large selections of
			feuillets by day, week or year can be filtered
			using search patterns after specifying the -s or
			--search option, which is useful to speed up
			searches in large carnets.
	-h		print out the short usage notice.
	--help		print out the full description, usage and help
			message.
	-ld, --list-dates
			list exhaustively valid dates.
	-le, --list-entries
			list week files of the carnet; this command
			is affected by the -R, or --reverse, option, see
			below.
	-lf, --list-feuillets
			list exhaustively valid feuillets.
	-lm, --list-modules
			list module programs shipped within a given
			carnet; see --module for modules purpose.
	-lts, --list-todo-statuses
			list all the available states of tasks entries,
			e.g. pending, cancelled, or lowprio; see --todo
			for more details on tasks management, those are
			really freeform states.
	-m, --module <module> [<argument> ...]
			execute a module program shipped within a given
			carnet; this is intended for power users as an
			escape hatch to e.g. implement customized search
			engines, or specific activity metrics.
	-qf, --quick-feuillet [<TIMESTAMP> ...]
			print out the entry pointed to by the time stamp
			similarly to --feuillet; this option is one
			order of magnitude faster, as it only returns
			the feuillet, instead of also calling the pager,
			searching for back references, etc; this is
			useful for certain situations, for instance for
			some unattended batch workloads.
	-R, --reverse	reverse the order in which weeks are processed;
			this is notably in use when carrying searches in
			entries or fetching todo items when doing some
			project management; this is useful to see the
			more recent week first while processing entries,
			which speeds up lookup of recent items, notably
			on large carnets.
	-r, --readonly [<pattern> ...]
			look up the present week's carnet entry in
			read-only mode, without appending a new entry;
			the option takes optionally a search pattern in
			order to rapidly locate a feuillet written in
			the past week.
	-s, --search [<pattern> ...]
			look up log book entries matching the provided
			search patterns; as a convenience, time stamps
			are highlighted for consumption by the option to
			retrieve feuillets; the command dumps the entire
			carnet in output if no pattern is given; week
			order can be reverted with -R, --reverse, to
			speed up finding recent feuillets.
	-t, --todo [<status>]
			list "todo" items found in the carnet; todo
			items can be materialized using a sort of square
			materialized by brackets like '[ ]' at the
			beginning of a line; the blank space may be
			later erased to materialize the new state of the
			task, e.g. '[*]' or '[DONE]' at the discretion
			of the writer.  The optional state argument may
			be used to list tasks materialized with a given
			state, again mostly free form at the discretion
			of the writer.  This command is affected by -R,
			--reverse.
	-v		print out the program version.
	--version	print out the program version and license terms.
	--done [<status>]
			shortcut for common markers representing "todo"
			items in the state done, primarily provided as a
			convenience to match "[*]" and other common
			patterns marking tasks completed.
	--motd		print out the message of the day.
	--edit-motd	edit the motd.txt file containing the message of
			the day.
	TIMESTAMP	this argument is one of the types YYYY to select
			a year, YYYY-QN to select a quarter, YYYY-WW to
			select a week, or YYYY-MM-DD to select a day, or
			YYYY-MM-DDThh:mm to select a feuillet, or hh:mm
			to select a feuillet written earlier today.

Environment:
	CARNET_CHRONOLOGICAL_TODO
			by default, todo items are printed out by more
			recent week first, to help focusing on recent
			issues; this boolean configuration option
			defaults to "false", but can be set to true for
			a consistent experience between tasks lists and
			regular search results.
	CARNET_CONF	indicates the file in which the carnet
			configuration is stored.  The default is
			~/.config/carnet.conf.
	CARNET_DIR	indicates the directory in which the carnet
			content is stored.  The default is to put it in
			the carnet/ directory within the user's default
			"Documents" directory.
	CARNET_EDITOR	overrides EDITOR and VISUAL environment
			variables settings.  The default is unset.
	CARNET_LANG	overrides LANG environment variable.  This is
			unset by default.
	CARNET_LESS	overrides LESS environment variable.  This is
			unset by default.
	CARNET_OVERTIME
			indicate after how many hours work on the carnet
			is considered overtime for the day; single
			integer values will express hours, otherwise
			durations may be expressed with the hh:mm
			format; unset or zero value will disable the
			option.
	CARNET_PAGER	provide a custom pager.  This is unset by
			default.  Remark: carnet does not rely on PAGER
			and will try to make use of less or more, in
			that order.
	CARNET_SHOW_MUSIC_ON_AIR
			this variable can be set to "true" or "false"
			and is provided as a convenience, to defuse the
			execution of scanning for the music playing
			while wrapping up the entry; this can help on
			slow systems to make carnet snappier, or when
			dealing with broken radio stations to avoid
			having trailing context lines with garbage.

Files:
	YYYY-WW.txt	these files, formated after the week-year and
			the week number, will store the plain text
			content of carnet entries; pretty often, those
			are the only files that are going to be present
			in a carnet directory, and that's fine.
	register.txt	this file can serve as landing pad for
			navigating through a complex carnet: it can
			store references to interesting feuillets,
			common search patterns or all kind of free form
			information that should be easy to retrieve; it
			does not have to be present if people don't feel
			the need to have one.
	motd.txt	a few lines can be dropped in this file, they
			will be printed out after the addition of an
			entry; this can be used to give and preserve the
			general orientation of the theme of the carnet;
			it is better to keep it simple and short, for
			example the vulcan idiom "live long and
			prosper"; having none is also perfectly fine.
	~/.config/carnet.conf
			This is the default configuration file for
			carnet.  It's location may be adjusted with the
			CARNET_CONF environment variable.  The file
			takes a series of carnet_key=value pairs that
			correspond to the above environment variables.
			The carnet.conf file is useful to unclutter the
			environment.  See also carnet.conf(5) manual for
			concrete samples.

Examples:
Main usage:
	carnet		without further ado, start editing immediately a
			new feuillet at the current time stamp; this is
			the main use of carnet: to record a note and
			give it a time stamp without having to think
			about it.
	carnet -s test	search and retrieve feuillets containing the
			character string "test"; pattern matching is
			case insensitive and the feuillet will highlight
			the matching strings.
	carnet -s sample sentence and '\\<pattern\\>'
			search and retrieve feuillets containing all the
			patterns "sample", "sentence", "and", and
			"\\<pattern\\>", which will be highlighted
			independently when the feuillet is returned by
			the carnet search function.
	carnet -r	read through carnet entries of the current week;
			this is useful to wrap up weekly activity
			reports.
	carnet -r topic
			search entries containing the string "topic" in
			the past week; this limits the scope of the
			search to the past week, which may be way faster
			than scanning the whole carnet to look something
			up, but would show a very partial view of the
			topic.
	carnet -f 2026-q2 -s topic
			search feuillets matching a given pattern in the
			second quarter of 2026; this slightly speeds up
			searches and avoids matching out of scope items,
			e.g. when wrapping up a report about "topic",
			but only for the second quarter of 2026; this
			kind of filtering also works for whole weeks or
			years.  Warning: there is no way to specify
			months for now.  Warning: weeks half way between
			two time windows will appear in both of them.
Task management:
	carnet -t	search and retrieve items left to do; they are
			identified by a "[ ]" or a "TODO:" at the
			beginning of a line of the feuillet.
	carnet --done	search and retrieve items done.
	carnet -t pending
			search and retrieve items to do but in pending
			state due to not having been actionable at some
			point.
	carnet -lts	list all available states of todo entries.
Feuillets referencing (Zettelkasten):
	carnet -f 2025-07-05T18:23
			retrieve the feuillet recorded on July 5th 2025
			at 18:23; feuillet retrieval will highlight RFC
			3339 compliant time stamps to facilitate
			navigation through feuillets that reference each
			others.
	carnet -f 2025-07-05
			retrieve all feuillets recorded on July 5th
			2025; this is useful when recalling about
			something done some day, but missing the right
			keywords to find it back.
	carnet -f 2025-27
			retrieve the entire week 27 of 2025 in the
			pager, including the contextual header; this is
			useful when wrapping up a weekly activity notice
			or timesheet.
	carnet -f 2025	dump all feuillets recorded in weeks including
			days in 2025; leaking days before and after is
			on purpose if we consider weeks as work units.
	carnet -f 2025-q3
			retrieve the entire third quarter of 2026,
			leaking days to fetch entire weeks similarly to
			full years.
	carnet -f 18:23
			retrieve the feuillet recorded today at 18:23.
	carnet -ef 2025-07-05T18:23
			open the text editor at the corresponding
			feuillet to bring modifications to it; this is
			useful for instance to cross reference with a
			feuillet in the future and weave further
			connections as the carnet grows.
	carnet -ef 2025-07-05
			open the text editor at the corresponding day.
	carnet -ef 2025-27
			open the text editor at the corresponding week.
	carnet -ef 18:23
			open the text editor at the feuillet recorded
			today at 18:23.
	carnet -s 2025-07-05T18:23
			retrieve all the feuillets referencing the
			2025-07-05T18:23 by its RFC-style timestamp;
			this is useful to establish cross-references.
	carnet -f	display the register of the carnet; this is
			useful to document aspects of a specific carnet
			to help with navigation throughout it, like time
			stamps of useful feuillets, or documentation of
			task management statuses.
	carnet -ef	open the register of the carnet for edition.
Escape hatches:
	carnet -m hello world
			invoke an executable file called "hello" and
			localised in the carnet directory, with argument
			"world"; this is useful to implement context
			specific user custom commands.
	carnet -lm	list modules available in the carnet directory
			that can be invoked with --module; this is
			useful when dealing with a hoard of custom
			scripts.
END
}

# Level 1 functions: primitives
###############################

readonly SEPARATOR="$(for i in $(seq 72); do printf _ ; done)"

# load_carnet_conf will attempt loading the carnet configuration, either
# from the default location of ~/.config/carnet.conf, or from anywhere
# else as defined by the environment variable CARNET_CONF.  The function
# does nothing if there is no configuration file.  This function can
# only be called once.
load_carnet_conf () {
	# Select the default configuration, unless the CARNET_CONF
	# environment variable is set.
	if [ -z "$CARNET_CONF" ]
	then CARNET_CONF="$HOME/.config/carnet.conf"
	fi
	export CARNET_CONF

	# Read the configuration from the file.
	if [ -r "$CARNET_CONF" ] && [ ! -d "$CARNET_CONF" ]
	then . "$CARNET_CONF"
	fi

	# Merge the environment to the configuration in file, so that
	# the environment takes precedence over the configuration file.
	# This won't work if variables are specified uppercase in the
	# configuration file.

	# The carnet_dir indicates where to look for the entries.
	if [ -z "$CARNET_DIR" ] && [ -n "$carnet_dir" ]
	then
		readonly carnet_dir="${carnet_dir%%/}"
	elif [ -n "$CARNET_DIR" ]
	then
		readonly carnet_dir="${CARNET_DIR%%/}"
	else
		if [ "$USER" = "root" ]
		then
			# Not necessarily wanting to encourage the use
			# of carnet as root, but it seems perfectly
			# legitimate to wish to record a host associated
			# carnet maintained by a team of system
			# administrator.  For such a scenario, putting
			# the carnet along the rest of the system logs
			# feels kind of appropriate by default.  This
			# can be overridden by the CARNET_DIR
			# environment in any case.
			readonly carnet_dir="/var/log/carnet"
		elif type xdg-user-dir > /dev/null
		then
			readonly xdg_doc_dir="$(xdg-user-dir DOCUMENTS)"
			readonly carnet_dir="${xdg_doc_dir%%/}/carnet"
		else
			readonly carnet_dir="${HOME%%/}/Documents/carnet"
		fi
	fi

	# The VISUAL editor may be specific to the carnet.
	if [ -n "$CARNET_EDITOR" ]
	then VISUAL="$CARNET_EDITOR"
	elif [ -n "$carnet_editor" ]
	then VISUAL="$carnet_editor"
	elif [ -z "$VISUAL" ] && [ -n "$EDITOR" ]
	then VISUAL="$EDITOR"
	elif [ -z "$VISUAL" ]
	then
		# No user configuration was found, so let's try to find
		# a sane editor in a selection appropriate for command
		# line usage.  Yes, this is biased list.
		if type -v nano > /dev/null
		then VISUAL=nano
		elif type -v vi > /dev/null
		then VISUAL=vi
		elif type -v emacs > /dev/null
		then VISUAL=emacs
		elif type -v vim > /dev/null
		then VISUAL=vim
		elif type -v joe > /dev/null
		then VISUAL=joe
		# ed is not a visual editor strictly speaking, but when
		# everything else fails…
		elif type -v ed > /dev/null
		then VISUAL=ed
		else
			cat >&2 <<-END
			error: no usual text editor was found on this system.
			       Please set CARNET_EDITOR to your favorite text editor.
			END
			exit 1
		fi
	fi
	# In every other cases assume VISUAL is already configured.
	export VISUAL

	# carnet may use localised time and dates in a language
	# independent of the user's, hence providing an environment
	# variable to set a specific one if requested.
	if [ -n "$CARNET_LANG" ]
	then LANG="$CARNET_LANG"
	elif [ -n "$carnet_lang" ]
	then LANG="$carnet_lang"
	fi
	# In every other cases assume LANG is already configured.
	export LANG

	if [ -z "$CARNET_LESS" ] && [ -n "$carnet_less" ]
	then CARNET_LESS="$carnet_less"
	fi
	export CARNET_LESS

	if [ -z "$CARNET_OVERTIME" ] && [ -n "$carnet_overtime" ]
	then CARNET_OVERTIME="$carnet_overtime"
	fi
	export CARNET_OVERTIME

	if [ -z "$CARNET_PAGER" ] && [ -n "$carnet_pager" ]
	then CARNET_PAGER="$carnet_pager"
	fi
	export CARNET_PAGER

	if [ -z "$CARNET_SHOW_MUSIC_ON_AIR" ] \
	   && [ -n "$carnet_show_music_on_air" ]
	then CARNET_SHOW_MUSIC_ON_AIR="$carnet_show_music_on_air"
	fi
	# The content of this variable is going to be executed and must
	# be sanitized first.
	case "$CARNET_SHOW_MUSIC_ON_AIR" in
	false | False | FALSE | disable* | Disable* | DISABLE* \
	| no | No | NO | off | Off | OFF | 0 )
		CARNET_SHOW_MUSIC_ON_AIR="false"
	;;
	* )
		CARNET_SHOW_MUSIC_ON_AIR="true"
	;;
	esac
	export CARNET_SHOW_MUSIC_ON_AIR

	if [ -z "$CARNET_CHRONOLOGICAL_TODO" ] \
	   && [ -n "$carnet_chronological_todo" ]
	then CARNET_CHRONOLOGICAL_TODO="$carnet_chronological_todo"
	fi
	# The content of this variable is going to be executed and must
	# be sanitized first.
	case "$CARNET_CHRONOLOGICAL_TODO" in
	true | True | TRUE | enable* | Enable* | ENABLE* \
	| yes | Yes | YES | on | On | ON | 1 )
		CARNET_CHRONOLOGICAL_TODO="true"
	;;
	* )
		CARNET_CHRONOLOGICAL_TODO="false"
	;;
	esac
	export CARNET_CHRONOLOGICAL_TODO

	# Ensure the carnet directory is in workable conditions.
	if [ ! -d "$carnet_dir" ]
	then
		cat >&2 <<-END
		warning: the carnet directory $carnet_dir did not exist yet.
		         It will be created now.
		END
		mkdir -p "$carnet_dir"
	fi
	if [ ! -d "$carnet_dir" ]
	then
		cat >&2 <<-END
		error: the carnet directory $carnet_dir could not be created.
		END
		exit 1
	fi
}

# terminfo_normal output the escape sequence to restore normal font
# depending on the terminal type.
terminfo_normal () {
	case "$TERM" in
	# sgr0 has crufty output on certain terminals.
	linux | screen* | tmux*)
		printf -- '\e[m'
		;;
	"" | dumb )
		# Do nothing, successfully.
		true
		;;
	*)
		tput sgr0
		;;
	esac
}

# setup_locks will check for the existence of carnet lock files for the
# text file given in argument, eventually editor swap files when
# possible, and set locks before modifying entries by the carnet, or
# aborts the script failing to do so.
#
# WARNING: this function sets the LOCKFILE global variable and a trap on
# EXIT, so it can be invoked at most once during a carnet invocation.
setup_locks () {
	local filename="$1"
	readonly LOCKFILE="$(dirname "$filename")/.${filename##*/}.carnet.lock"
	# Abort in case of detection of certain editors posing locks.
	local vi_type_lock="$(dirname "$filename")/.${filename##*/}.swp"
	if [ -e "$LOCKFILE" ] || [ -e "$vi_type_lock" ]
	then
		cat >&2 <<-END
		error: the following entries look to be under edition:
		       $filename
		END
		if [ -r "$LOCKFILE" ]
		then
			local pid="$(cat "$LOCKFILE")"
			local user="$( \
				ps aux \
				| sed -n 's/^\([^ ]*\) '"$pid"' .*/\1/p'
			)"
			printf '       carnet has pid %s' "$pid" >&2
			if [ -n "$user" ]
			then printf ', owner: %s.\n' "$user" >&2
			else printf '.\n' >&2
			fi
		else
			printf '       They seem opened outside carnet.\n'
		fi
		exit 1
	fi
	# Apply the lock file
	echo "$$" > "$LOCKFILE"
	# Make sure the lock file has the expected content.  The compare
	# and swap is not atomic, but this is much better than no checks.
	if [ "$$" != "$(cat "$LOCKFILE")" ]
	then
		cat >&2 <<-END
		error: carnet raced against pid $(cat "$LOCKFILE").
		END
		exit 1
	fi
	trap 'rm "$LOCKFILE"' EXIT
}

# list_entries lists week files currently existing in increasing order.
# The order can optionally be reversed by passing the optional argument
# -r or --reverse.  The function returns a non-zero code in case of
# problems.
list_entries () {
	local reverse="$1"
	cd "$carnet_dir"
	local files="$( \
		echo [0-9][0-9][0-9][0-9]-[0-5][0-9].txt
	)"
	if [ "$files" = "[0-9][0-9][0-9][0-9]-[0-5][0-9].txt" ]
	then
		cat >&2 <<-END
		error: there are no weekly entries in $carnet_dir.
		END
		return 1
	fi
	local order=""
	case "$reverse" in
	-r | --reverse ) local order="--reverse" ;;
	esac
	for file in $files
	do echo "$file"
	done \
	| sort $order
}

# indent_output takes stdin and shifts it by one tab on stdout.
indent_output () {
	while IFS= read line
	do printf -- '\t%s\n' "$line"
	done
}

# read_with_pager reads its standard input using one of less, more or
# cat, in that order.  The hard coded selection is on purpose, in order
# to have a controlled selection of pagers able to parse terminal escape
# controls without too much fuss.  Hard-wiring the reader may be
# reconsidered when more comfortable with the handling of a large
# selection of pagers.  Execution of the pager is on purpose, we always
# want the pager to be the final command of carnet execution.
read_with_pager () {
	if [ -t 1 ] && [ -n "$TERM" ] && [ "$TERM" != dumb ]
	then
		if [ -n "$CARNET_PAGER" ] \
		   && type "$CARNET_PAGER" > /dev/null
		then exec "$CARNET_PAGER"
		elif type less > /dev/null
		then exec less -R
		elif type more > /dev/null
		then exec more
		else exec cat
		fi
	else
		exec cat
	fi
}

# set_less_options does what is written on the can.  If neither
# environment variables LESS or CARNET_LESS are set, then offer a sane
# default value to LESS.
set_less_options () {
	type less > /dev/null 2>&1 || return 0
	if [ -n "$CARNET_LESS" ]
	then
		LESS="$CARNET_LESS"
		export LESS
	fi
	test -z "$LESS" || return 0
	local less_version="$(less --version)"
	local less_version="${less_version#* }"
	local less_version="${less_version%% *}"
	if [ "$less_version" -ge 668 ]
	then
		case "$TERM" in
		xterm* | foot* | tmux* | screen*)
			LESS="FRi--redraw-on-quit"
		;;
		*)
			LESS="FRi"
		;;
		esac
	else
		LESS="FRi"
	fi
	export LESS
}

# lineno_argument takes the name of a text editor in argument and a line
# number as a second argument.  It echoes the character string
# corresponding to the argument which is needed for the given text
# editor to start at a certain line number.
lineno_argument () {
	local editor="$1"
	local lineno="$2"
	case "$editor" in
	kate | kwrite | mousepad )
		# This line setting mechanism is compatible with KDE and
		# Xfce editors.
		printf -- '%s' "--line=$lineno"
	;;
	* )
		# This mechanism works with a range of text editors such
		# as vi, emacs or nano.
		printf -- '%s' "+$lineno"
	;;
	esac
}

# match_clock takes a string in argument and returns 0 if it matches the
# hh:mm time format, otherwise it returns 1.
match_clock () {
	local hh_mm="$1"
	if echo "$hh_mm" \
	   | grep -q '^[0-2][0-9]:[0-5][0-9]$'
	then return 0
	else return 1
	fi
}

# match_timestamp takes a string in argument and returns 0 if it matches
# the YYYY-MM-DDThh:mm date format, otherwise it returns 1.
match_timestamp () {
	local yy_mm_ddThh_mm="$1"
	if echo "$yy_mm_ddThh_mm" \
	   | grep -q '^[0-9]\{4\}-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]$'
	then return 0
	else return 1
	fi
}

# match_day takes a string in argument and returns 0 if it matches the
# YYYY-MM-DD date format, otherwise it returns 1.
match_day () {
	local yy_mm_dd="$1"
	if echo "$yy_mm_dd" \
	   | grep -q '^[0-9]\{4\}-[0-1][0-9]-[0-3][0-9]$'
	then return 0
	else return 1
	fi
}

# match_week takes a string in argument and returns 0 if it matches the
# YYYY-WW week specification format, otherwise it returns 1.
match_week () {
	local yy_ww="$1"
	if echo "$yy_ww" \
	   | grep -q '^[0-9]\{4\}-[0-5][0-9]$'
	then return 0
	else return 1
	fi
}

# match_quarter takes a string in argument and returns 0 if it matches
# the YYYY-QN quarter specification format, otherwise it returns 1.
match_quarter () {
	local yyyy_qn="$1"
	if echo "$yyyy_qn" \
	   | grep -q '^[0-9]\{4\}-[Qq][1-4]$'
	then return 0
	else return 1
	fi
}

# match_year takes a string in argument and returns 0 if it matches the
# YYYY year specification format, otherwise it returns 1.
match_year () {
	local yyyy="$1"
	if echo "$yyyy" \
	   | grep -q '^[0-9]\{4\}$'
	then return 0
	else return 1
	fi
}

# match_search takes a string in argument and returns 0 if it matches
# one of the --search argument options, otherwise it returns 1.
match_search () {
	local argument="$1"
	case "$argument" in
	-s | --search )
		return 0
	;;
	* )
		return 1
	;;
	esac
}

# print_motd prints out the message of the day.
print_motd () {
	local motd="${carnet_dir}/motd.txt"
	[ ! -r "$motd" ] || cat "$motd"
}

# create_search_pattern converts a list of patterns passed as arguments
# into a search pattern suitable for gawk consumption in output.  This
# search pattern will also be sanitized.
create_search_pattern () {
	local search_pattern=""
	for search_item in "$@"
	do
		local sanitized_search_item="$( \
			echo "$search_item" \
			| sed -z 's@/@\\/@g; s/\n/\\n/g; s/\\n$//;' \
		)"
		if [ -z "$search_pattern" ]
		then
			local search_pattern="/$sanitized_search_item/"
			continue
		fi
		local search_pattern="${search_pattern} && /${sanitized_search_item}/"
	done
	printf '%s' "$search_pattern"
}

# create_highlight_pattern converts a list of patterns passed as
# arguments into a highlight pattern suitable for sed consumption in
# output.
create_highlight_pattern () {
	local highlight_pattern=""
	for highlight_item in "$@"
	do
		# The pattern needs preparation before being fed to sed.
		local sanitized_highlight_item="$( \
			echo "$highlight_item" \
			| sed -z 's@/@\\/@g; s/\n/\\n/g; s/\\n$//;' \
		)"
		if [ -z "$highlight_pattern" ]
		then
			local highlight_pattern="$sanitized_highlight_item"
			continue
		fi
		local highlight_pattern="${highlight_pattern}|${sanitized_highlight_item}"
	done
	printf '%s' "$highlight_pattern"
}

# filter_pattern reads carnet entries from files or standard input and
# returns feuillets matching the given pattern as first argument.  This
# search pattern must be understood as a gawk search pattern, which
# offers more expressiveness than just a plain regular expression.  This
# facilitates matching multiple distinct patterns in a single go.
filter_pattern () {
	local search_pattern="$1"
	shift

	# If there is no search pattern, then do nothing and show all.
	if [ -z "$search_pattern" ]
	then
		cat "$@"
		return
	fi

	# The research proper is done by an extensive gawk script.
	gawk '
		BEGIN{
			RS="\n\n'"$SEPARATOR"'\n|\n\n([0-2][0-9]:[0-6][0-9] :[^\n]*)";
			ORS="\n\n'"$SEPARATOR"'\n";
			IGNORECASE=1;
			indate = 0;
			inheader = 1;
			matching_feuillets = 0;
		}

		/^'$SEPARATOR'\n/ {
			inheader = 1;
		}

		/^Date :/ {
			indate = 1;
			inheader = 0;
			DATE = $0;
			sub("Date", "", DATE);
		}

		(FNR==1 || indate==1) {
			indate = 0;
			HEAD = RT;
			sub(".*\n\n([0-2][0-9]:[0-6][0-9] :)", "&", HEAD)
			next;
		}

		('"$search_pattern"' && inheader == 0) {
			ENTRY = $0;
			sub("\n\n", " : ", HEAD);
			# Normalize time stamp to facilitate reuse as feuillet.
			sub(/\)$/, "", DATE);
			sub(/ ?: /, "T", HEAD);
			sub(/ :/, ") :", HEAD);
			WEEK = DATE;
			sub(".*\\(", "", WEEK);
			gsub("-", " ", WEEK);
			WEEK = WEEK " 00 00 00";
			WEEK = mktime(WEEK);
			WEEK = strftime("%G-%V", WEEK);
			OUTPUT = WEEK DATE HEAD TIMESTAMP ENTRY;
			print OUTPUT;
			matching_feuillets += 1;
		}

		{
			HEAD = RT;
		}

		END{
			ORS="\n";
			if (matching_feuillets == 1) {
				print "1 feuillet found.";
				exit;
			}
			print matching_feuillets, "feuillets found.";
		}
	' "$@"
}

# locate_year takes a year in argument and shows all the weeks that
# include days in the given year.  For year N, this can range from week
# (N-1)-52, to (N+1)-1, also possibly (N-1)-53.  Printing out a whole
# year is mostly useful to speed up searches when looking up somewhat
# recent feuillets in large carnets.
locate_year () {
	local year="$1"
	local first_week="$(date -d"$year-01-01" +%G-%V)"
	local last_week="$(date -d"$year-12-31" +%G-%V)"
	cd "$carnet_dir"

	# First and last days of the year may leak on the previous or
	# next years respectively in terms of week-year.
	if [ "$year" != "${first_week%-*}" ] && [ -r "$first_week.txt" ]
	then local weeks="$(echo "$first_week.txt" "$year"-[0-5][0-9].txt)"
	elif [ "$year" != "${last_week%-*}" ] && [ -r "$last_week.txt" ]
	then local weeks="$(echo "$year"-[0-5][0-9].txt "$last_week.txt")"
	else local weeks="$(echo "$year"-[0-5][0-9].txt)"
	fi

	for week in $weeks
	do
		printf '\n\n%s\n%s\n' "$SEPARATOR" "$SEPARATOR"
		cat "$week"
	done
}

# locate_quarter takes a quarter (YYYY-QN) in argument and shows all the
# weeks that include days in the given quarter.  Feuillets may leak a
# few days before and after the quarter range in order to include the
# full weeks.
locate_quarter () {
	local yyyy_qn="$1"
	local yyyy="${yyyy_qn%-*}"
	local qn="${yyyy_qn#*-}"
	local weeks=""

	cd "$carnet_dir"
	if [ "$qn" = "Q1" ] || [ "$qn" = "q1" ]
	then
		local first_week="$(date -d"$yyyy-01-01" +%G%V)"
		local last_week="$(date -d"$yyyy-03-31" +%G%V)"
	elif [ "$qn" = "Q2" ] || [ "$qn" = "q2" ]
	then
		local first_week="$(date -d"$yyyy-04-01" +%G%V)"
		local last_week="$(date -d"$yyyy-06-30" +%G%V)"
	elif [ "$qn" = "Q3" ] || [ "$qn" = "q3" ]
	then
		local first_week="$(date -d"$yyyy-07-01" +%G%V)"
		local last_week="$(date -d"$yyyy-09-30" +%G%V)"
	else # qn = q4
		local first_week="$(date -d"$yyyy-10-01" +%G%V)"
		local last_week="$(date -d"$yyyy-12-31" +%G%V)"
	fi
	local weeks="$( \
		seq -w "$first_week" "$last_week" \
		| sed 's/\(..\)$/-\1.txt/'
	)"

	for week in $weeks
	do
		test -r "$week" || continue
		printf '\n\n%s\n%s\n' "$SEPARATOR" "$SEPARATOR"
		cat "$week"
	done
}


# Level 2 functions: glue layer
###############################

# list_todo_statuses returns the list of candidate statuses for todo
# items.
list_todo_statuses () {
	cd "$carnet_dir"
	sed -n 's/^\[\([^]]*\)\].*$/\1/p' $(list_entries) \
	| sort \
	| uniq \
	| grep -v \
		-e '^[0-9,]\+$' \
		-e '^\*$' \
		-e '^DONE$' \
		-e '^x$' \
		-e '^ $' \
		-e '^…$'
}

# list_feuillets returns the exhaustive list of feuillets.
list_feuillets () {
	cd "$carnet_dir"
	local reverse="$1"
	gawk '
		BEGIN {
			RS="\n\n";
			FS="\n";
			ORS="\n";
		}

		/^[\n]*'"$SEPARATOR"'\nDate : [^(]* ([^)]*)/ {
			DATE=$0;
			gsub("^[\n]*'"$SEPARATOR"'\nDate : [^(]* \\(|\\)", "", DATE);
		}

		/^[\n]*[0-2][0-9]:[0-5][0-9] :/ {
			gsub("^[\n]+| :.*", "", $0);
			print(DATE "T" $0);
		}
	' $(list_entries $reverse)
}

# list_dates returns the exhaustive list of dates.
list_dates () {
	cd "$carnet_dir"
	local reverse="$1"
	gawk '
		BEGIN {
			RS="\n\n";
			FS="\n";
			ORS="\n";
		}

		/^[\n]*'"$SEPARATOR"'\nDate : [^(]* ([^)]*)/ {
			gsub("^[\n]*'"$SEPARATOR"'\nDate : [^(]* \\(|\\)", "", $0);
			print;
		}
	' $(list_entries $reverse)
}

# paint_timestamps takes no arguments.  It reads its standard input for
# text and highlights all occurrences of time stamps in its standard
# output.
paint_timestamps () {
	# Neutralize painting when dealing with basic or no terminals.
	if [ -z "$TERM" ] || [ "$TERM" = dumb ]
	then
		cat
		exit
	fi
	local ts_highlight="$(tput bold)$(tput setaf 4)"
	local normal="$(terminfo_normal)"
	sed 's/\([0-9]\{4\}-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]:[0-5][0-9]\)/'"$ts_highlight"'\1'"$normal"'/g'
}

# paint_highlight_pattern takes a pattern to highlight as an argument.
# It reads its standard input for text and highlights all occurrences of
# time stamps in its standard output.
paint_highlight_pattern () {
	local highlight_pattern="$1"
	# Neutralize painting when dealing with basic or no terminals.
	if [ -z "$TERM" ] \
	   || [ "$TERM" = dumb ] \
	   || [ -z "$highlight_pattern" ]
	then
		cat
		exit
	fi
	local ts_highlight="$(tput bold)$(tput setaf 1)"
	local normal="$(terminfo_normal)"
	gawk '
		BEGIN{ IGNORECASE=1 }
		{
			gsub(/'"$highlight_pattern"'/,
				"'"$ts_highlight"'&'"$normal"'");
			print;
		}
	'
}

# search_log prints entries of the carnet which match a pattern passed
# as argument.
search_log () {
	local search_pattern="$(create_search_pattern "$@")"
	local highlight_pattern="$(create_highlight_pattern "$@")"
	cd "$carnet_dir"

	if "$REVERSE_WEEK_ORDER"
	then local entries="$(list_entries --reverse)"
	else local entries="$(list_entries)"
	fi

	filter_pattern "$search_pattern" $entries \
	| paint_highlight_pattern "$highlight_pattern" \
	| paint_timestamps
}

# show_back_references takes a feuillet reference and returns the list
# of feuillets that contain references to toward the given feuillet.
# This is useful to navigate the graph of connections between feuillets
# upstream.
show_back_references () {
	local feuillet="$1"
	local search_pattern="$( \
		create_search_pattern "\<$feuillet(\>|T)"
	)"
	cd "$carnet_dir"
	local out="$( \
		filter_pattern "$search_pattern" $(list_entries) \
		| sed -n 's/^....-.. : .\+ (.*\([0-9]\{4\}-[0-1][0-9]-[0-3][0-9]T[0-2][0-9]:[0-6][0-9]\).*) :.*/\1/p'
	)"
	if [ -z "$out" ]
	then
		echo none
		exit 0
	fi
	echo "$out"
}

# show_music_on_air captures the music playing at t time and prints it
# to standard output.  If nothing plays or if no player is available at
# all, then it simply outputs nothing.
show_music_on_air () {
	# Check that mpc is available and can connect to mpd before use.
	if type mpc >/dev/null 2>&1 && mpc status >/dev/null 2>&1
	then
		local on_air="$( \
			mpc status --format '[[%artist% - ]%title%]|[%file%]' \
			| sed -n 1p \
		)"
		if [ "${on_air% n/a  *}" = "volume:" ]
		then local on_air=
		fi
	fi
	if [ -z "$on_air" ] && type mocp >/dev/null 2>&1
	then
		local on_air="$(mocp -Q '%artist - %song' 2>/dev/null ||:)"
		# Trim leading " - " if any, this comes from radio
		# stations not able to differentiate authors and titles
		# in their metadata stream.
		local on_air="${on_air# - }"
	fi
	if [ -n "$on_air" ]
	then local on_air=" on air: $on_air"
	fi
	echo "$on_air"
}

# calculate_overtime takes the CARNET_OVERTIME value and tries to
# estimate whether too much time has been spent since the first entry of
# the day has been written.  If CARNET_OVERTIME is unset or 0 hour, the
# functionality is ignored, otherwise the integer value stored in the
# environment variable will store the duration in hours tolerated
# between the current entry and the first entry of the day.
calculate_overtime () {
	# First check whether it makes sense to calculate overtime.
	case "$CARNET_OVERTIME" in
	'' | '0' | '0:0' | '0:00' | '00:00' ) return ;;
	esac
	# Then we begin to capture arguments of calculate_overtime.
	local yyyy_mm_dd="$1"
	local hh_mm="$2"
	local file="$3"
	local overtime_hh="${CARNET_OVERTIME%:*}"
	local overtime_h="${overtime_hh#0}"
	if [ "${CARNET_OVERTIME#*:}" != "${CARNET_OVERTIME}" ]
	then
		local overtime_mm_partial="${CARNET_OVERTIME#*:}"
		local overtime_m_partial="${overtime_mm_partial#0}"
	else
		local overtime_m_partial=0
	fi
	# This is the total overtime in minutes
	local overtime_m="$((60 * overtime_h + overtime_m_partial))"
	# Check whether we already have an entry today: if so, obtain
	# the time, or nothing if not.
	test -r "$file" || return
	local start_hh_mm="$(
		gawk '/^Date : .* \('"$yyyy_mm_dd"'\)$/ {
			getline;
			getline header;
			split(header, Time, / :/);
			print Time[1];
		}' \
		"$file"
	)"
	test -n "$start_hh_mm" || return
	# Slice hh:mm to separate the two numbers.
	local start_hh="${start_hh_mm%:*}"
	# Trim potential leading 0 tripping octal calculations.
	local start_h="${start_hh#0}"
	# Same dance for the current hour.
	local hh="${hh_mm%:*}"
	local h="${hh#0}"
	# Calculate the durations; minutes can be negative.
	local duration_h="$((h - start_h))"
	# Same dance as the hours above, but for minutes.
	local start_mm="${start_hh_mm#*:}"
	local start_m="${start_mm#0}"
	local mm="${hh_mm#*:}"
	local m="${mm#0}"
	# This is the total duration in minutes, not just the relative one.
	local duration_m="$((60 * duration_h + m - start_m))"
	if [ "$duration_m" -le "$((overtime_m - 30))" ]
	then
		# Still time to work
		return
	elif [ "$duration_m" -le "$overtime_m" ]
	then
		local rest="$((overtime_m - duration_m))"
		echo " day is ending: still ${rest}m to go"
	elif [ "$duration_m" -le "$((overtime_m + 30))" ]
	then
		local trail="$((duration_m - overtime_m))"
		echo " warning: overtime for ${trail}m."
	else
		echo " WARNING: EXCESSIVE OVERTIME"
	fi
}

# list_modules does what is written on the can.  It looks up the carnet
# directory and lists executable files that can be invoked to be run as
# modules.
list_modules () {
	local file
	for file in "$carnet_dir"/*
	do
		if [ -x "$file" ]
		then echo "${file#${carnet_dir}/}"
		fi
	done
}

# run_module runs modules associated to a carnet, that is, an executable
# stored in the carnet_dir.  This may be useful to power users after
# very specific capabilities, or to prototype new functionalities.
run_module () {
	local module="$1"
	if [ -z "$module" ]
	then
		cat >&2 <<-END
		error: please specify the carnet module name.
		END
		exit 1
	fi
	shift
	load_carnet_conf
	if [ ! -x "$carnet_dir/$module" ]
	then
		cat >&2 <<-END
		error: the module $module does not exist in carnet directory:
		       $carnet_dir
		       or it exists but is not executable.
		END
		exit 1
	fi
	# If this is not shipped via environment, then there is no
	# obvious way for the module to get the carnet_dir location.
	CARNET_DIR="$carnet_dir"
	export CARNET_DIR
	exec "$carnet_dir/$module" "$@"
}

# locate_feuillet takes a time stamp in format YYYY-MM-DDThh:mm and
# outputs the corresponding entry of the carnet if one is found,
# otherwise it outputs nothing and returns an error code.  It takes an
# optional second argument "only_location" to instruct the function to
# only return the file name and line number of the entry, useful for
# passing as argument of a text editor.
locate_feuillet () {
	local yy_mm_ddThh_mm="$1"
	local only_location="$2"
	local week="$(date -d"$yy_mm_ddThh_mm" +"%G-%V")"
	local weekfile="$carnet_dir/${week}.txt"
	local yy_mm_dd="$(echo "$yy_mm_ddThh_mm" | cut -f1 -dT)"
	local hh_mm="$(   echo "$yy_mm_ddThh_mm" | cut -f2 -dT)"
	if [ "$only_location" = "only_location" ]
	then local extra_found_instructions="print FILENAME\":\"NR ;exit 0;"
	else local extra_found_instructions="print \"$SEPARATOR\";"
	fi
	gawk '
		BEGIN {
			inday=0;
			intime=0;
			found=0;
		}

		/^Date ?: [^ ]* \([^)]*\)$/ {
			inday=0;
		}

		/^[0-2][0-9]:[0-5][0-9] ?:/ {
			intime=0;
		}

		/^Date ?: [^ ]* \('"$yy_mm_dd"'\)$/ {
			inday=1;
		}

		/^'"$hh_mm"' ?:/ {
			if (inday) {
				intime=1;
				found=1;
				'"$extra_found_instructions"'
			}
		}

		(intime && inday) {
			if (/^'"$SEPARATOR"'$/) exit;
			print;
		}

		END {
			if (!found) {
				print "error: no entry on '"$yy_mm_ddThh_mm"'." \
				> "/dev/stderr";
				exit 1;
			}
		}
	' "$weekfile"
}

# locate_day takes a date in format YYYY-MM-DD and outputs the
# corresponding day of the carnet if one is found, otherwise it outputs
# nothing and returns an error code.  It takes an optional second
# argument "only_location" to instruct the function to only return the
# file name and line number of the entry, useful for passing as argument
# of a text editor.
locate_day () {
	local yy_mm_dd="$1"
	local only_location="$2"
	local week="$(date -d"$yy_mm_dd" +"%G-%V")"
	local weekfile="$carnet_dir/${week}.txt"
	if [ "$only_location" = "only_location" ]
	then
		local extra_found_instructions="print FILENAME\":\"NR ;exit 0;"
	else
		local extra_found_instructions="print \"$SEPARATOR\";"
		# Kick in week header detection in filter_pattern.
		echo
		echo
	fi
	gawk '
		BEGIN {
			inday=0;
			found=0;
		}

		/^Date ?: [^ ]* \('"$yy_mm_dd"'\)$/ {
			inday=1;
			found=1;
			'"$extra_found_instructions"'
		}

		(inday) {
			if (/^'"$SEPARATOR"'$/) exit;
			print;
		}

		END {
			if (!found) {
				print "error: no entry on '"$yy_mm_dd"'." \
				> "/dev/stderr";
				exit 1;
			}
		}
	' "$weekfile"
}

# locate_week takes a date in format YYYY-WW and outputs the
# corresponding week of the carnet if one is found, otherwise it outputs
# nothing and returns an error code.  It takes an optional second
# argument "only_location" to instruct the function to only return the
# file name of the entry, useful for passing as argument of a text
# editor.
locate_week () {
	local week="$1"
	local only_location="$2"
	local weekfile="$carnet_dir/${week}.txt"

	if [ "$only_location" = "only_location" ]
	then
		echo "${weekfile}:1"
	else
		printf '\n\n%s\n%s\n' "$SEPARATOR" "$SEPARATOR"
		cat "$weekfile"
	fi
}

# print_feuillets takes various types of timestamps in argument and
# outputs the corresponding feuillets, with contextual information about
# a given day or week in output.
print_feuillets () {
	local quick_feuillet=false
	if [ "$1" = "-q" ] || [ "$1" = "--quick" ]
	then
		shift
		local quick_feuillet=true
	fi
	local first_feuillet=true
	for timestamp in "$@"
	do
		if match_timestamp "$timestamp"
		then locate_feuillet "$timestamp"
		elif match_day "$timestamp"
		then locate_day "$timestamp"
		elif match_week "$timestamp"
		then locate_week "$timestamp"
		elif match_clock "$timestamp"
		then locate_feuillet "$(date +%F)T$timestamp"
		elif match_year "$timestamp"
		then locate_year "$timestamp"
		elif match_quarter "$timestamp"
		then locate_quarter "$timestamp"
		elif match_search "$timestamp"
		then break
		# Try something with date's special specs.
		elif date -d "$timestamp" > /dev/null 2>&1
		then
			case "$timestamp" in
			*year* )
				locate_year "$(date -d "$timestamp" +%Y)"
			;;
			*month* )
				locate_quarter "$(date -d "$timestamp" +%Y-Q%q)"
				# FIXME: should this be a locate_month?
			;;
			*week* )
				locate_week "$(date -d "$timestamp" +%G-%V)"
			;;
			* )
				locate_day "$(date -d "$timestamp" +%F)"
			;;
			esac
		else
			cat <<-END >&2
			error: $timestamp is a malformed entry reference.
			       Please use one of:
			         * YYYY-MM-DDThh:mm to choose a feuillet,
			         * YYYY-MM-DD to choose a date,
			         * YYYY-WW to choose a week,
			         * YYYY-QN to choose a quarter,
			         * YYYY to choose a year,
			         * hh:mm to choose a feuillet from today,
			         * date(1) specifications might work too.
			END
			exit 1
		fi
		# There are no easy ways to distinguish YYYY-WW from
		# YYYY-MM.  Besides, it would be desirable but expensive
		# to map days to the given week.  Finally, the amount of
		# back references may be hardly manageable for a tightly
		# knit graph of notes.  Therefore, skip listing back
		# reference to given weeks.  Also skip years as there is
		# no interest to gather all references to a given year.
		if ! "$quick_feuillet" \
		   && ! match_week "$timestamp" \
		   && ! match_quarter "$timestamp" \
		   && ! match_year "$timestamp"
		then
			cat <<-END
			$SEPARATOR
			Back references:
			END
			if match_clock "$timestamp"
			then show_back_references "$(date +%F)T$timestamp"
			else show_back_references "$timestamp"
			fi \
			| column -c 72 \
			| indent_output
		fi
		local first_feuillet=false
	done
}


# Level 3 functions: main logic exposed to end users
####################################################

# handle_list_todo_statuses shows the list of possible statuses for todo
# items, that are neither done or undefined.  This is useful to
# determine which categories of todo items are available throughout the
# carnet.
handle_list_todo_statuses () {
	load_carnet_conf
	if [ -t 1 ]
	then
		list_todo_statuses \
		| column
	else
		list_todo_statuses
	fi
}

# handle_search takes a regex to search for entries and outputs all
# feuillets having expressions matching the argument.
handle_search () {
	# When no pattern is specified, match all entries without
	# excessive cluttering of the output with highlight escape
	# sequences.
	load_carnet_conf
	set_less_options
	search_log "${@:-^}" \
	| paint_timestamps \
	| read_with_pager
}

# handle_todo_items takes no argument and outputs all entries having
# leftover items identified as still to be done.  The convention for an
# item to do is to materialise it as an opening bracket, one or more
# spaces and a closing bracket; the idea is to resolve the item once
# done by filling it with  mark is deemed the most appropriate by the
# writer.  If an argument is passed anyways, then it is used to
# materialize the state of an item to do that has been done, or
# cancelled, or is pending, or whichever state can be imagined for a
# task.
handle_todo_items () {
	local state="$1"
	load_carnet_conf
	set_less_options
	# Warning: reversal logic is reverted for todo items ergonomics,
	# unless CARNET_CHRONOLOGICAL_TODO is true.  From an ergonomics
	# perspective, it is good, as it helps focusing on what is
	# important now.  From a user friendliness perspective, it can
	# be disconcerting, therefore there is a way to defuse the
	# situation with a configuration entry.
	if "$CARNET_CHRONOLOGICAL_TODO"
	then
		if "$REVERSE_WEEK_ORDER"
		then REVERSE_WEEK_ORDER="true"
		else REVERSE_WEEK_ORDER="false"
		fi
	else
		if "$REVERSE_WEEK_ORDER"
		then REVERSE_WEEK_ORDER="false"
		else REVERSE_WEEK_ORDER="true"
		fi
	fi
	if test -z "$state"
	then search_log '\n\[  *\]|\nTODO[[:space:]]?: |\nFIXME[[:space:]]?: '
	else search_log '\n\[ *'"$state"' *\]'
	fi \
	| paint_timestamps \
	| read_with_pager
}

# handle_done_items is mostly equivalent to handle_todo_items but
# facilitates fetching resolved items without further ado.
handle_done_items () {
	local state="$1"
	load_carnet_conf
	set_less_options
	if test -z "$state"
	then search_log '\n\[ *([*]|done|x|✓) *\]|\nDONE[[:space:]]?: '
	else search_log '\n\[ *'"$state"' *\]'
	fi \
	| paint_timestamps \
	| read_with_pager
}

# handle_activity is a function taking no arguments.  It is designed to
# describe accurately the behavior of the carnet option --activity.
handle_activity () {
	load_carnet_conf
	local bindir="$(command -v carnet ||:)"
	local bindir="${bindir%/*}"
	if [ -x ./carnet_activity.pl ]
	then
		# We're in development directory context.
		exec ./carnet_activity.pl "$carnet_dir"
	elif type carnet_activity.pl > /dev/null 2>&1
	then
		# We're in "pre 1.13.0" deployment context.
		carnet_activity.pl "$carnet_dir"
		cat <<-END >&2
		warning: carnet_activity.pl was caught in the PATH, but
		         it moved to the \$PREFIX/share directory.
		         Please remove $(command -v carnet_activity.pl).
		END
		exit 0
	elif [ -n "$bindir" ]
	then
		# We're in "production" deployment context.
		exec "$bindir/../share/$PROGNAME/carnet_activity.pl" "$carnet_dir"
	else
		cat <<-END >&2
		error: missing carnet_activity.pl to process statistics.
		       This file is supposed to be part of the source
		       code and the installation.
		END
		exit 1
	fi
}

# handle_list_modules is a function taking no arguments.  It is designed
# to describe the behavior of the carnet option --list-modules.
handle_list_modules () {
	load_carnet_conf
	if [ -t 1 ]
	then
		list_modules \
		| column
	else
		list_modules
	fi
}

# handle_edit_feuillet is a function taking a string in format
# YYYY-MM-DDThh:mm in order to open the carnet at the corresponding
# feuillet.  It is designed to describe accurately the behavior of the
# carnet command option --edit-feuillet.
handle_edit_feuillet () {
	local timestamp="$1"
	local no_warn=true
	load_carnet_conf
	if [ ! -r "$carnet_dir/register.txt" ] && [ -z "$timestamp" ]
	then local no_warn=false
	fi
	if [ -z "$timestamp" ]
	then
		setup_locks "$filename"
		"$VISUAL" "$carnet_dir/register.txt"
		"$no_warn" || cat <<-END>&2
		warning: there were no time stamp provided to --feuillet,
		         and no entries of interest were recorded in:
		         $carnet_dir/register.txt
		         so carnet started a blank register.
		END
		exit
	else
		local entrypoint=
		if match_timestamp "$timestamp"
		then local entrypoint="$(locate_feuillet "$timestamp" only_location)"
		elif match_day "$timestamp"
		then local entrypoint="$(locate_day "$timestamp" only_location)"
		elif match_week "$timestamp"
		then local entrypoint="$(locate_week "$timestamp" only_location)"
		elif match_clock "$timestamp"
		then
			local entrypoint="$( \
				locate_feuillet \
					"$(date +%F)T$timestamp" \
					only_location
			)"
		else
			cat <<-END >&2
			error: $timestamp is a malformed entry reference.
			       Please use one of:
			         * YYYY-MM-DDThh:mm to choose a feuillet,
			         * YYYY-MM-DD to choose a date,
			         * YYYY-WW to choose a week,
			         * hh:mm to choose a feuillet from today.
			END
			exit 1
		fi
		local filename="${entrypoint%:*}"
		local lineno="${entrypoint##*:}"
		setup_locks "$filename"
		"$VISUAL" \
			"$(lineno_argument "$VISUAL" "$lineno")" \
			"$filename"
		exit
	fi
}

# handle_feuillet is a function taking a string in format
# YYYY-MM-DDThh:mm in order to print out the corresponding feuillet.  It
# is designed to describe accurately the behavior of the carnet command
# option --feuillet.
handle_feuillet () {
	load_carnet_conf
	set_less_options
	# No option were passed.
	if [ -z "$*" ] && [ -r "$carnet_dir/register.txt" ]
	then
		cat "$carnet_dir/register.txt" \
		| paint_timestamps \
		| read_with_pager
	elif [ -z "$*" ] && [ ! -r "$carnet_dir/register.txt" ]
	then
		cat <<-END>&2
		error: there were no time stamp provided to --feuillet,
		       and no entries of interest are recorded in:
		       $carnet_dir/register.txt
		END
		return 1
	fi

	# Isolate feuillets from search arguments.
	local matched_feuillet=false
	for argument in "$@"
	do
		shift
		if match_timestamp "$argument"
		then local matched_feuillet=true
		fi
		if match_search "$argument"
		then break
		else local feuillets="$feuillets $argument"
		fi
	done

	# As long as filtering does not play well with single selections
	# of feuillets, do not attempt to carry filtered searches.
	if "$matched_feuillet" || [ -z "$*" ]
	then
		{
		if [ -n "$*" ]
		then
		cat <<-END
		WARNING: the search pattern is ignored because single feuillets were
		selected.  This is on purpose to avoid mixing search pattern and back
		referencing, both computationally expensive.
		END
		fi
		print_feuillets $feuillets
		} \
		| paint_timestamps \
		| read_with_pager
		return
	fi

	local search_pattern="$(create_search_pattern "$@")"
	local highlight_pattern="$(create_highlight_pattern "$@")"
	print_feuillets --quick $feuillets \
	| filter_pattern "$search_pattern" \
	| paint_highlight_pattern "$highlight_pattern" \
	| paint_timestamps \
	| read_with_pager
}

# handle_quick_feuillet is a function taking a string in format
# YYYY-MM-DDThh:mm in order to print out the corresponding feuillet
# without the bells and whistles of the original handle_feuillet.
# It is designed to describe accurately the behavior of the carnet
# command option --quick-feuillet.  This variant is useful for blinking
# fast retrieval of given feuillets without extra processing involved by
# fetching back references, filtering, painting.
handle_quick_feuillet () {
	load_carnet_conf
	set_less_options
	# No option were passed.
	if [ -z "$*" ] && [ -r "$carnet_dir/register.txt" ]
	then
		cat "$carnet_dir/register.txt"
	elif [ -z "$*" ] && [ ! -r "$carnet_dir/register.txt" ]
	then
		cat <<-END>&2
		error: there were no time stamp provided to --quick-feuillet,
		       and no entries of interest are recorded in:
		       $carnet_dir/register.txt
		END
		return 1
	fi

	print_feuillets --quick "$@"
}

# handle_list_dates implements full dates listing.
handle_list_dates () {
	load_carnet_conf
	set_less_options
	if "$REVERSE_WEEK_ORDER"
	then local reverse="--reverse"
	fi
	if [ -t 1 ]
	then
		list_dates $reverse \
		| column \
		| paint_timestamps \
		| read_with_pager
	else
		list_dates $reverse
	fi
}

# handle_list_entries implements weekly files listing.  This is not very
# useful apart for debug purpose or reuse in modules.  This option
# admits an optional -r or --reverse argument in order to list files
# from newer to older week.
handle_list_entries () {
	local reverse="$1"
	load_carnet_conf
	# Sanitize the reverse option
	case "$reverse" in
	-R | -r | --reverse )
		local reverse="--reverse"
	;;
	* )
		local reverse=""
	;;
	esac
	# Also account for the standalone --reverse option.
	if "$REVERSE_WEEK_ORDER"
	then local reverse="--reverse"
	fi
	list_entries "$reverse"
}

# handle_list_feuillets implements full feuillets listing.
handle_list_feuillets () {
	load_carnet_conf
	set_less_options
	if "$REVERSE_WEEK_ORDER"
	then local reverse="--reverse"
	fi
	if [ -t 1 ]
	then
		list_feuillets $reverse \
		| column \
		| paint_timestamps \
		| read_with_pager
	else
		list_feuillets $reverse
	fi
}

# handle_motd shows the motd.txt on screen.
handle_motd () {
	load_carnet_conf
	print_motd \
	| paint_timestamps
}

# handle_edit_motd opens the motd.txt file in the text editor.
handle_edit_motd () {
	load_carnet_conf
	"$VISUAL" "$carnet_dir/motd.txt"
}

# handle_short_help assembles the help message when carnet is invoked
# with the shorthand -h argument.  The idea is to have a help message
# short enough that it fits within any reasonably sized terminal window,
# i.e. 72 columns by 24 lines.
handle_short_help () {
	print_usage
	cat << END
For extended description, please read the carnet(1) manual page, or run:

	\$ carnet --help
END
}

# handle_help assembles the help message when carnet is invoked with the
# long --help argument.  The idea is to provide extended help when
# manual pages are not an option; the function is also used for
# bootstrapping the manual page with help2man.
handle_help () {
	if [ -t 1 ]
	then
		load_carnet_conf
		set_less_options
		{
		print_description
		print_usage
		print_help
		} | read_with_pager
	else
		print_description
		print_usage
		print_help
	fi
}

# handle_carnet is the main function of the carnet program.  It will
# open the configured editor at the right point in time to append a new
# entry to the carnet, eventually creating a new week when the previous
# entry has been written before Monday on midnight.
handle_carnet () {
	load_carnet_conf

	# date is recorded only once, in order to avoid inconsistencies,
	# if for example the script starts up slightly before midnight
	# and finishes slightly after : state is preserved only at the
	# beginning of the script and in a single pass, and slices the
	# character string only there after as needed.
	local date="$( date +'%F-%T%z;%G-%V;%A' )"

	# But there is the special case of obtaining the past and next
	# weeks, which date does not allow acquiring in a single pass.
	local last_week="$( date -d'last week' '+%G-%V' )"
	local next_week="$( date -d'next week' '+%G-%V' )"

	# Slicing of the "now" time stamp starts here.
	local year="$(      echo "$date" | cut -f1 -d-   )"
	local month="$(     echo "$date" | cut -f2 -d-   )"
	local day="$(       echo "$date" | cut -f3 -d-   )"
	local time="$(      echo "$date" | cut -f4 -d- | cut -f1-2 -d: )"
	local weekyear="$(  echo "$date" | cut -f2 -d';' | cut -f1 -d- )"
	local week="$(      echo "$date" | cut -f2 -d';' | cut -f2 -d- )"
	local dayofweek="$( echo "$date" | cut -f3 -d';' )"

	# There's no simple way to return the last Monday, including
	# when such Monday is today, by using date, hence the slightly
	# complicate logic here.  Due to usage of at least month and
	# day, this logic must be applied after the slicing.  The
	# local_monday is guaranteed to be the Monday localised in the
	# language of the carnet, no matter the point in time in which
	# date is executed.
	local local_monday="$(date -d'Monday' +%A)"
	local latest_sunday="$( \
		if [ "$dayofweek" = "$local_monday" ]
		then echo "$month$day"
		else date -d'last Monday' +'%m%d'
		fi
	)"

	# entry is the file of the week in which to record the new
	# carnet entry.
	local entry="${carnet_dir}/${weekyear}-${week}.txt"
	local last_entry="${last_week}.txt"
	local next_entry="${next_week}.txt"
	local datestamp="Date : $dayofweek (${year}-${month}-${day})"

	local first_entry="$( \
		ls -1 "${carnet_dir}" \
		| grep '[0-9][0-9][0-9][0-9]-[0-5][0-9]\.txt' \
		| sed -n 1p
	)"
	local first_entry="${first_entry:-${weekyear}-${week}.txt}"

	# Calculate the overtime alert if applicable; this depends on
	# the setting of environment variable CARNET_OVERTIME.
	local overtime_alert="$(
		calculate_overtime "${year}-${month}-${day}" "$time" "$entry"
	)"

	# Prepare the trailing context appearing after the time stamp,
	# if anything relevant is of interest to see appear.  This shows
	# the music airing, but will prioritize overtime alerts if
	# applicable.
	local trailing_context="$( \
		if [ "$USER" = "root" ] && [ -n "$SUDO_USER" ]
		then echo " from $SUDO_USER"
		elif test -n "$overtime_alert"
		then echo "$overtime_alert"
		elif "$CARNET_SHOW_MUSIC_ON_AIR"
		then show_music_on_air
		fi
	)"

	setup_locks "$entry"

	if [ -e "$entry" ]
	then
		# This is the most common situation, when adding a new
		# entry in an existing carnet file.
		if grep -q "^${datestamp}\$" "$entry"
		then
			# The new entry can be added to the ongoing day.
			cat >> "$entry" <<-END

			${time} :${trailing_context}

			END
		else
			# Or it can be the first one of the day.
			cat >> "$entry" <<-END

			${SEPARATOR}
			${datestamp}

			${time} :${trailing_context}

			END
		fi
	else
		# Otherwise we are in the situation where a new carnet
		# file needs to be issued, due to previous week's end.
		cat > "$entry" <<-END
		${last_entry} - previous week
		${next_entry} - next week
		${first_entry} - first week

		carnet logbook, year ${weekyear}, week ${week}

		END

		# When available, calendar(1) enhances the carnet by
		# summarizing the activity of the upcoming week.
		if type calendar > /dev/null 2>&1
		then
			cat >> "$entry" <<-END
			On schedule:
			$(LANG=C.UTF-8 calendar -A6 -t "$latest_sunday")

			END
		fi

		cat >> "$entry" <<-END
		${SEPARATOR}
		${datestamp}

		${time} :${trailing_context}

		END
	fi

	# Now editing the current entry by starting at the last line of
	# the file straight away.
	local lineno="$(wc -l "$entry"  | cut -f 1 -d ' ')"
	"$VISUAL" "$(lineno_argument "$VISUAL" "$lineno")" "$entry"

	# Trailing information after file edition.
	{
	print_motd
	printf 'feuillet %s recorded in %s.\n' "$year-$month-${day}T$time" "$entry"
	} \
	| paint_timestamps
}


# Level 4: arguments handling and main functionality
####################################################

# State variables
REVERSE_WEEK_ORDER="false"

while [ -n "$1" ]
do
	case "$1" in
	-a|--activity)
		shift
		handle_activity
		exit
	;;
	-d|--debug)
		shift
		set -x
	;;
	-ef|--edit-feuillet)
		shift
		handle_edit_feuillet "$1"
		exit
	;;
	-f|--feuillet)
		shift
		handle_feuillet "$@"
		exit
	;;
	-h)
		shift
		handle_short_help
		exit 0
	;;
	--help)
		shift
		handle_help
		exit 0
	;;
	-ld|--list-date|--list-dates)
		shift
		handle_list_dates
		exit 0
	;;
	-le|--list-entry|--list-entries)
		shift
		handle_list_entries "$@"
		exit 0
	;;
	-lf|--list-feuillet|--list-feuillets)
		shift
		handle_list_feuillets
		exit 0
	;;
	-lm|--list-module|--list-modules)
		shift
		handle_list_modules
		exit 0
	;;
	-lts|--list-todo-status|--list-todo-statuses)
		shift
		handle_list_todo_statuses
		exit 0
	;;
	-m|--module)
		shift
		run_module "$@"
		exit
	;;
	-qf|--quick-feuillet)
		shift
		handle_quick_feuillet "$@"
		exit
	;;
	-R|--reverse)
		shift
		REVERSE_WEEK_ORDER="true"
	;;
	-r|--readonly)
		shift
		if [ -z "$*" ]
		then handle_feuillet "$(date +"%G-%V")" --search ^
		else handle_feuillet "$(date +"%G-%V")" --search "$@"
		fi
		exit
	;;
	-s|--search)
		shift
		handle_search "$@"
		exit
	;;
	-Rs)
		shift
		REVERSE_WEEK_ORDER="true"
		handle_search "$@"
		exit
	;;
	-t|--todo)
		shift
		# Warning: reversal logic is reverted for todo items
		# ergonomics, unless CARNET_CHRONOLOGICAL_TODO is true.
		handle_todo_items "$1"
		exit
	;;
	-Rt)
		shift
		# Warning: reversal logic is reverted for todo items
		# ergonomics, unless CARNET_CHRONOLOGICAL_TODO is true.
		REVERSE_WEEK_ORDER="true"
		handle_todo_items "$1"
		exit
	;;
	--done)
		shift
		handle_done_items "$1"
		exit
	;;
	--motd)
		shift
		handle_motd
		exit
	;;
	--edit-motd)
		shift
		handle_edit_motd
		exit
	;;
	-v)
		shift
		print_version
		exit 0
	;;
	--version)
		shift
		print_version
		print_copyright_information
		exit 0
	;;
	*)
		cat <<-END >&2
		error: $1: unknown argument.
		       Pass --help for information how to use $PROGNAME.
		END
		exit 1
	;;
	esac
done

handle_carnet
