#!/usr/bin/env bash
###############################################################################
# dcalc — Advanced Scientific Calculator via dmenu
#
# Maintainer:   Kris Yotam <krisyotam@pm.me>
# License:      MIT
# Created:      2026-02-15
#
# Features:
#   - Arbitrary precision arithmetic (bc -l backend)
#   - Trigonometric functions (sin, cos, tan, asin, acos, atan) — radians & degrees
#   - Logarithms (ln, log10, log2)
#   - Powers, roots, factorials
#   - Constants (pi, e, phi, sqrt2, sqrt3, c, avogadro, planck)
#   - Unit conversions (temperature, distance, weight, data)
#   - Persistent history (~/.cache/dcalc_history)
#   - Result auto-copied to clipboard
#
# Usage:
#   dcalc              # Launch calculator
#   Type expression    # e.g. sin(pi/4), log10(1000), 2^10
#   Type "hist"        # Browse calculation history
#   Type "const"       # List available constants
#   Type "conv"        # Unit conversion mode
#   Type "clear"       # Clear history
###############################################################################

HIST_FILE="${HOME}/.cache/dcalc_history"
mkdir -p "$(dirname "$HIST_FILE")"
touch "$HIST_FILE"

DMENU="dmenu -i -l 20 -p"

###############################################################################
# bc prelude — define functions and constants
###############################################################################

BC_PRELUDE='
scale=20

/* constants */
pi=4*a(1)
e=e(1)
phi=(1+sqrt(5))/2
sqrt2=sqrt(2)
sqrt3=sqrt(3)

/* trig (radians) */
define sin(x)  { return s(x) }
define cos(x)  { return c(x) }
define tan(x)  { return s(x)/c(x) }
define asin(x) { return a(x/sqrt(1-x*x)) }
define acos(x) { if (x==1) return 0; return a(sqrt(1-x*x)/x) }
define atan(x) { return a(x) }

/* trig (degrees) */
define dsin(x)  { return s(x*pi/180) }
define dcos(x)  { return c(x*pi/180) }
define dtan(x)  { return s(x*pi/180)/c(x*pi/180) }
define dasin(x) { return a(x/sqrt(1-x*x))*180/pi }
define dacos(x) { if (x==1) return 0; return a(sqrt(1-x*x)/x)*180/pi }
define datan(x) { return a(x)*180/pi }

/* degrees <-> radians */
define rad(x) { return x*pi/180 }
define deg(x) { return x*180/pi }

/* logarithms */
define ln(x)    { return l(x) }
define log10(x) { return l(x)/l(10) }
define log2(x)  { return l(x)/l(2) }
define log(x)   { return l(x)/l(10) }

/* powers and roots */
define pow(x,y) { return e(y*l(x)) }
define exp(x)   { return e(x) }
define sqrt(x)  { return sqrt(x) }
define cbrt(x)  { return e(l(x)/3) }
define nrt(x,n) { return e(l(x)/n) }

/* combinatorics */
define fact(n) { if (n<=1) return 1; return n*fact(n-1) }
define abs(x)  { if (x<0) return -x; return x }
define ceil(x) { auto s; s=scale; scale=0; x=x/1; scale=s; return x }
define floor(x) { auto s; s=scale; scale=0; x=x/1; scale=s; return x }
define max(a,b) { if (a>b) return a; return b }
define min(a,b) { if (a<b) return a; return b }

/* hyperbolic */
define sinh(x) { return (e(x)-e(-x))/2 }
define cosh(x) { return (e(x)+e(-x))/2 }
define tanh(x) { return (e(x)-e(-x))/(e(x)+e(-x)) }
'

###############################################################################
# helpers
###############################################################################

calc() {
    echo "${BC_PRELUDE}; $1" | bc -l 2>&1
}

show_constants() {
    printf '%s\n' \
        "pi      = 3.14159265358979323846" \
        "e       = 2.71828182845904523536" \
        "phi     = 1.61803398874989484820" \
        "sqrt2   = 1.41421356237309504880" \
        "sqrt3   = 1.73205080756887729352" \
        "c       = 299792458 (m/s)" \
        "avogadro= 6.02214076e23 (1/mol)" \
        "planck  = 6.62607015e-34 (J*s)" \
    | $DMENU "󰎢  Constants" > /dev/null
}

show_help() {
    printf '%s\n' \
        "── Arithmetic ──" \
        "  2+3  4*5  10/3  2^10  15%4" \
        "── Trig (radians) ──" \
        "  sin(x) cos(x) tan(x) asin(x) acos(x) atan(x)" \
        "── Trig (degrees) ──" \
        "  dsin(x) dcos(x) dtan(x) dasin(x) dacos(x) datan(x)" \
        "── Conversion ──" \
        "  rad(deg) deg(rad)" \
        "── Logarithms ──" \
        "  ln(x) log10(x) log2(x)" \
        "── Powers/Roots ──" \
        "  pow(x,y) exp(x) sqrt(x) cbrt(x) nrt(x,n)" \
        "── Other ──" \
        "  fact(n) abs(x) ceil(x) floor(x) max(a,b) min(a,b)" \
        "── Hyperbolic ──" \
        "  sinh(x) cosh(x) tanh(x)" \
        "── Commands ──" \
        "  const  hist  conv  clear  help" \
    | $DMENU "󰋙  Help" > /dev/null
}

unit_convert() {
    local category
    category=$(printf '%s\n' \
        "󰔏  Temperature" \
        "󰕰  Distance" \
        "󰖑  Weight" \
        "  Data" \
        "󰁫  Time" \
        "  Speed" \
    | $DMENU "󰑣  Convert")

    [ -z "$category" ] && return

    local conversion
    case "$category" in
        *Temperature*)
            conversion=$(printf '%s\n' \
                "C→F" "F→C" "C→K" "K→C" "F→K" "K→F" \
            | $DMENU "󰔏  Temp")
            [ -z "$conversion" ] && return
            local val
            val=$($DMENU "  Value" < /dev/null)
            [ -z "$val" ] && return
            local result
            case "$conversion" in
                "C→F") result=$(calc "$val * 9/5 + 32") ;;
                "F→C") result=$(calc "($val - 32) * 5/9") ;;
                "C→K") result=$(calc "$val + 273.15") ;;
                "K→C") result=$(calc "$val - 273.15") ;;
                "F→K") result=$(calc "($val - 32) * 5/9 + 273.15") ;;
                "K→F") result=$(calc "($val - 273.15) * 9/5 + 32") ;;
            esac
            ;;
        *Distance*)
            conversion=$(printf '%s\n' \
                "mi→km" "km→mi" "ft→m" "m→ft" "in→cm" "cm→in" "yd→m" "m→yd" \
            | $DMENU "󰕰  Dist")
            [ -z "$conversion" ] && return
            local val
            val=$($DMENU "  Value" < /dev/null)
            [ -z "$val" ] && return
            local result
            case "$conversion" in
                "mi→km") result=$(calc "$val * 1.60934") ;;
                "km→mi") result=$(calc "$val / 1.60934") ;;
                "ft→m")  result=$(calc "$val * 0.3048") ;;
                "m→ft")  result=$(calc "$val / 0.3048") ;;
                "in→cm") result=$(calc "$val * 2.54") ;;
                "cm→in") result=$(calc "$val / 2.54") ;;
                "yd→m")  result=$(calc "$val * 0.9144") ;;
                "m→yd")  result=$(calc "$val / 0.9144") ;;
            esac
            ;;
        *Weight*)
            conversion=$(printf '%s\n' \
                "lb→kg" "kg→lb" "oz→g" "g→oz" "st→kg" "kg→st" \
            | $DMENU "󰖑  Weight")
            [ -z "$conversion" ] && return
            local val
            val=$($DMENU "  Value" < /dev/null)
            [ -z "$val" ] && return
            local result
            case "$conversion" in
                "lb→kg") result=$(calc "$val * 0.453592") ;;
                "kg→lb") result=$(calc "$val / 0.453592") ;;
                "oz→g")  result=$(calc "$val * 28.3495") ;;
                "g→oz")  result=$(calc "$val / 28.3495") ;;
                "st→kg") result=$(calc "$val * 6.35029") ;;
                "kg→st") result=$(calc "$val / 6.35029") ;;
            esac
            ;;
        *Data*)
            conversion=$(printf '%s\n' \
                "B→KB" "KB→MB" "MB→GB" "GB→TB" \
                "TB→GB" "GB→MB" "MB→KB" "KB→B" \
                "B→bits" "bits→B" \
            | $DMENU "  Data")
            [ -z "$conversion" ] && return
            local val
            val=$($DMENU "  Value" < /dev/null)
            [ -z "$val" ] && return
            local result
            case "$conversion" in
                "B→KB")   result=$(calc "$val / 1024") ;;
                "KB→MB")  result=$(calc "$val / 1024") ;;
                "MB→GB")  result=$(calc "$val / 1024") ;;
                "GB→TB")  result=$(calc "$val / 1024") ;;
                "TB→GB")  result=$(calc "$val * 1024") ;;
                "GB→MB")  result=$(calc "$val * 1024") ;;
                "MB→KB")  result=$(calc "$val * 1024") ;;
                "KB→B")   result=$(calc "$val * 1024") ;;
                "B→bits") result=$(calc "$val * 8") ;;
                "bits→B") result=$(calc "$val / 8") ;;
            esac
            ;;
        *Time*)
            conversion=$(printf '%s\n' \
                "hr→min" "min→hr" "min→sec" "sec→min" \
                "hr→sec" "sec→hr" "days→hr" "hr→days" \
            | $DMENU "󰁫  Time")
            [ -z "$conversion" ] && return
            local val
            val=$($DMENU "  Value" < /dev/null)
            [ -z "$val" ] && return
            local result
            case "$conversion" in
                "hr→min")  result=$(calc "$val * 60") ;;
                "min→hr")  result=$(calc "$val / 60") ;;
                "min→sec") result=$(calc "$val * 60") ;;
                "sec→min") result=$(calc "$val / 60") ;;
                "hr→sec")  result=$(calc "$val * 3600") ;;
                "sec→hr")  result=$(calc "$val / 3600") ;;
                "days→hr") result=$(calc "$val * 24") ;;
                "hr→days") result=$(calc "$val / 24") ;;
            esac
            ;;
        *Speed*)
            conversion=$(printf '%s\n' \
                "mph→kph" "kph→mph" "mph→m/s" "m/s→mph" "kph→m/s" "m/s→kph" "knots→kph" "kph→knots" \
            | $DMENU "  Speed")
            [ -z "$conversion" ] && return
            local val
            val=$($DMENU "  Value" < /dev/null)
            [ -z "$val" ] && return
            local result
            case "$conversion" in
                "mph→kph")    result=$(calc "$val * 1.60934") ;;
                "kph→mph")    result=$(calc "$val / 1.60934") ;;
                "mph→m/s")    result=$(calc "$val * 0.44704") ;;
                "m/s→mph")    result=$(calc "$val / 0.44704") ;;
                "kph→m/s")    result=$(calc "$val / 3.6") ;;
                "m/s→kph")    result=$(calc "$val * 3.6") ;;
                "knots→kph")  result=$(calc "$val * 1.852") ;;
                "kph→knots")  result=$(calc "$val / 1.852") ;;
            esac
            ;;
    esac

    [ -z "$result" ] && return

    # Trim trailing zeros but keep reasonable precision
    result=$(echo "$result" | sed 's/\.\{0,1\}0*$//')

    echo "$conversion: $val → $result" >> "$HIST_FILE"
    printf '%s' "$result" | xclip -selection clipboard
    notify-send "󰃬 dcalc" "$conversion: $val → $result"
}

###############################################################################
# main loop
###############################################################################

while true; do
    # Build prompt from history (most recent at top) + empty for new input
    INPUT=$(tac "$HIST_FILE" 2>/dev/null | $DMENU "󰃬  Calc" < /dev/null)

    # Exit on empty / escape
    [ -z "$INPUT" ] && exit 0

    # Handle special commands
    case "$INPUT" in
        hist|history)
            if [ -s "$HIST_FILE" ]; then
                sel=$(tac "$HIST_FILE" | $DMENU "󰋚  History")
                if [ -n "$sel" ]; then
                    # Extract just the result (after = sign)
                    result="${sel##*= }"
                    printf '%s' "$result" | xclip -selection clipboard
                    notify-send "󰃬 dcalc" "Copied: $result"
                fi
            else
                notify-send "󰃬 dcalc" "No history yet"
            fi
            continue
            ;;
        const|constants)
            show_constants
            continue
            ;;
        conv|convert)
            unit_convert
            continue
            ;;
        clear)
            : > "$HIST_FILE"
            notify-send "󰃬 dcalc" "History cleared"
            continue
            ;;
        help|"?")
            show_help
            continue
            ;;
        quit|exit|q)
            exit 0
            ;;
    esac

    # If user selected a history entry, extract the expression
    if [[ "$INPUT" == *" = "* ]]; then
        INPUT="${INPUT%% = *}"
    fi

    # Evaluate the expression
    RESULT=$(calc "$INPUT" 2>&1)

    # Check for errors
    if [[ "$RESULT" == *"error"* ]] || [[ "$RESULT" == *"illegal"* ]] || [ -z "$RESULT" ]; then
        notify-send "󰃬 dcalc" "Error: $INPUT"
        continue
    fi

    # Trim trailing zeros for cleaner display
    RESULT=$(echo "$RESULT" | sed '/\..*[^0]/{s/0*$//}; s/\.$//')

    # Save to history
    echo "$INPUT = $RESULT" >> "$HIST_FILE"

    # Copy to clipboard
    printf '%s' "$RESULT" | xclip -selection clipboard

    # Show result — selecting it continues the loop with result as new input
    notify-send "󰃬 dcalc" "$INPUT = $RESULT"
done
