#!/bin/sh

version="0.26.0"

usage() {
	echo "ereandel v$version: Browse the gemini web on the terminal."
	echo ""
	echo "Usage: ereandel [URL]|[OPTION]"
	echo ""
	echo "Options:"
	echo "  -h, --help		show this help"
	echo "  -v, --version		show version info"
	echo ""
	echo "Configuration:"
	echo "You can setup a config file at ~/.config/ereandel/ereandel.conf to configure *ereandel* the way you like."
	echo ""
	echo "Commands:"
	echo "These are the default keybindings to use while running ereandel:"
	echo ""
	echo "  q	quit"
	echo "  g	go to a link"
	echo "  r	reload current page"
	echo "  b	go back one page"
	echo "  u	jump one path segment up"
	echo "  o	open an address"
	echo "  s	save current page"
	echo "  H	go to homepage"
	echo "  m	add bookmark"
	echo "  M	go to a bookmark"
	echo "  K	remove bookmark for current url"
	echo ""
	echo "Examples:"
	echo "  ereandel		Start browsing the default webpage"
	echo "  ereandel url	Start browsing url"
	echo "  ereandel --help	Show help"
	echo ""
	echo "Report bugs to: bleemayer@gmail.com"
	echo "Home page: <https://www.github.com/blmayer/ereandel/>"
	echo "General help: <https://www.github.com/blmayer/ereandel/wiki>"
}

version() {
	echo "ereandel $version"
	echo ""
	echo "Copyright (C) 2021-2023 Brian Mayer."
	echo "License MIT: MIT License <https://opensource.org/licenses/MIT>"
	echo "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,"
	echo "EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF"
	echo "MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT."
	echo ""
	echo "Written by Brian Lee Mayer."
}

debug() {
	[ "$debug" ] && echo "DEBUG: $*" >&2 && sleep "$debug"
}

# Parse arguments
debug "parsing args"
while [ "$1" ]
do
	case $1 in
		"-v")
			debug=1
			;;
		"-h" | "--help")
			usage
			exit 0
			;;
		"--version")
			version
			exit 0
			;;
		"-f"|"--file")
			shift
			file="$1"
			;;
		"")
			break
			;;
		*)
			url="$1"
			;;
	esac
	shift
done

# Save terminal
tput smcup

# Configuration
confighome=${XDG_CONFIG_HOME:-$HOME/.config}
mkdir -p "$confighome/ereandel"
configfile="$confighome/ereandel/ereandel.conf"
bookmarkfile="$confighome/ereandel/bookmarks"
certdir="$confighome/ereandel/certs"
mkdir -p "$certdir"

cachedir="${XDG_CACHE_HOME:-$HOME/.cache}/ereandel"

# Default values

# user config
margin=8
homepage="geminiprotocol.net/"

# keybindings
openkey='o'
openlocalkey='O'
gokey='g'
refreshkey='r'
backkey='b'
quitkey='q'
markkey='b'
gomarkkey='M'
delmarkkey='K'
goupkey='u'
homekey='H'

# styles
sty_header1='\033[35;7;1m'
sty_header2='\033[35;4;1m'
sty_header3='\033[35;4m'
sty_quote='\033[2;3m  '
sty_linkb='\033[35m'
sty_linkt=' => \033[36;3m '
sty_listb='\033[35;1m  •'
sty_listt='\033[0m '

if [ ! -s "$configfile" ]
then
	# Default values
	cat <<- EOF > "$configfile"
	# where to store temp files
	cachedir="$cachedir"

	# user config
	margin=8
	homepage="gemini.circumlunar.space/"

	# keybindings
	openkey='o'
	openlocalkey='O'
	gokey='g'
	refreshkey='r'
	backkey='b'
	quitkey='q'
	markkey='b'
	gomarkkey='M'
	delmarkkey='K'
	goupkey='u'
	homekey='H'

	# styles
	sty_header1='\033[35;7;1m'
	sty_header2='\033[35;4;1m'
	sty_header3='\033[35;4m'
	sty_quote='\033[2;3m  '
	sty_linkb='\033[35m'
	sty_linkt=' => \033[36;3m '
	sty_listb='\033[35;1m  •'
	sty_listt='\033[0m '
	EOF
fi

# shellcheck source=/dev/null
. "$configfile"

mkdir -p "$cachedir"
pagefile="$(mktemp -p "$cachedir" -t curpage.XXXXXX)"
histfile="$(mktemp -p "$cachedir" -t history.XXXXXX)"
linksfile="$(mktemp -p "$cachedir" -t links.XXXXXX)"
tracefile="$(mktemp -p "$cachedir" -t trace.XXXXXX)"
debug "read configs"

# Restore terminal
thetrap() {
	rm -f "$histfile" "$linksfile" "$pagefile" "$tracefile" > /dev/null 2>&1
	tput rmcup
	printf "\033[?25h"
	stty echo
	exit
}

trap thetrap EXIT INT HUP

getprevious() {
	sed -i '$d' "$histfile"
	prev="$(tail -n 1 "$histfile")"
	sed -i '$d' "$histfile"
	echo "$prev"
}

# Returns the complete url scheme with gemini defaults
# Parameters: url
parseurl() {
	# Credits: https://stackoverflow.com/a/6174447/7618649
	debug "parsing: $url oldhost: $oldhost oldpath: $oldpath"
	proto="$(echo "$url" | grep :// | sed -e 's,^\(.*://\).*,\1,g')"
	if [ "$proto" ]
	then 
		url="$(echo "$url" | sed -e "s@$proto@@g")"
	else
		if [ "$oldhost" ]
		then
			case "$url" in
				"/"*) url="$oldhost$url" ;;
				*) oldpath="/${oldpath#/*}"; url="$oldhost${oldpath%/*}/$url" ;;
			esac
		fi
	fi
	debug "url: $url"

	proto="$(echo "$proto" | sed -e 's,://,,g')"
	user="$(echo "$url" | grep @ | cut -d@ -f1)"
	hostport="$(echo "$url" | sed -e "s/$user@//g" | cut -d/ -f1)"
	host="$(echo "$hostport" | sed -e 's,:.*,,g')"
	port="$(echo "$hostport" | sed -e 's,^.*:,:,g' -e 's,.*:\([0-9]*\).*,\1,g' -e 's,[^0-9],,g')"
	path="$(echo "$url" | sed "s@^$hostport@@")"

	debug "parsed: proto: ${proto:-gemini} host: $host port: ${port:-1965} path: ${path#/*} rest: $rest"
	echo "${proto:-gemini}" "$host" "${port:-1965}" "${path#/*}" "$rest"
}

typesetgmi() {
	# some setup first
	[ -f "$linksfile" ] && rm "$linksfile"
	cols=$(tput cols)
	linkcount="0"

	# shellcheck disable=SC2154
	width=$((cols - (2*margin)))
	debug "text width: $width"

	while IFS='' read -r line || [ -n "$line" ];
	do
		line="$(printf "%s\n" "$line" | tr -d '\r')"
		printf "%s\n" "$line" | grep -q "^\`\`\`" && pre=$((1 - pre)) && line=""

		# add margins and fold
		if [ "$pre" = 1 ]
		then
			printf '%*s%s\n' "$margin" "" "$line"
			continue
		fi

		# shellcheck disable=SC2154
		case "$line" in
			"### "*) sty="$sty_header3" && line="${line#'### '}" ;;
			"## "*) sty="$sty_header2" && line="${line#'## '}"  ;;
			"# "*) sty="$sty_header1" && line="${line#'# '}" ;;
			"> "*) sty="$sty_quote" && line="${line#> }" ;;
			"=>"*)
				link="$(echo "${line#'=>'}" | tr -s '\t' ' ')"
				echo "${link#' '}" >> "$linksfile"
				linkcount=$((linkcount+1))

				line="$(echo "$link" | cut -d' ' -f2-)"
				[ -z "$line" ] && line="$link"

				sty="$sty_linkb${linkcount}$sty_linkt"
				;;
			'* '*) sty="$sty_listb$sty_listt" && line="${line#* }";;
			*) sty='' ;;
		esac

		printf -- "%s\n" "$line" | fold -w "$width" -s | while IFS='' read -r txt
		do
			printf "%*s" "$margin" ""

			# The '--' here is to make sure the next string is treated as the 
			# format string, even if $sty begins with a '-'.
			# shellcheck disable=SC2059
			printf -- "$sty%s\n\033[m" "$txt"
		done
	done
}

# some help:
# \033[;H	move to top 
# \033[NH 	move to bottom N lines
# \033[?25l	hide cursor
# \033[?25h	unhide cursor
# \033[2K	erase line
pager() {
	clear

	# hide cursor
	printf '\033[?25l'

	# lines columns
	lines="$(tput lines)"
	head -n "$((lines-1))" "$1"
	l="$(wc -l < "$1")"
	if [ "$lines" -lt "$l" ]; then pos="$lines"; else pos="$l"; fi

	# stop echoing user input and read input unbufered
	stty -echo -icanon min 1 time 0

	# read inputs
	while k="$(dd bs=1 count=1 status=none | od -c -An | tr -d ' ')"
	do
		case "$k" in
		# command sequences
		"033")
			b="$(dd bs=2 count=1 status=none)"
			case "$b" in
				# up arrow
				'[A')
					[ "$pos" -le "$lines" ] && continue
					line="$(sed "$((pos-lines))q;d" "$1")"
					pos="$((pos-1))"
					printf '\033[H\033[L%s\033[%sH\033[2K' "$line" "$lines"
					;;
				# down arrow
				'[B')
					[ "$pos" -ge "$l" ] && continue
					printf '\033[%sH' "$lines"
					sed "${pos}q;d" "$1"
					pos="$((pos+1))"
					;;
				# page up
				'[5')
					# discard one extra byte
					dd bs=1 count=1 status=none > /dev/null

					[ "$pos" -le "$lines" ] && continue
					scroll="$((pos-lines))"
					[ "$scroll" -gt "$lines" ] && scroll="$((lines-1))"

					# shellcheck disable=SC2086
					for i in $(seq 1 "$scroll")
					do
						line="$(sed "$((pos-lines))q;d" "$1")"
						pos="$((pos-1))"
						printf '\033[H\033[L%s\033[%sH\033[2K' "$line" "$lines"
					done
					;;
				# page down
				'[6') 
					# discard one extra byte
					dd bs=1 count=1 status=none > /dev/null

					[ "$pos" -ge "$l" ] && continue
					scroll="$((lines-1))"
					end="$((pos+scroll))"
					[ "$end" -ge "$l" ] && scroll="$((l-pos))"

					# shellcheck disable=SC2086
					for i in $(seq 1 "$scroll")
					do
						printf '\033[%sH' "$lines"
						sed "${pos}q;d" "$1"
						pos="$((pos+1))"
					done
					;;
			esac ;;
		"$quitkey") thetrap ;;
		"$openkey")
			printf '\033[%sH\033[?25h\033[2KType url: ' "$lines"
			stty echo icanon
			read -r url

			# add gemini:// to begining if not (for convenience)
			case "$url" in
			       "gemini://"*) ;;
			       *) url="gemini://$url" ;;
			esac
			return
			;;
		"$openlocalkey")
			# Open local gmi file
			printf '\033[?25h\033[2KType file path: '
			stty echo
			read -r file
			typesetgmi < "$file" > "$pagefile"
			pager "$pagefile"
			return
			;;
		"$refreshkey") return ;;
		"$gokey")
			printf '\033[?25h\033[2KEnter link number: '
			stty echo icanon
			read -r i
			debug "selected $i"
			url="$(sed "${i}q;d" "$linksfile" | cut -f1 | cut -d' ' -f1)"
			return
			;;
		"$backkey") 
			read -r proto host port path <<- EOF
			$(getprevious)
			EOF
			url="$proto://$host:$port/$path"
			return
			;;
		"$homekey") url="$homepage"; return ;;
		"$markkey") 
			printf '\033[?25h\033[KEnter description: (optional)'
			stty echo icanon
			read -r desc
			echo "$url $desc" >> "$bookmarkfile"
			return
			;;
		"$gomarkkey")
			clear
			cat -n "$bookmarkfile"
			printf "\033[?25h\033[KEnter link number: "
			stty echo icanon
			read -r i
			url="$(sed "${i}q;d" "$bookmarkfile" | cut -d' ' -f1)"
			return
			;;
		"$delmarkkey")
			grep -iv "^$url " "$bookmarkfile" > "$cachedir/bookmarks"
			mv "$cachedir/bookmarks" "$bookmarkfile"
			return
			;;
		"$goupkey")
			newpath=$(echo "$url" | rev | cut -d'/' -f2- | rev)
			url="${url%/*}/$newpath"
			return
			;;
		esac
	done
}

# borrowed from https://gist.github.com/cdown/1163649
urlencode() {
	old_lang=$LANG
	LANG=C

	old_lc_collate=$LC_COLLATE
	LC_COLLATE=C

	length="${#1}"
	i=1
	while [ "$i" -le "$length" ]
	do
		c=$(printf '%s' "$1" | cut -c $i)
		case $c in
			[a-zA-Z0-9.~_-]) printf '%s' "$c" ;;
			*) printf '%%%02X' "'$c" ;;
		esac
		i=$((i+1))
	done

	LC_COLLATE=$old_lc_collate
	LANG=$old_lang
}

# Fetches the gemini response from server
# Parameters: proto, host, port and path
# Spec draft is here: https://gemini.circumlunar.space/docs/specification.html
fetch() {
	if [ ! "$1" = "gemini" ]
	then
		echo "Only gemini links are supported."
		echo "Type a key to continue."
		read -r i
		read -r proto host port path <<- EOF
			$(getprevious)
		EOF
		url="$proto://$host:$port/$path"
		url="${url:-$homepage}"
		debug "previous page: $url"
		return
	fi

	debug "requesting $1://$2:$3/$4$5"
	
	# set title
	printf '\033]2;%s\007' "ereandel (c): $2/$4"
	
	echo "$1 $2 $3 $4 $5" >> "$histfile"

	certfile=""
	if [ -f "$certdir/$2.crt" ] && [ -f "$certdir/$2.key" ]
	then
		certfile="-cert \"$certdir/$2.crt\" -key \"$certdir/$2.key\""
		debug "using client cert for domain: $certfile"
	fi

	port=$( [ "$3" = "1965" ] || ":$3" )
	url="$1://$2$port/$4$5"
	[ "$trace" ] && echo "url: $url" >> "$tracefile"

	echo "$url" | eval openssl s_client \
		-connect "$2:$3" "$certfile" -crlf -quiet \
		-ign_eof 2> /dev/null > "$pagefile"

	printf '\033]2;%s\007' "ereandel (r): $2/$4"

	# First line is status and meta information
	read -r status meta < "$pagefile"
	status="$(echo "$status" | tr -d '\r\n')"
	meta="$(echo "$meta" | tr -d '\r\n')"
	tail -n +2 "$pagefile" > "$pagefile.tmp" && mv "$pagefile.tmp" "$pagefile"
	debug "response status - meta: $status - $meta"

	# Validate
	case "$status" in
		10)
			echo "Input needed: $meta" >&2
			echo "Please provide the input:" >&2
			read -r input
			url="$1://$2:$3/$4?$(urlencode "$input")"
			return 0
			;;
		11)
			echo "Sensitive input needed: $meta" >&2
			read -r input
			url="$1://$2:$3/$4?$(urlencode "$input")"
			return 0
			;;
		30|31)
			# Redirect
			debug "redirecting to: $meta"

			# shellcheck disable=SC2046
			read -r proto host port path <<- EOF
				$(oldhost="$2" oldpath="$4" url="$meta" parseurl)
			EOF
			url="$proto://$host:$port/$path"
			return 0
			;;
		40)
			echo "Temporary failure" >&2
			echo "Type a key to continue. "
			read -r i
			return 3
			;;
		41)
			return 4
			;;
		42)
			return 5
			;;
		43)
			return 6
			;;
		44)
			return 7
			;;
		50|51)
			echo "Page not found!" >&2
			echo "Type a key to return to previous page."
			read -r i <&1
			read -r proto host port path <<- EOF
				$(getprevious)
			EOF
			url="$proto://$host:$port/$path"
			debug "previous page: $url"
			return 0
			;;
		52)
			return 10
			;;
		53)
			return 11
			;;
		59)
			echo "Bad request: $meta" >&2
			echo "Type a key to continue."
			read -r i <&1
			return 12
			;;
		60)
			printf "client certificate required, to create a client cert use the following command:\n\n"
			printf "\topenssl req -x509 -newkey rsa:4096 -keyout %s/%s.key -out %s/%s.crt -days 36500 -nodes\n\n" "$certdir" "$2" "$certdir" "$2"
			printf "press 'return' to reload the page or 'b' to go back to the previous page:\n"
			read -r in <&1
			if [ "$in" = "b" ]
			then
				read -r proto host port path <<- EOF
					$(getprevious)
				EOF
				url="$proto://$host:$port/$path"
			else
				url="$1://$2:$3/$4?$5"
			fi
			return 0
			;;
		61)
			return 14
			;;
		62)
			return 15
			;;
	esac

	# Success
	oldhost="$2"
	oldpath="$4"

	# Set charset
	charset="$(echo "$meta" | grep -i "charset=" | sed -e 's/.*charset=\([^;]\+\).*/\1/Ig')"
	case "$charset" in
		"iso-8859-1" | "ISO-8859-1") charset="iso8859" ;;
		"utf-8" | "UTF-8" | "") charset="utf8" ;;
		"us-ascii" | "US-ASCII") charset="ascii" ;;
	esac
	debug "charset: $charset"

	printf '\033]2;%s\007' "ereandel (t): $2/$4"
	case $meta in
		"text/gemini"* | "") typesetgmi < "$pagefile" > "$pagefile.gmi"; mv "$pagefile.gmi" "$pagefile" ;;
		*) ;;
	esac 

	debug "starting pager"
	printf '\033]2;%s\007' "ereandel (l): $2/$4"
	pager "$pagefile"

	debug "new url: $url"
}

# files are handled differently
if [ "$file" ] && [ -f "$file" ]
then
	typesetgmi < "$file" > "$pagefile"
	pager "$pagefile"
fi

# first request
url="${url:-$homepage}"
while :
do
	printf '\033]2;%s\007' "ereandel (w)"

	# shellcheck disable=SC2046
	fetch $(parseurl)
done

