#!/usr/bin/env bash

# This script will take an HTML compatible hex colour string and turn it into
# a terminal sequence for true colour and other formats which best approximate
# its colour value.
#
# After that, we can map a few other special named symbols.  The idea is to
# permit shell variables such as `aaffcc bold italic` to indicate what you
# want a format to look like.

debug=0

# This is the named color map.  Just add new entries and they'll be supported...

declare -A map
map[white]="FFFFFF"
map[silver]="C0C0C0"
map[gray]="808080"
map[grey]="808080"
map[black]="000000"
map[red]="FF0000"
map[maroon]="800000"
map[yellow]="FFFF00"
map[olive]="808000"
map[lime]="00FF00"
map[green]="008000"
map[aqua]="00FFFF"
map[teal]="008080"
map[blue]="0000FF"
map[navy]="000080"
map[fuchsia]="FF00FF"
map[purple]="800080"

# Some VGA color names can be promoted:
map[brown]="aa5500"
map[cyan]="00aaaa"


# The VGA names can be prefixed with "vga-" to explicitly avoid the collisions with HTML names, above...
# Based upon wikipedia's article on ANSI.  See the table on that page for details.
map[vga-black]="000000"
map[vga-red]="aa0000"
map[vga-green]="00aa00"
map[vga-brown]="aa5500"
map[vga-yellow]="aa5500"
map[vga-blue]="0000aa"
map[vga-magenta]="aa00aa"
map[vga-cyan]="00aaaa"
map[vga-white]="aaaaaa"

map[vga-brightblack]="555555"
map[vga-grey]="555555"
map[vga-gray]="555555"
map[vga-brightred]="ff5555"
map[vga-brightgreen]="55ff55"
map[vga-brightyellow]="ffff55"
map[vga-brightblue]="5555ff"
map[vga-brightmagenta]="ff55ff"
map[vga-brightcyan]="55ffff"
map[vga-brightwhite]="ffffff"

nocsi=0
if [[ $1 == "no-csi" ]]
then
    nocsi=1
    shift 1
fi

function dump_colors()
{
    for key in ${!map[@]}
    do
        printf "%-24s${map[${key}]}  |" ${key}
        printf "`$0 reset reverse fg:${key}`native-sample`$0 reset`|"
        if [[ ! -v CSHENV_TERMINAL_COLORS ]] || (( ${CSHENV_TERMINAL_COLORS} >= 256 ))
        then
            printf "`env CSHENV_TERMINAL_COLORS=256 $0 reset reverse fg:${key}`8-bit sample`$0 reset`|"
        fi
        if [[ ! -v CSHENV_TERMINAL_COLORS ]] || (( ${CSHENV_TERMINAL_COLORS} >= 16 ))
        then
            printf "`env CSHENV_TERMINAL_COLORS=16 $0 reset reverse fg:${key}`4-bit sample`$0 reset`|"
        fi
        if [[ ! -v CSHENV_TERMINAL_COLORS ]] || (( ${CSHENV_TERMINAL_COLORS} >= 8 ))
        then
            printf "`env CSHENV_TERMINAL_COLORS=8 $0 reset reverse fg:${key}`3-bit sample`$0 reset`|"
        fi
        printf "\n"
    done

    exit 0
}

function setup_color_depth()
{
    use_8_bit=0
    use_4_bit=0
    use_3_bit=0

    # Must only be set if terminal colors are reduced...
    if [[ -v CSHENV_TERMINAL_COLORS ]]
    then
        if (( ${CSHENV_TERMINAL_COLORS} == 256 ))
        then
            use_8_bit=1
        elif (( ${CSHENV_TERMINAL_COLORS} == 16 ))
        then
            use_4_bit=1
        elif (( ${CSHENV_TERMINAL_COLORS} == 8 ))
        then
            use_3_bit=1
        else
            exit -1
        fi
    fi
}

function render_color()
{
    command_color=30
    if (( ${use_3_bit} ))
    then
        # Underline colours need 8-bit or more
        if (( ${background} > 1 )) ; then return; fi
        basecolor=$(( ${command_color} + ${background}*10))
        next="$(( ${basecolor} + ${legacy_3_bit} ))"
    elif (( ${use_4_bit} ))
    then
        # Underline colours need 8-bit or more
        if (( ${background} > 1 )) ; then return; fi
        command_color=$(( ${command_color} + ${intensity_1_bit} * 60 ))
        basecolor=$(( ${command_color} + ${background}*10 ))
        next="$(( ${basecolor} + ${legacy_3_bit} ))"
    elif (( ${use_8_bit} ))
    then
        basecolor=$(( 8 + ${command_color} + ${background}*10 ))
        next="${basecolor};5;${ext_8_bit}"
    else
        basecolor=$(( 8 + ${command_color} + ${background}*10 ))
        next="${basecolor};2;${red_dec};${green_dec};${blue_dec}"
    fi
}



function ansi_color()
{
    if (( ${1} > 15 ))
    then
        exit -1
    elif (( ${1} >= 8 ))
    then
        use_4_bit=1
        intensity_1_bit=1
        legacy_3_bit=$(( ${1} - 8 ))
    else
        use_3_bit=1
        intensity_1_bit=0
        legacy_3_bit=$(( ${1} ))
    fi
}

function ext_color()
{
    use_8_bit=1
    selection=${1:4}
    if (( ${1} > 255 ))
    then
        exit -1
    else
        ext_8_bit=${1}
    fi
}

function rgb_color()
{
    color=$1
    case $1 in
        [0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]) color=${1:0:1}${1:0:1}${1:1:1}${1:1:1}${1:2:1}${1:2:1} ;;
    esac
    # First split off the red, green, and blue components...
    red_hex=${color:0:2}
    green_hex=${color:2:2}
    blue_hex=${color:4:2}

    # Convert to decimal...
    red_dec=$((16#${red_hex}))
    green_dec=$((16#${green_hex}))
    blue_dec=$((16#${blue_hex}))

    if (( ${debug} != 0 ))
    then
        echo "Red: $red_dec"
        echo "Green: $green_dec"
        echo "Blue: $blue_dec"
    fi

    # Now compute the rest of the stuff...

    # We probably don't have to compute all the unused color states.  We only have to compute
    # the color state we're signed up for by CSHENV_TERMINAL_COLORS... but whatever...

    # We round up by 128 points of colour, so that if we're above a certain intensity, we get the top
    # bit set.

    red_1_bit=$(( ( ( ${red_dec} + 128 )  >> 8 ) & 1 ))
    green_1_bit=$(( ( ( ${green_dec} + 128 )  >> 8 ) & 1 ))
    blue_1_bit=$(( ( ( ${blue_dec} + 128 )  >> 8 ) & 1 ))
    intensity_1_bit=0


    # If we can support an intensity bit, we'll turn that on too...
    # But we're going to stop using bold to set the colour "intense"
    # We'll use the 9x and 10x forms...
    if (( ${red_dec} >= 192 || ${green_dec} >= 192 || ${blue_dec} >= 192 ))
    then
        intensity_1_bit=1
    fi

    if (( ${debug} != 0 ))
    then
        echo "Red bit: " $red_1_bit
        echo "Green bit: " $green_1_bit
        echo "Blue bit: " $blue_1_bit
        echo "Intensity bit: " $intensity_1_bit
    fi


    # Because grey is tricky, I'm going to special case it:
    if (( 1 && ${red_dec} == ${green_dec} && ${green_dec} == ${blue_dec} && ${red_dec} < 128 && ${red_dec} >= 80 ))
    then
        red_1_bit=0
        green_1_bit=0
        blue_1_bit=0
        intensity_1_bit=1
    fi

    # Because brown is tricky, I'm going to special case it:
    if (( ${red_dec} > 128 && ${green_dec} > 80 && ${blue_dec} < 50 ))
    then
        red_1_bit=1
        green_1_bit=1
        blue_1_bit=0
    fi

    if (( ${debug} != 0 ))
    then
        echo "Red bit: " $red_1_bit
        echo "Green bit: " $green_1_bit
        echo "Blue bit: " $blue_1_bit
        echo "Intensity bit: " $intensity_1_bit
    fi

    # This lets us combine them for a legacy colour value in the legacy colour space...

    legacy_3_bit=$(( ( ${blue_1_bit} << 2 ) + ( ${green_1_bit} << 1 ) + ( ${red_1_bit} ) ))

    if (( ${debug} != 0 ))
    then
        echo "Legacy colour: " ${legacy_3_bit} " Intensity: " ${intensity_1_bit}
    fi

    # Now compute an extended colour cube placement (216 colours):

    red_ext_val=$(( ${red_dec} * 6 / 256 ))
    green_ext_val=$(( ${green_dec} * 6 / 256 ))
    blue_ext_val=$(( ${blue_dec} * 6 / 256 ))

    if (( ${debug} != 0 ))
    then
        echo "Red ext: " ${red_ext_val}
        echo "Green ext: " ${green_ext_val}
        echo "Blue ext: " ${blue_ext_val}
    fi

    ext_8_bit=$(( 16 + 36 * ${red_ext_val} + 6 * ${green_ext_val} + ${blue_ext_val} ))

    if (( ${debug} ))
    then
        echo "Computed 216 color cube: " ${ext_8_bit}
    fi

    # Check for precise greyscale, which would replace the above:
    # (Note that precise greyscale is implied by a user explicitly making ALL of RGB the
    # same value, thus demanding greyscale, rather than a subtle tinting...)

    if (( ${red_dec} == ${green_dec} && ${green_dec} == ${blue_dec} ))
    then
        ext_8_bit=$(( 232 + ${red_dec} * 24 / 256 ))
    fi

    if (( ${debug} ))
    then
        echo "Computed 256 color choice, after applying greyscale scan: " ${ext_8_bit}
    fi

    # Check for SPECIFIC white and black and grey:
    # The half-saturation, no saturation, and full saturation values are mapped
    # to specific points in the legacy set.  (This may be ill-advised as a manually
    # built reverse video terminal setting through colour mapping may make it so that
    # we wind up in la-la land.  Perhaps the better way is to find the head and tail
    # points in the 6x6x6 colour cube.)
    #
    # TODO: Only map the head and tail of the colour cube.
    if (( ${red_dec} == ${green_dec} && ${green_dec} == ${blue_dec} ))
    then
        if (( ${red_dec} == 0 ))
        then
            ext_8_bit=0
        elif (( ${red_dec} == 255 ))
        then
            ext_8_bit=15
        elif (( ${red_dec} == 128 ))
        then
            ext_8_bit=8
        elif (( ${red_dec} == 192 ))
        then
            ext_8_bit=7
        fi
    fi

    if (( ${debug} ))
    then
        echo "Computed 256 color choice, after applying white and black scan: " ${ext_8_bit}
    fi

    # We do NOT check for the base colours precisely.  Since those 16 can be custom mapped by your terminal emulator, you just pass `ansi:0` thru
    # `ansi:15` to select them by ID.  Or use `ext:0` thru `ext:255` to select a specific extended colour.
}

function make_color()
{
    setup_color_depth
    background=$1 # $1 is the background bit
    $2 $3 # $2 is the color function
    render_color
}

function named_color()
{
    name=${1,,}
    if [[ ! -v map[$name] ]]
    then
        echo "ERROR!" 1>&2 ; exit -1
    fi

    mapped=${map[$name]}
    rgb_color ${mapped}
}

output=""

while [[ ! -z $1 ]]
do
    case $1 in
        dump-colors) dump_colors ;;

        reset) next=0 ;;
        bold) next=1 ;;
        dim) next=2 ;;
        italic) next=3 ;;
        underline|under) next=4 ;;
        blink) next=5 ;;
        fastblink) next=6 ;;
        reverse) next=7 ;;
        hide|conceal) next=8 ;;
        strike|strikethrough|strikethru) next=9 ;;
        doubleunder) next=21 ;;
        reveal) next=28 ;;

        ansi:*) make_color 0 ansi_color ${1:5} ;;
        ext:*) make_color 0 ext_color ${1:4} ;;
        # 12-bit color also supported
        [0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]) make_color 0 rgb_color $1 ;;
        [0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]) make_color 0 rgb_color $1 ;;

        fg:ansi:*) make_color 0 ansi_color ${1:8} ;;
        fg:ext:*) make_color 0 ext_color ${1:7} ;;
        # 12-bit color also supported
        fg:[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]) make_color 0 rgb_color ${1:3} ;;
        fg:[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]) make_color 0 rgb_color ${1:3} ;;

        bg:ansi:*) make_color 1 ansi_color ${1:8} ;;
        bg:ext:*) make_color 1 ext_color ${1:7} ;;
        # 12-bit color also supported
        bg:[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]) make_color 1 rgb_color ${1:3} ;;
        bg:[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]) make_color 1 rgb_color ${1:3} ;;

        ul:ext:*) make_color 2 ext_color ${1:7} ;;
        # 12-bit color also supported
        ul:[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]) make_color 2 rgb_color ${1:3} ;;
        ul:[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]) make_color 2 rgb_color ${1:3} ;;

        fg:*) make_color 0 named_color ${1:3} ;;
        bg:*) make_color 1 named_color ${1:3} ;;
        ul:*) make_color 2 named_color ${1:3} ;;
        *) make_color 0 named_color $1 ;;
    esac
    if [[ ! -z ${output} ]]
    then
        output="${output};"
    fi
    output="${output}${next}"
    shift 1
done

if [[ ! -z ${output} ]] && (( ! ${nocsi} ))
then
    output="[${output}m"
fi
if [[ ! -z ${output} ]]
then
    printf ${output}
fi


exit 0
