#!/usr/bin/env bash
#
# Author: Georg Voell - georg.voell@standby.cloud
# Version: @(#)convert-json 3.2.1 14.10.2024 (c)2024 Standby.cloud
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
#
# This script can be used free of charge. Use it as is or customize as needed. It is not guaranteed to be
# error free and you will not be reimbursed for any damage it may cause.
#
#@  Reads a JSON file and converts it into a human readable format.
#@
#@Usage: convert-json [options] [keys]
#@  Options:
#@    -h, --help           : Displays helptext.
#@    -v, --version        : Displays the version of the script.
#@    -n, --noheader       : Don't write a header (needed for concatenating files) in output "tsv" or "etsv".
#@    -q, --quiet          : Just write error message to LOGFILE.
#@    -r, --raw            : Don't convert JSON e.g. hyphens: lifecycle-state to lifecycleState
#@    -o, --output <string>: Output format: <string> can be "line" (default), "json", "etsv", "tsv" or "table".
#@    -i, --import <string>: JSON file to read from - if not specified, read from stdin.
#@    -s, --select <number>: Select only the nth JSON item (starting with 1 for the first record).
#@    -l, --level <number> : Depth for analyzing JSON file (0 is default).
#@  Keys: Optional - Select the keys you want to convert. Use a colon (or tab) separated string for keys.
#@          If empty - display all keys in JSON.
#
# Exit codes:
#  01: Unknown or wrong parameter.
#  02: **jq** not found. This script needs **jq** to perform.
#  03: JSON error.
#  04: Key in JSON contains dash ("-") or blank. **jq** does not work here. - this is now obsolete
#  05: JSON does not start with "{".
#  06: JSON contains less then 2 lines.
#  07: **jq** join error.
#  99: User interrupt.
#
# See also:
#  **print-table**(1), **install-scripts**(1)
#
# Update history:
#
# V 1.0.0 17.10.2017 New version
# V 2.0.0 22.06.2019 Some fixes
# V 3.0.0 02.06.2020 Redesign (was an include)
# V 3.0.1 11.06.2020 Using library
# V 3.0.2 11.01.2021 Using script "norm-jon" and removed option "strict"
# V 3.0.3 27.03.2021 New option --filename (V 3.2.0: Renamed to --import)
# V 3.0.4 07.04.2021 Also accept tab separated fields string
# V 3.1.0 05.06.2023 New copyright
# V 3.1.1 07.09.2023 Check if sed is capable of writing Camel Case
# V 3.1.2 28.07.2024 Trapping ctrl-c
# V 3.1.3 09.08.2024 Also use print-table with output format "json" (instead of just cat)
# V 3.2.0 12.08.2024 New minor version
# V 3.2.1 14.10.2024 Converted to bash
#

# Find executable bash library and source it
lib=`which lib.bash 2>/dev/null | sed 's|^no 'lib.bash' in .*||'`
if [ "$lib" != "" ]; then
	source "$lib"
else
	progdir=`dirname "$0"`
	if [ -r "${progdir}/lib.bash" ]; then
		source "${progdir}/lib.bash"
	else
		echo "Unexpected error: Unable to locate bash library 'lib.bash'."
		exit 1
	fi
fi

# Defined maximal depth to analyze JSON structure
readonly maxlevel=3

# Do extra cleanup
function ExtraCleanup() {
	local i=0
	
	filecheck -rm ${scratchfile}.object
	filecheck -rm ${scratchfile}.etsv
	
	while [ $i -le $maxlevel ]; do
		filecheck -rm ${scratchfile}.keys.$i
		let i++
	done
}

function DisplayElements() {
	local displayheader=${1}
	local key=${2}
	local i=0
	local stat=0
	local elements=0
	local curlevel=0
	local hasChilds=false
	local keysfile="${scratchfile}.keys"
	local result=""
	local jqkey=""
	local elkey=""
	local eltype=""
	local elvalue=""
	local orgkey=${2}

	if [ -r ${scratchfile}.object ]; then
		if [ "$key" = "" ]; then
			jqkey="."
			curlevel=0
		else
			key=`echo "$key" | tr -s '/' | sed 's|^/||' | sed 's|/$||'`
			jqkey=`echo "/${key}/" | sed 's|\(\[[0-9]*\]\)*/|\"\1\.\"|g'`
			jqkey=`echo "$jqkey" | sed 's|^"||' | sed 's|\."$||' | sed 's|""||'`  ### | sed 's|\\\|\\\\\\\\|g'`
			curlevel=`echo "/$key" | tr '[' '/' | tr -cd '/' | wc -c`
		fi
		
		keysfile="${keysfile}.$curlevel"
		if [ "$DEBUG_CJ" = true ]; then
			echo "Start: curlevel: '$curlevel' - orgkey: '$orgkey' - key: '$key' - jqkey: '$jqkey'" >> /tmp/keysfile
		fi
		
		elements=`"$jq" -r ${jqkey}' | length' ${scratchfile}.object`
		stat=$?
		if [ $stat -gt 0 ]; then
			ShowVariable "(after length) key" "${key}"
			ShowVariable "(after length) jqkey" "${jqkey}"
		fi
		
		"$jq" ${jqkey}' | keys_unsorted, map(type), map(.)' ${scratchfile}.object > $keysfile
		stat=$?
		if [ $stat -gt 0 ]; then
			ShowVariable "(after map) key" "${key}"
			ShowVariable "(after map) jqkey" "${jqkey}"
		fi
		
		if [ "$DEBUG_CJ" = true ]; then
			cat $keysfile >> /tmp/keysfile
		fi
		
		if [ -r "$keysfile" ]; then
			i=0
			while [ $i -lt $elements ]; do
				result=`"$jq" -r 'nth('$i')' "$keysfile" | tr '\n' '\t'`
				elkey=`echo "$result" | cut -d$'\t' -f1`
				eltype=`echo "$result" | cut -d$'\t' -f2`
				elvalue=`echo "$result" | cut -d$'\t' -f3`
				
				hasChilds=false
				case "$eltype" in
					null)
						elvalue="/null/"
						;;
					object)
						if [ "$elvalue" != "{}" ]; then
							hasChilds=true
						fi
						elvalue="/{}/"
						;;
					array)
						if [ "$elvalue" != "[]" ]; then
							hasChilds=true
						fi
						elvalue="/[]/"
						;;
					number | boolean)
						elvalue="/$elvalue/"
						;;
				esac
				
#				# Handle empty values
#				if [ "$elvalue" = "" ]; then
#					elvalue="/$elvalue/"
#				fi

				result=`echo "$elkey" | grep '^[0123456789]*$'`
				if [ "$result" != "" ]; then
					elkey=`echo "[${elkey}]"`
				fi
				
				if [ "$key" = "" ]; then
					result="$elkey"
				else
					if [ "$result" != "" ]; then
						result=`echo "${key}$elkey"`
					else
						result=`echo "${key}/$elkey"`
					fi
				fi
				
				if [ $level -gt $curlevel -a "$hasChilds" = true ]; then
					if [ "$DEBUG_CJ" = true ]; then
						echo "Vor Rekursion: curlevel: '$curlevel' - eltype: '$eltype' - elkey: '$elkey' - result: '$result'" >> /tmp/keysfile
					fi
					
					DisplayElements "$displayheader" "$result" >> ${scratchfile}.etsv
				else
					if [ "$displayheader" = true ]; then
						if [ "$firstelement" = true ]; then
							firstelement=false
							printf "%s" "$result" >> ${scratchfile}.etsv
						else
							printf "\t%s" "$result" >> ${scratchfile}.etsv
						fi
					else
						if [ "$firstelement" = true ]; then
							firstelement=false
							printf "%s" "$elvalue" >> ${scratchfile}.etsv
						else
							printf "\t%s" "$elvalue" >> ${scratchfile}.etsv
						fi
					fi
				fi
				
				let i++
			done
		fi
	fi
}

function CreateETSVFile() {
	local j=0			# iterate over objects in json file
	local objects=0		# objects in json file
	local result=""
	local firstline=true
	local firstelement=true

	result=`filecheck -sl $scratchfile`
	if [ "$result" != "" ]; then
		printf "" > ${scratchfile}.etsv
		objects=`"$jq" -r '. | length' $scratchfile`
		while [ $j -lt $objects ]; do
			"$jq" 'nth('$j')' $scratchfile > ${scratchfile}.object
			
			if [ "$firstline" = true ]; then
				# Print header line
				firstline=false
				DisplayElements "true" "" >> ${scratchfile}.etsv
				printf "\n" >> ${scratchfile}.etsv
			fi

			firstelement=true
			DisplayElements "false" "" >> ${scratchfile}.etsv
			printf "\n" >> ${scratchfile}.etsv
			
			let j++
		done
	fi
}

# Preset
noheader=false
qopt=""
ropt=""
nopt=""
number=""
level=""
filename=""
# fieldsstr=""		# List of fields we have to convert from json
formatstr=""		# Output format
param=""

# Loop until all parameters are used up
while [ $# -gt 0 ]; do
	pname=${1}
	case "$pname" in
		-i | --import)
			shift
			if [ "$1" != "" ]; then
				if [ "$filename" = "" ]; then
					filename="$1"
					if [ ! -r "$filename" ]; then
						errstr="Can't read from filename '$filename'."
					fi
				else
					errstr="Option '$pname' used more then once."
				fi
				shift
			else
				errstr="Please specify a filename after parameter '$pname'."
			fi
			;;
		-o | --output)
			shift
			if [ "$1" != "" ]; then
				if [ "$formatstr" = "" ]; then
					formatstr=`echo "$1" | tolower`
					if [ "$formatstr" != "table" -a "$formatstr" != "line" -a "$formatstr" != "json" -a "$formatstr" != "etsv" -a "$formatstr" != "tsv" ]; then
						errstr="Unknown format '$formatstr' after parameter '$pname'. Please choose from 'table', 'line', 'json', 'etsv' or 'tsv'."
					fi
				else
					errstr="Option '$pname' used more then once."
				fi
				shift
			else
				errstr="Please specify a format ('table', 'line', 'json', 'etsv' or 'tsv') after parameter '$pname'."
			fi
			;;
		-s | --select)
			shift
			if [ "$1" != "" ]; then
				if [ "$number" = "" ]; then
					number="$1"
					range=`echo "$number" | grep '^[0123456789]*$'`
					if [ "$range" = "" ]; then
						errstr="Invalid number after option '$pname'."
					else
						nopt="$pname $number"
					fi
				else
					errstr="Option '$pname' used more then once."
				fi
				shift
			else
				errstr="Please specify a number after option '$pname'."
			fi
			;;
		-l | --level)
			shift
			if [ "$1" != "" ]; then
				if [ "$level" = "" ]; then
					level="$1"
					range=`echo "$level" | grep '^[0123456789]*$'`
					if [ "$range" = "" ]; then
						errstr="Invalid number after option '$pname'."
					else
						if [ $level -lt 0 -o $level -gt $maxlevel ]; then
							errstr="Please choose between '0' and '$maxlevel' for option '$pname'."
						fi
					fi
				else
					errstr="Option '$pname' used more then once."
				fi
				shift
			else
				errstr="Please specify a number after option '$pname'."
			fi
			;;
		-r | --raw)
			shift
			ropt="$pname"
			;;
		-q | --quiet)
			shift
			qopt="$pname"
			;;
		-n | --noheader)
			shift
			noheader=true
			;;
		-v | --version)
			shift
			showversion=true
			;;
		-h | --help)
			shift
			showhelp=true
			;;
		*)
			shift
			paramck=`echo "$pname" | grep '^-'` # Keys don't begin with '-'
			if [ "$paramck" != "" ]; then
				errstr="Unknown option '$pname'."
			else
				if [ "$errstr" = "" ]; then
					if [ "$param" = "" ]; then
						param="$pname"
					else
						errstr="Keys were already specified '$param'. Unknown additional parameter: '$pname'."
					fi
				fi
			fi
	esac
done

# Display help or error message
DisplayHelp

# Set output to line if not specified
if [ "$formatstr" = "" ]; then
	formatstr="line"
fi
if [ "$level" = "" ]; then
	level=0
fi

# Check if he have jq in path and right version
result=`check-version jq --min 1.5`
jq=`echo "$result" | cut -d' ' -f1`
jqvers=`echo "$result" | cut -d' ' -f2`
jqversok=`echo "$result" | cut -d' ' -f3`

if [ "$jqversok" != "ok" ]; then
	# At least jq version 1.5 is needed for 'first(inputs)', 'keys_unsorted' and 'nth(0)'
	exitcode=2
	errormsg $qopt $exitcode "($progstr) No 'jq' in '$PATH' or version is less than '1.5'."
	ClearSTDIN
	exit $exitcode
else
	if [ "$filename" != "" ]; then
		norm-json --import "$filename" $nopt $qopt $ropt > $scratchfile 2>&1
		stat=$?
	else
		if [ ! -t 0 ]; then
			cat /dev/stdin | norm-json $nopt $qopt $ropt > $scratchfile 2>&1
			stat=$?
		else
			exitcode=3
			errormsg $qopt $exitcode "($progstr) No filename specified and stdin seems to be empty."
			exit $exitcode
		fi
	fi

	if [ $stat -gt 0 ]; then
		exitcode=3
		if [ "$qopt" = "" ]; then
			errrsn=`head -n 2 $scratchfile`
			printf "${errrsn}\n"	### Don't change this line
		fi
		
		Cleanup
		exit $exitcode
	else
		if [ "$nopt" != "" ]; then
			mv -f $scratchfile ${scratchfile}.object
			printf "[\n" > $scratchfile
			cat ${scratchfile}.object >> $scratchfile
			printf "]\n" >> $scratchfile
			rm -f ${scratchfile}.object
		fi
		
		if [ "$DEBUG_CJ" = true ]; then
			echo "" > /tmp/keysfile
		fi

		# Create enhanced tsv file
		CreateETSVFile
		
		if [ "$DEBUG_CJ" = true ]; then
			cat ${scratchfile}.etsv > /tmp/result
		fi

		if [ "$ENVELOPE_TABLE" != false -a "$formatstr" = "json" ]; then
			print-table --import ${scratchfile}.etsv --output $formatstr "$param" | sed 's|\("contentItems":.*\)|\1,\n  "creator": "'$progstr'"|'
		else
			if [ "$noheader" = true -a "$formatstr" = "etsv" -o "$noheader" = true -a "$formatstr" = "tsv" ]; then
				print-table --import ${scratchfile}.etsv --output $formatstr "$param" | tailfromline2
			else
				print-table --import ${scratchfile}.etsv --output $formatstr "$param"
			fi
		fi
	fi
fi

# Cleanup and exit
Cleanup
exit $exitcode