#!/bin/sh

# vim: shiftwidth=4

script_name="$(basename "$0")"

short_help="Usage: $script_name [OPTIONS] <image_file>

This is a script to display images in the terminal using the kitty graphics
protocol with Unicode placeholders. It is very basic, please use something else
if you have alternatives.

Options:
  -h                  Show this help.
  -s SCALE            The scale of the image, may be floating point.
  -c N, --cols N      The number of columns.
  -r N, --rows N      The number of rows.
  --max-cols N        The maximum number of columns.
  --max-rows N        The maximum number of rows.
  --cell-size WxH     The cell size in pixels.
  -m METHOD           The uploading method, may be 'file', 'direct' or 'auto'.
  --speed SPEED       The multiplier for the animation speed (float).
"

# Exit the script on keyboard interrupt
trap "echo 'icat-mini was interrupted' >&2; exit 1" INT

cols=""
rows=""
file=""
tty="/dev/tty"
uploading_method="auto"
cell_size=""
scale=1
max_cols=""
max_rows=""
speed=""

# Parse the command line.
while [ $# -gt 0 ]; do
    case "$1" in
        -c|--columns|--cols)
            cols="$2"
            shift 2
            ;;
        -r|--rows|-l|--lines)
            rows="$2"
            shift 2
            ;;
        -s|--scale)
            scale="$2"
            shift 2
            ;;
        -h|--help)
            echo "$short_help"
            exit 0
            ;;
        -m|--upload-method|--uploading-method)
            uploading_method="$2"
            shift 2
            ;;
        --cell-size)
            cell_size="$2"
            shift 2
            ;;
        --max-cols)
            max_cols="$2"
            shift 2
            ;;
        --max-rows)
            max_rows="$2"
            shift 2
            ;;
        --speed)
            speed="$2"
            shift 2
            ;;
        --)
            file="$2"
            shift 2
            ;;
        -*)
            echo "Unknown option: $1" >&2
            exit 1
            ;;
        *)
            if [ -n "$file" ]; then
                echo "Multiple image files are not supported: $file and $1" >&2
                exit 1
            fi
            file="$1"
            shift
            ;;
    esac
done

file="$(realpath "$file")"

#####################################################################
# Adjust the terminal state
#####################################################################

stty_orig="$(stty -g < "$tty")"
stty -echo < "$tty"
# Disable ctrl-z. Pressing ctrl-z during image uploading may cause some
# horrible issues otherwise.
stty susp undef < "$tty"
stty -icanon < "$tty"

restore_echo() {
    [ -n "$stty_orig" ] || return
    stty $stty_orig < "$tty"
}

trap restore_echo EXIT TERM

#####################################################################
# Detect imagemagick
#####################################################################

# If there is the 'magick' command, use it instead of separate 'convert' and
# 'identify' commands.
if command -v magick > /dev/null; then
    identify="magick identify"
    convert="magick"
else
    identify="identify"
    convert="convert"
fi

#####################################################################
# Detect tmux
#####################################################################

# Check if we are inside tmux.
inside_tmux=""
if [ -n "$TMUX" ]; then
    case "$TERM" in
        *tmux*|*screen*)
            inside_tmux=1
            ;;
    esac
fi

#####################################################################
# Compute the number of rows and columns
#####################################################################

is_pos_int() {
    if [ -z "$1" ]; then
        return 1 # false
    fi
    if [ -z "$(printf '%s' "$1" | tr -d '[:digit:]')" ]; then
        if [ "$1" -gt 0 ]; then
            return 0 # true
        fi
    fi
    return 1 # false
}

if [ -n "$cols" ] || [ -n "$rows" ]; then
    if [ -n "$max_cols" ] || [ -n "$max_rows" ]; then
        echo "You can't specify both max-cols/rows and cols/rows" >&2
        exit 1
    fi
fi
# Get the max number of cols and rows.
[ -n "$max_cols" ] || max_cols="$(tput cols)"
[ -n "$max_rows" ] || max_rows="$(tput lines)"
if [ "$max_rows" -gt 255 ]; then
    max_rows=255
fi

python_ioctl_command="import array, fcntl, termios
buf = array.array('H', [0, 0, 0, 0])
fcntl.ioctl(0, termios.TIOCGWINSZ, buf)
print(int(buf[2]/buf[1]), int(buf[3]/buf[0]))"

# Get the cell size in pixels if either cols or rows are not specified.
if [ -z "$cols" ] || [ -z "$rows" ]; then
    cell_width=""
    cell_height=""
    # If the cell size is specified, use it.
    if [ -n "$cell_size" ]; then
        cell_width="${cell_size%x*}"
        cell_height="${cell_size#*x}"
        if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then
            echo "Invalid cell size: $cell_size" >&2
            exit 1
        fi
    fi
    # Otherwise try to use TIOCGWINSZ ioctl via python.
    if [ -z "$cell_width" ] || [ -z "$cell_height" ]; then
        cell_size_ioctl="$(python3 -c "$python_ioctl_command" < "$tty" 2> /dev/null)"
        cell_width="${cell_size_ioctl% *}"
        cell_height="${cell_size_ioctl#* }"
        if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then
            cell_width=""
            cell_height=""
        fi
    fi
    # If it didn't work, try to use csi XTWINOPS.
    if [ -z "$cell_width" ] || [ -z "$cell_height" ]; then
        if [ -n "$inside_tmux" ]; then
            printf '\ePtmux;\e\e[16t\e\\' >> "$tty"
        else
            printf '\e[16t' >> "$tty"
        fi
        # The expected response will look like ^[[6;<height>;<width>t
        term_response=""
        while true; do
            char=$(dd bs=1 count=1 <"$tty" 2>/dev/null)
            if [ "$char" = "t" ]; then
                break
            fi
            term_response="$term_response$char"
        done
        cell_height="$(printf '%s' "$term_response" | cut -d ';' -f 2)"
        cell_width="$(printf '%s' "$term_response" | cut -d ';' -f 3)"
        if ! is_pos_int "$cell_height" || ! is_pos_int "$cell_width"; then
            cell_width=8
            cell_height=16
        fi
    fi
fi

# Compute a formula with bc and round to the nearest integer.
bc_round() {
    LC_NUMERIC=C printf '%.0f' "$(printf '%s\n' "scale=2;($1) + 0.5" | bc)"
}

# Compute the number of rows and columns of the image.
if [ -z "$cols" ] || [ -z "$rows" ]; then
    # Get the size of the image and its resolution. If it's an animation, use
    # the first frame.
    format_output="$($identify -format '%w %h\n' "$file" | head -1)"
    img_width="$(printf '%s' "$format_output" | cut -d ' ' -f 1)"
    img_height="$(printf '%s' "$format_output" | cut -d ' ' -f 2)"
    if ! is_pos_int "$img_width" || ! is_pos_int "$img_height"; then
        echo "Couldn't get image size from identify: $format_output" >&2
        echo >&2
        exit 1
    fi
    opt_cols_expr="(${scale}*${img_width}/${cell_width})"
    opt_rows_expr="(${scale}*${img_height}/${cell_height})"
    if [ -z "$cols" ] && [ -z "$rows" ]; then
        # If columns and rows are not specified, compute the optimal values
        # using the information about rows and columns per inch.
        cols="$(bc_round "$opt_cols_expr")"
        rows="$(bc_round "$opt_rows_expr")"
        # Make sure that automatically computed rows and columns are within some
        # sane limits
        if [ "$cols" -gt "$max_cols" ]; then
            rows="$(bc_round "$rows * $max_cols / $cols")"
            cols="$max_cols"
        fi
        if [ "$rows" -gt "$max_rows" ]; then
            cols="$(bc_round "$cols * $max_rows / $rows")"
            rows="$max_rows"
        fi
    elif [ -z "$cols" ]; then
        # If only one dimension is specified, compute the other one to match the
        # aspect ratio as close as possible.
        cols="$(bc_round "${opt_cols_expr}*${rows}/${opt_rows_expr}")"
    elif [ -z "$rows" ]; then
        rows="$(bc_round "${opt_rows_expr}*${cols}/${opt_cols_expr}")"
    fi

    if [ "$cols" -lt 1 ]; then
        cols=1
    fi
    if [ "$rows" -lt 1 ]; then
        rows=1
    fi
fi

#####################################################################
# Generate an image id
#####################################################################

image_id=""
while [ -z "$image_id" ]; do
    image_id="$(shuf -i 16777217-4294967295 -n 1)"
    # Check that the id requires 24-bit fg colors.
    if [ "$(expr \( "$image_id" / 256 \) % 65536)" -eq 0 ]; then
        image_id=""
    fi
done

#####################################################################
# Uploading the image
#####################################################################

# Choose the uploading method
if [ "$uploading_method" = "auto" ]; then
    if [ -n "$SSH_CLIENT" ] || [ -n "$SSH_TTY" ] || [ -n "$SSH_CONNECTION" ]; then
        uploading_method="direct"
    else
        uploading_method="file"
    fi
fi

# Functions to emit the start and the end of a graphics command.
if [ -n "$inside_tmux" ]; then
    # If we are in tmux we have to wrap the command in Ptmux.
    graphics_command_start='\ePtmux;\e\e_G'
    graphics_command_end='\e\e\\\e\\'
else
    graphics_command_start='\e_G'
    graphics_command_end='\e\\'
fi

start_gr_command() {
    printf "$graphics_command_start" >> "$tty"
}
end_gr_command() {
    printf "$graphics_command_end" >> "$tty"
}

# Send a graphics command with the correct start and end
gr_command() {
    start_gr_command
    printf '%s' "$1" >> "$tty"
    end_gr_command
}

# Send an uploading command. Usage: gr_upload <action> <command> <file>
# Where <action> is a part of command that specifies the action, it will be
# repeated for every chunk (if the method is direct), and <command> is the rest
# of the command that specifies the image parameters. <action> and <command>
# must not include the transmission method or ';'.
# Example:
# gr_upload "a=T,q=2" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$file"
gr_upload() {
    arg_action="$1"
    arg_command="$2"
    arg_file="$3"
    if [ "$uploading_method" = "file" ]; then
        # base64-encode the filename
        encoded_filename=$(printf '%s' "$arg_file" | base64 -w0)
        gr_command "${arg_action},${arg_command},t=f;${encoded_filename}"
    fi
    if [ "$uploading_method" = "direct" ]; then
        # Create a temporary directory to store the chunked image.
        chunkdir="$(mktemp -d)"
        if [ ! "$chunkdir" ] || [ ! -d "$chunkdir" ]; then
            echo "Can't create a temp dir" >&2
            exit 1
        fi
        # base64-encode the file and split it into chunks. The size of each
        # graphics command shouldn't be more than 4096, so we set the size of an
        # encoded chunk to be 3968, slightly less than that.
        chunk_size=3968
        cat "$arg_file" | base64 -w0 | split -b "$chunk_size" - "$chunkdir/chunk_"

        # Issue a command indicating that we want to start data transmission for
        # a new image.
        gr_command "${arg_action},${arg_command},t=d,m=1"

        # Transmit chunks.
        for chunk in "$chunkdir/chunk_"*; do
            start_gr_command
            printf '%s' "${arg_action},i=${image_id},m=1;" >> "$tty"
            cat "$chunk" >> "$tty"
            end_gr_command
            rm "$chunk"
        done

        # Tell the terminal that we are done.
        gr_command "${arg_action},i=$image_id,m=0"

        # Remove the temporary directory.
        rmdir "$chunkdir"
    fi
}

delayed_frame_dir_cleanup() {
    arg_frame_dir="$1"
    sleep 2
    if [ -n "$arg_frame_dir" ]; then
        for frame in "$arg_frame_dir"/frame_*.png; do
            rm "$frame"
        done
        rmdir "$arg_frame_dir"
    fi
}

upload_image_and_print_placeholder() {
    # Check if the file is an animation.
    frame_count=$($identify -format '%n\n' "$file" | head -n 1)
    if [ "$frame_count" -gt 1 ]; then
        # The file is an animation, decompose into frames and upload each frame.
        frame_dir="$(mktemp -d)"
        frame_dir="$HOME/temp/frames${frame_dir}"
        mkdir -p "$frame_dir"
        if [ ! "$frame_dir" ] || [ ! -d "$frame_dir" ]; then
            echo "Can't create a temp dir for frames" >&2
            exit 1
        fi

        # Decompose the animation into separate frames.
        $convert "$file" -coalesce "$frame_dir/frame_%06d.png"

        # Get all frame delays at once, in centiseconds, as a space-separated
        # string.
        delays=$($identify -format "%T " "$file")

        frame_number=1
        for frame in "$frame_dir"/frame_*.png; do
            # Read the delay for the current frame and convert it from
            # centiseconds to milliseconds.
            delay=$(printf '%s' "$delays" | cut -d ' ' -f "$frame_number")
            delay=$((delay * 10))
            # If the delay is 0, set it to 100ms.
            if [ "$delay" -eq 0 ]; then
                delay=100
            fi

            if [ -n "$speed" ]; then
                delay=$(bc_round "$delay / $speed")
            fi

            if [ "$frame_number" -eq 1 ]; then
                # Upload the first frame with a=T
                gr_upload "q=2,a=T" "f=100,U=1,i=${image_id},c=${cols},r=${rows}" "$frame"
                # Set the delay for the first frame and also play the animation
                # in loading mode (s=2).
                gr_command "a=a,v=1,s=2,r=${frame_number},z=${delay},i=${image_id}"
                # Print the placeholder after the first frame to reduce the wait
                # time.
                print_placeholder
            else
                # Upload subsequent frames with a=f
                gr_upload "q=2,a=f" "f=100,i=${image_id},z=${delay}" "$frame"
            fi

            frame_number=$((frame_number + 1))
        done

        # Play the animation in loop mode (s=3).
        gr_command "a=a,v=1,s=3,i=${image_id}"

        # Remove the temporary directory, but do it in the background with a
        # delay to avoid removing files before they are loaded by the terminal.
        delayed_frame_dir_cleanup "$frame_dir" 2> /dev/null &
    else
        # The file is not an animation, upload it directly
        gr_upload "q=2,a=T" "U=1,i=${image_id},f=100,c=${cols},r=${rows}" "$file"
        # Print the placeholder
        print_placeholder
    fi
}

#####################################################################
# Printing the image placeholder
#####################################################################

print_placeholder() {
    # Each line starts with the escape sequence to set the foreground color to
    # the image id.
    blue="$(expr "$image_id" % 256 )"
    green="$(expr \( "$image_id" / 256 \) % 256 )"
    red="$(expr \( "$image_id" / 65536 \) % 256 )"
    line_start="$(printf "\e[38;2;%d;%d;%dm" "$red" "$green" "$blue")"
    line_end="$(printf "\e[39;m")"

    id4th="$(expr \( "$image_id" / 16777216 \) % 256 )"
    eval "id_diacritic=\$d${id4th}"

    # Reset the brush state, mostly to reset the underline color.
    printf "\e[0m"

    # Fill the output with characters representing the image
    for y in $(seq 0 "$(expr "$rows" - 1)"); do
        eval "row_diacritic=\$d${y}"
        printf '%s' "$line_start"
        for x in $(seq 0 "$(expr "$cols" - 1)"); do
            eval "col_diacritic=\$d${x}"
            # Note that when $x is out of bounds, the column diacritic will
            # be empty, meaning that the column should be guessed by the
            # terminal.
            if [ "$x" -ge "$num_diacritics" ]; then
                printf '%s' "${placeholder}${row_diacritic}"
            else
                printf '%s' "${placeholder}${row_diacritic}${col_diacritic}${id_diacritic}"
            fi
        done
        printf '%s\n' "$line_end"
    done

    printf "\e[0m"
}

d0="̅"
d1="̍"
d2="̎"
d3="̐"
d4="̒"
d5="̽"
d6="̾"
d7="̿"
d8="͆"
d9="͊"
d10="͋"
d11="͌"
d12="͐"
d13="͑"
d14="͒"
d15="͗"
d16="͛"
d17="ͣ"
d18="ͤ"
d19="ͥ"
d20="ͦ"
d21="ͧ"
d22="ͨ"
d23="ͩ"
d24="ͪ"
d25="ͫ"
d26="ͬ"
d27="ͭ"
d28="ͮ"
d29="ͯ"
d30="҃"
d31="҄"
d32="҅"
d33="҆"
d34="҇"
d35="֒"
d36="֓"
d37="֔"
d38="֕"
d39="֗"
d40="֘"
d41="֙"
d42="֜"
d43="֝"
d44="֞"
d45="֟"
d46="֠"
d47="֡"
d48="֨"
d49="֩"
d50="֫"
d51="֬"
d52="֯"
d53="ׄ"
d54="ؐ"
d55="ؑ"
d56="ؒ"
d57="ؓ"
d58="ؔ"
d59="ؕ"
d60="ؖ"
d61="ؗ"
d62="ٗ"
d63="٘"
d64="ٙ"
d65="ٚ"
d66="ٛ"
d67="ٝ"
d68="ٞ"
d69="ۖ"
d70="ۗ"
d71="ۘ"
d72="ۙ"
d73="ۚ"
d74="ۛ"
d75="ۜ"
d76="۟"
d77="۠"
d78="ۡ"
d79="ۢ"
d80="ۤ"
d81="ۧ"
d82="ۨ"
d83="۫"
d84="۬"
d85="ܰ"
d86="ܲ"
d87="ܳ"
d88="ܵ"
d89="ܶ"
d90="ܺ"
d91="ܽ"
d92="ܿ"
d93="݀"
d94="݁"
d95="݃"
d96="݅"
d97="݇"
d98="݉"
d99="݊"
d100="߫"
d101="߬"
d102="߭"
d103="߮"
d104="߯"
d105="߰"
d106="߱"
d107="߳"
d108="ࠖ"
d109="ࠗ"
d110="࠘"
d111="࠙"
d112="ࠛ"
d113="ࠜ"
d114="ࠝ"
d115="ࠞ"
d116="ࠟ"
d117="ࠠ"
d118="ࠡ"
d119="ࠢ"
d120="ࠣ"
d121="ࠥ"
d122="ࠦ"
d123="ࠧ"
d124="ࠩ"
d125="ࠪ"
d126="ࠫ"
d127="ࠬ"
d128="࠭"
d129="॑"
d130="॓"
d131="॔"
d132="ྂ"
d133="ྃ"
d134="྆"
d135="྇"
d136="፝"
d137="፞"
d138="፟"
d139="៝"
d140="᤺"
d141="ᨗ"
d142="᩵"
d143="᩶"
d144="᩷"
d145="᩸"
d146="᩹"
d147="᩺"
d148="᩻"
d149="᩼"
d150="᭫"
d151="᭭"
d152="᭮"
d153="᭯"
d154="᭰"
d155="᭱"
d156="᭲"
d157="᭳"
d158="᳐"
d159="᳑"
d160="᳒"
d161="᳚"
d162="᳛"
d163="᳠"
d164="᷀"
d165="᷁"
d166="᷃"
d167="᷄"
d168="᷅"
d169="᷆"
d170="᷇"
d171="᷈"
d172="᷉"
d173="᷋"
d174="᷌"
d175="᷑"
d176="᷒"
d177="ᷓ"
d178="ᷔ"
d179="ᷕ"
d180="ᷖ"
d181="ᷗ"
d182="ᷘ"
d183="ᷙ"
d184="ᷚ"
d185="ᷛ"
d186="ᷜ"
d187="ᷝ"
d188="ᷞ"
d189="ᷟ"
d190="ᷠ"
d191="ᷡ"
d192="ᷢ"
d193="ᷣ"
d194="ᷤ"
d195="ᷥ"
d196="ᷦ"
d197="᷾"
d198="⃐"
d199="⃑"
d200="⃔"
d201="⃕"
d202="⃖"
d203="⃗"
d204="⃛"
d205="⃜"
d206="⃡"
d207="⃧"
d208="⃩"
d209="⃰"
d210="⳯"
d211="⳰"
d212="⳱"
d213="ⷠ"
d214="ⷡ"
d215="ⷢ"
d216="ⷣ"
d217="ⷤ"
d218="ⷥ"
d219="ⷦ"
d220="ⷧ"
d221="ⷨ"
d222="ⷩ"
d223="ⷪ"
d224="ⷫ"
d225="ⷬ"
d226="ⷭ"
d227="ⷮ"
d228="ⷯ"
d229="ⷰ"
d230="ⷱ"
d231="ⷲ"
d232="ⷳ"
d233="ⷴ"
d234="ⷵ"
d235="ⷶ"
d236="ⷷ"
d237="ⷸ"
d238="ⷹ"
d239="ⷺ"
d240="ⷻ"
d241="ⷼ"
d242="ⷽ"
d243="ⷾ"
d244="ⷿ"
d245="꙯"
d246="꙼"
d247="꙽"
d248="꛰"
d249="꛱"
d250="꣠"
d251="꣡"
d252="꣢"
d253="꣣"
d254="꣤"
d255="꣥"
d256="꣦"
d257="꣧"
d258="꣨"
d259="꣩"
d260="꣪"
d261="꣫"
d262="꣬"
d263="꣭"
d264="꣮"
d265="꣯"
d266="꣰"
d267="꣱"
d268="ꪰ"
d269="ꪲ"
d270="ꪳ"
d271="ꪷ"
d272="ꪸ"
d273="ꪾ"
d274="꪿"
d275="꫁"
d276="︠"
d277="︡"
d278="︢"
d279="︣"
d280="︤"
d281="︥"
d282="︦"
d283="𐨏"
d284="𐨸"
d285="𝆅"
d286="𝆆"
d287="𝆇"
d288="𝆈"
d289="𝆉"
d290="𝆪"
d291="𝆫"
d292="𝆬"
d293="𝆭"
d294="𝉂"
d295="𝉃"
d296="𝉄"

num_diacritics="297"

placeholder="􎻮"

#####################################################################
# Upload the image and print the placeholder
#####################################################################

upload_image_and_print_placeholder
