_reset_cursors() {
    export START_CURSOR=1
    export END_CURSOR=1
}

_constrain_cursors() {
    # constrain the cursors to be within the bounds of the file [0, total_lines+1]
    local total_lines=$(awk 'END {print NR}' "$CURRENT_FILE")
    total_lines=$((total_lines < 1 ? 1 : total_lines))  # if the file is empty, set total_lines to 1
    local start_line=$((CURRENT_LINE - WINDOW / 2))
    local end_line=$((CURRENT_LINE + WINDOW / 2))
    start_line=$((start_line < 1 ? 1 : start_line))
    end_line=$((end_line > total_lines ? total_lines : end_line))
    if [ "$START_CURSOR" -lt "$start_line" ]; then
        echo "START_CURSOR moved to $start_line"
        START_CURSOR=$start_line
    elif [ "$START_CURSOR" -gt "$end_line" ]; then
        START_CURSOR=$end_line
        echo "START_CURSOR moved to $end_line"
    fi
    if [ "$END_CURSOR" -lt "$start_line" ]; then
        echo "END_CURSOR moved to $start_line"
        END_CURSOR=$start_line
    elif [ "$END_CURSOR" -gt "$end_line" ]; then
        echo "END_CURSOR moved to $end_line"
        END_CURSOR=$end_line
    fi
    export START_CURSOR END_CURSOR
}

_print() {
    _constrain_cursors
    local total_lines=$(awk 'END {print NR}' $CURRENT_FILE)
    echo "[File: $(realpath "$CURRENT_FILE") ($total_lines lines total)]"
    local start_line=$((CURRENT_LINE - WINDOW / 2))
    local end_line=$((CURRENT_LINE + WINDOW / 2))
    start_line=$((start_line < 1 ? 1 : start_line))
    end_line=$((end_line > total_lines ? total_lines : end_line))
    local lines=()
    local i=0
    while IFS= read -r line; do
        lines[i++]="$line"
    done < <(awk -v start="$start_line" -v end="$end_line" 'NR>=start && NR<=end {print}' "$CURRENT_FILE")
    local num_lines=${#lines[@]}
    if [ $start_line -gt 1 ]; then
        echo "($((start_line - 1)) more lines above)"
    fi
    for ((i=0; i<num_lines+1; i++)) do
        local line_number=$((start_line + i))
        if [ $line_number -eq $START_CURSOR ]
        then
            echo $START_CURSOR_MARK
        fi
        # if i in [0, num_lines-1] then print the line number and the line
        if [ $i -ge 0 ] && [ $i -lt $num_lines ]
        then
            echo "$line_number:${lines[i]}"
        fi
        if [ $line_number -eq $END_CURSOR ]
        then
            echo $END_CURSOR_MARK
        fi
    done
    lines_below=$(jq -n "$total_lines - $start_line - $num_lines + 1" | jq '[0, .] | max')
    if [ $lines_below -gt 0 ]; then
        echo "($lines_below more lines below)"
    fi
}

_print_search() {
    if [ -z "$SEARCH_RESULTS" ]
    then
        echo "No search results found."
        return
    # else if number of search files is > 50, print "Too many search results. Please narrow your search."
    elif [ ${#SEARCH_RESULTS[@]} -gt 50 ]
    then
        echo "Found ${#SEARCH_RESULTS[@]} search results."
        echo "Too many search results. Please consider narrowing your search."
        return
    fi
    export CURRENT_FILE=$(realpath ${SEARCH_FILES[$SEARCH_INDEX]})
    export CURRENT_LINE=${SEARCH_RESULTS[$SEARCH_INDEX]}
    echo "[Result "$(jq -n "($SEARCH_INDEX + 1)")"/${#SEARCH_RESULTS[@]} (Line $CURRENT_LINE)]"
    _constrain_line  # reset CURRENT_LINE if it is out of bounds
    _reset_cursors
    _print
}

_constrain_line() {
    if [ -z "$CURRENT_FILE" ]
    then
        echo "No file open. Use the open command first."
        return
    fi
    local max_line=$(awk 'END {print NR}' $CURRENT_FILE)
    export CURRENT_LINE=$(jq -n "[$CURRENT_LINE, $max_line - $WINDOW/2] | min") _print
    export CURRENT_LINE=$(jq -n "[$CURRENT_LINE, $WINDOW/2] | max") _print
}

# @yaml
# signature: set_cursors <start_line> <end_line>
# docstring: sets the start and end cursors to the given line numbers
set_cursors() {
    if [ -z "$CURRENT_FILE" ]
    then
        echo "No file open. Use the open command first."
        return
    fi
    if [ $# -lt 2 ]
    then
        echo "Usage: set_cursors <start_line> <end_line>"
        return
    fi
    local start_line=$1
    local end_line=$2
    local re='^[0-9]+$'
    if ! [[ $start_line =~ $re ]]
    then
        echo "Usage: set_cursors <start_line> <end_line>"
        echo "Error: start_line must be a number"
        return
    fi
    if ! [[ $end_line =~ $re ]]
    then
        echo "Usage: set_cursors <start_line> <end_line>"
        echo "Error: end_line must be a number"
        return
    fi
    if [ $start_line -gt $end_line ]
    then
        echo "Usage: set_cursors <start_line> <end_line>"
        echo "Error: start_line must be less than or equal to end_line"
        return
    fi
    export START_CURSOR=$start_line
    export END_CURSOR=$end_line
    _print
}

# @yaml
# signature: open <path>
# docstring: opens the file at the given path in the editor
open() {
    if [ -z "$1" ]
    then
        echo "Usage: open <file>"
        return
    fi

    if [ -f "$1" ]
    then
        if [ "$(realpath $1)" = "$CURRENT_FILE" ]
        then
            echo "$1"' is already open. Use `scroll_down`, `scroll_up`, `goto` and `search` to navigate file.'
            return
        fi
        export CURRENT_FILE=$(realpath $1)
        export CURRENT_LINE=$(jq -n "$WINDOW/2")
        _constrain_line
        _reset_cursors
        _print
    else
        echo "File $1 not found"
    fi
}

# moves the window up {WINDOW} lines
scroll_down() {
    if [ -z "$CURRENT_FILE" ]
    then
        echo "No file open. Use the open command first."
        return
    fi
    export CURRENT_LINE=$(jq -n "$CURRENT_LINE + $WINDOW - $OVERLAP")
    _constrain_line
    _print
}

# moves the window down {WINDOW} lines
scroll_up() {
    if [ -z "$CURRENT_FILE" ]
    then
        echo "No file open. Use the open command first."
        return
    fi
    export CURRENT_LINE=$(jq -n "$CURRENT_LINE - $WINDOW + $OVERLAP")
    _constrain_line
    _print
}

# @yaml
# signature: goto <n>
# docstring: moves the window to be centered on line n
goto() {
    if [ $# -gt 1 ]; then
        echo "goto allows only one line number at a time."
        return
    fi
    if [ -z "$CURRENT_FILE" ]
    then
        echo "No file open. Use the open command first."
        return
    fi
    if [ -z "$1" ]
    then
        echo "Usage: goto <line>"
        return
    fi
    if ! [[ $1 =~ ^[0-9]+$ ]]
    then
        echo "Usage: goto <line>"
        echo "Error: <line> must be a number"
        return
    fi
    local max_line=$(awk 'END {print NR}' $CURRENT_FILE)
    if [ $1 -gt $max_line ]
    then
        echo "Error: <line> must be less than or equal to $max_line"
        return
    fi
    export CURRENT_LINE=$1
    _constrain_line
    _print
}

# @yaml
# signature: get_symbols [<path>]
# docstring: gets all of the symbols in the given file (defaults to current open file if no file is provided)
get_symbols() {
    if [ -z "$1" ]; then
        if [ -z "$CURRENT_FILE" ]; then
            echo "Usage: get_symbols <file>"
            return
        fi
        file_path="$CURRENT_FILE"
    else
        file_path="$1"
    fi

    if [ ! -f "$file_path" ]; then
        echo "File not found: $file_path"
        return
    fi

    /root/miniconda3/bin/python3 /root/tools/get_symbols.py "$file_path"
}

# @yaml
# signature: search <string> [<file_path>]
# docstring: searches for the given (case sensitive) string in the specified file or the open file
search() {
    if [ $# -lt 1 ] || [ $# -gt 2 ]; then
        echo "Usage: search <term> [<file_path>]"
        return
    fi

    search_term="$1"
    file_path="$2"

    if [ -z "$file_path" ]; then
        if [ -z "$CURRENT_FILE" ]; then
            echo "No file open. Provide a file path or use the open command first."
            return
        fi
        file_path="$CURRENT_FILE"
    fi

    if [ ! -f "$file_path" ]; then
        echo "The given file does not exist: $file_path"
        return
    fi

    export SEARCH_FILES=( $(grep -nIH "$search_term" "$file_path" | cut -d: -f1) )
    export SEARCH_RESULTS=( $(grep -nIH "$search_term" "$file_path" | cut -d: -f2) )
    export SEARCH_INDEX=0
    _print_search
}

# @yaml
# signature: search_all <string> [<directory>]
# docstring: searches for the given (case sensitive) string in all files in the specified directory (or the current working directory)
search_all() {
    if [ $# -lt 1 ] || [ $# -gt 2 ]; then
        echo "Usage: search_all <term> [<directory>]"
        return
    fi

    search_term="$1"
    directory="$2"

    if [ -z "$directory" ]; then
        # Use current directory if not provided
        directory="."
    fi

    if [ ! -d "$directory" ]; then
        # Check that the directory exists
        echo "The given directory does not exist: $directory"
        return
    fi

    export SEARCH_FILES=( $(grep -rnIH "$search_term" "$directory" | cut -d: -f1) )
    export SEARCH_RESULTS=( $(grep -rnIH "$search_term" "$directory" | cut -d: -f2) )
    export SEARCH_INDEX=0
    _print_search
}

# moves to the next search result
next() {
    if [ -z "$CURRENT_FILE" ]
    then
        echo "No file open. Use the open command first."
        return
    fi
    export SEARCH_INDEX=$(jq -n "($SEARCH_INDEX + 1) % ([${#SEARCH_RESULTS[@]}, 1] | max)")
    _print_search
}

# moves to the previous search result
prev() {
    if [ -z "$CURRENT_FILE" ]
    then
        echo "No file open. Use the open command first."
        return
    fi
    export SEARCH_INDEX=$(jq -n "($SEARCH_INDEX - 1) % ([${#SEARCH_RESULTS[@]}, 1] | max)")
    _print_search
}
