#!/usr/bin/env bash

# splice - interactively diff and edit files

# Script that diffs two files; gives summary of findings. Allows user to splice content together in one of two files or the same file. Auto mode (less user input) and Manual (more prompts, every choice made by user).
# Summary first, points out each diff area then prompts per region on whether to splice and to where.
# Optional preview operator to show preview before committing; ask mode previews and prompts.
# Specify what lines to insert at and to what file.
# Option to splice into existing documents or make new merged doc.
# dmenu integration
# Add log file

set -u
set -o pipefail

VERSION="0.1.0"

# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------

VERBOSE=false
PREVIEW=false
ASK=false
AUTO=false

XDG_STATE_HOME=${XDG_STATE_HOME:-"$HOME/.local/state"}
LOG_DIR="$XDG_STATE_HOME/splice"
LOG_FILE="$LOG_DIR/splice.log"
LOG_MAX_SIZE=1048576

SOURCE_A=""
SOURCE_B=""
SOURCE_A_PARENT=""
SOURCE_B_PARENT=""

MERGE_MODE=false
MERGED_FILE=""
PROMPT_COPY_OUTA=false
PROMPT_COPY_OUTB=false

LINES_A=0
LINES_B=0

REGION_COUNT=0
INSERT_COUNT=0
DELETE_COUNT=0
REPLACE_COUNT=0
PREVIEW_CONTEXT=3

FILES_DIFFER=false

TMP_FILES=()
DIFF_FILE=""
REGION_FILE=""
PLAN_FILE=""

# ---------------------------------------------------------------------------
# Utility functions
# ---------------------------------------------------------------------------

rotate_log() {
    [[ -f "$LOG_FILE" ]] || return 0
    (( $(stat -c '%s' "$LOG_FILE") < LOG_MAX_SIZE )) && return 0

    rm -f -- "$LOG_FILE.2" ||
        die "Failed to remove oldest log backup."

    [[ -f "$LOG_FILE.1" ]] &&
        mv -- "$LOG_FILE.1" "$LOG_FILE.2" ||
        :

    mv -- "$LOG_FILE" "$LOG_FILE.1" ||
        die "Failed to rotate log file."

    : >"$LOG_FILE" ||
        die "Failed to create new log file."
}

init_log() {
    mkdir -p -- "$LOG_DIR" ||
        die "Failed to create log directory."

    touch -- "$LOG_FILE" ||
        die "Failed to create log file."
    rotate_log
}

log() {
    printf '%s | %s\n' \
        "$(date '+%Y-%m-%d %H:%M:%S')" \
        "$*" >>"$LOG_FILE"
}

die() {
    printf 'splice [ERROR]: %s\n' "$*" >&2
    [[ -n "${LOG_FILE:-}" ]] &&
        printf '%s | ERROR | %s\n' \
            "$(date '+%Y-%m-%d %H:%M:%S')" \
            "$*" >>"$LOG_FILE" 2>/dev/null || :
    exit 1
}

warn() {
    printf 'splice: [WARN] %s\n' "$*" >&2
}

debug() {
    "$VERBOSE" || return 0
    log "DEBUG | $*"
    printf 'splice [DEBUG]: %s\n' "$*" >&2
}

backup_file() {
    local timestamp
    local backup

    timestamp=$(date '+%Y%m%d_%H%M%S')
    backup="${PROMPT_DESTINATION}.bak_${timestamp}"

    cp -- \
        "$PROMPT_DESTINATION" \
        "$backup" ||
        die "Failed to create backup."

    printf 'Backup created.\n'
    printf '  File: %s\n' "$backup"
}

ask_verify() {
    while :; do
        printf '\n'
        printf 'Verify output file? [Y/n/q]: '
        read -r confirm </dev/tty || cancel

        case "$confirm" in
        "" | y | Y | yes | YES)
            verify_output
            return
            ;;
        n | N | no | NO)
            return
            ;;
        q | Q | quit | QUIT)
            cancel
            ;;
        *)
            warn "Please enter y, n, or q."
            ;;
        esac
    done
}

ask_verbose() {
    while :; do
        printf '\n'
        printf 'See verbose preview? [Y/n/q]: '
        read -r confirm </dev/tty || cancel

        case "$confirm" in
        "" | y | Y | yes | YES)
            printf '\nRaw diff:\n'
            cat -- "$DIFF_FILE"
            printf '\nParsed regions:\n'
            cat -- "$REGION_FILE"
            return
            ;;
        n | N | no | NO)
            return
            ;;
        q | Q | quit | QUIT)
            cancel
            ;;
        *)
            warn "Please enter y, n, or q."
            ;;
        esac
    done
}

ask_context() {
    while :; do
        printf '\n'
        printf 'Preview context lines [3]: '
        read -r context </dev/tty || cancel

        case "$context" in
        "")
            PREVIEW_CONTEXT=3
            return
            ;;
        q | Q | quit | QUIT)
            cancel
            ;;
        *)
            [[ "$context" =~ ^[0-9]+$ ]] &&
                {
                    PREVIEW_CONTEXT=$context
                    return
                }

            warn "Please enter a non-negative integer or q."
            ;;
        esac
    done
}

# ---------------------------------------------------------------------------
# Help
# ---------------------------------------------------------------------------

print_help() {
    cat <<'EOF'
Usage:
    splice [OPTIONS] FILE1 FILE2

Compare two files and interactively construct a splice.

Options:
    -h, --help       Show this help
    -p, --preview    Preview the resulting file 
    -v, --verbose    Show additional information
    -V, --verify     Verify a written output file at program close
    -b, --backup     Automatically create a backup for destructive edits
    -a, --ask        Ask more questions and give more prompt options
    -A, --auto       Automatically resolve eligible regions
    -y, --yeet       Automatically confirm edits

Examples:
    splice file1.txt file2.txt
    splice -v file1.txt file2.txt
    splice --preview file1.txt file2.txt
    splice -av file1.txt file2.txt
EOF
}

# ---------------------------------------------------------------------------
# Argument parsing/Commands
# ---------------------------------------------------------------------------

parse_args() {
    while getopts ":hpvVbaAy-" opt; do
        case "$opt" in
        h)
            print_help
            exit 0
            ;;
        p)
            PREVIEW=true
            ;;
        v)
            VERBOSE=true
            ;;
        V)
            VERIFY=true
            ;;
        b)
            BACKUP=true
            ;;
        a)
            ASK=true
            ;;
        A)
            AUTO=true
            ;;
        y)
            YEET=true
            ;;
        -)
            case "$OPTARG" in
            help)
                print_help
                exit 0
                ;;
            preview)
                PREVIEW=true
                ;;
            verbose)
                VERBOSE=true
                ;;
            verify)
                VERIFY=true
                ;;
            backup)
                BACKUP=true
                ;;
            ask)
                ASK=true
                ;;
            auto)
                AUTO=true
                ;;
            yeet)
                YEET=true
                ;;
            *)
                die "Unknown option: --$OPTARG."
                ;;
            esac
            ;;
        :)
            die "Option -$OPTARG requires an argument."
            ;;
        \?)
            die "Unknown option: -$OPTARG."
            ;;
        esac
    done

    shift $((OPTIND - 1))

    (($# == 2)) ||
        die "Expected exactly two files."

    SOURCE_A=$1
    SOURCE_B=$2

}

cancel() {
    printf 'splice: Cancelled.\n'
    exit 0
}

# ---------------------------------------------------------------------------
# Validation
# ---------------------------------------------------------------------------

validate_files() {
    [[ -f "$SOURCE_A" ]] ||
        die "Not a regular file: $SOURCE_A."
    [[ -f "$SOURCE_B" ]] ||
        die "Not a regular file: $SOURCE_B."
    [[ -r "$SOURCE_A" ]] ||
        die "Cannot read: $SOURCE_A."
    [[ -r "$SOURCE_B" ]] ||
        die "Cannot read: $SOURCE_B."
    SOURCE_A_PARENT=$(dirname -- "$SOURCE_A")
    SOURCE_B_PARENT=$(dirname -- "$SOURCE_B")
}

validate_operation() {
    local type=$1
    local operation=$2

    case "$type:$operation" in
    insert:delete)
        warn "Cannot delete an inserted region."
        return 1
        ;;
    delete:insert)
        warn "Cannot insert into a deleted region."
        return 1
        ;;
    esac

    return 0
}

is_source_destination() {
    [[ "$PROMPT_DESTINATION" == "$SOURCE_A" ||
        "$PROMPT_DESTINATION" == "$SOURCE_B" ]]
}

verify_output() {
    printf '\n'
    printf '=======================================\n'
    printf 'Output File: %s\n' "$PROMPT_DESTINATION"
    printf '=======================================\n'
    ls -la -- "$PROMPT_DESTINATION"
    printf '\n'
    cat -- "$PROMPT_DESTINATION"
}

# ---------------------------------------------------------------------------
# Analysis
# ---------------------------------------------------------------------------

parse_diff_regions() {
    awk '
    BEGIN {
        region = 0
    }

    /^[0-9]/ && /[cad]/ {
        command = $0

        command ~ /c/ && split(command, parts, "c") && (type = "replace")
        command ~ /a/ && split(command, parts, "a") && (type = "insert")
        command ~ /d/ && split(command, parts, "d") && (type = "delete")

        split(parts[1], a, ",")
        split(parts[2], b, ",")

        a_start = a[1]
        a_end = (a[2] != "" ? a[2] : a[1])

        b_start = b[1]
        b_end = (b[2] != "" ? b[2] : b[1])

        region++
        print region "|" type "|" a_start "|" a_end "|" b_start "|" b_end
    }
    ' "$DIFF_FILE" >"$REGION_FILE"
}

summarize_regions() {
    REGION_COUNT=$(wc -l <"$REGION_FILE")
    INSERT_COUNT=$(grep -c '^[0-9]\+|insert|' "$REGION_FILE")
    DELETE_COUNT=$(grep -c '^[0-9]\+|delete|' "$REGION_FILE")
    REPLACE_COUNT=$(grep -c '^[0-9]\+|replace|' "$REGION_FILE")
}

show_region() {
    local region_id=$1
    local type=$2
    local a_start=$3
    local a_end=$4
    local b_start=$5
    local b_end=$6

    printf '\n'
    printf '%s\n' '----------------------------------------'
    printf 'Region %s of %s\n' "$region_id" "$REGION_COUNT"
    printf 'Type: %s\n' "$type"
    printf '%s\n' '----------------------------------------'

    case "$type" in
    insert)
        printf '\nFile A:\n'
        printf '  Insert after line %s\n' "$a_start"

        printf '\nFile B (lines %s-%s):\n' "$b_start" "$b_end"
        sed -n "${b_start},${b_end}p" "$SOURCE_B"
        ;;

    delete)
        printf '\nFile A (lines %s-%s):\n' "$a_start" "$a_end"
        sed -n "${a_start},${a_end}p" "$SOURCE_A"

        printf '\nFile B:\n'
        printf '  Deletion region: after line %s\n' "$b_start"
        ;;

    replace)
        printf '\nFile A (lines %s-%s):\n' "$a_start" "$a_end"
        sed -n "${a_start},${a_end}p" "$SOURCE_A"

        printf '\nFile B (lines %s-%s):\n' "$b_start" "$b_end"
        sed -n "${b_start},${b_end}p" "$SOURCE_B"
        ;;
    esac
}

print_summary() {
    printf '\n'
    printf 'Summary:\n'

    "$FILES_DIFFER" &&
        printf 'Files differ.\n'

    "$FILES_DIFFER" ||
        printf 'Files are identical.\n'

    printf '  Differences:        %s\n' "$REGION_COUNT"
    printf '  Insertions:         %s\n' "$INSERT_COUNT"
    printf '  Deletions:          %s\n' "$DELETE_COUNT"
    printf '  Replacements:       %s\n' "$REPLACE_COUNT"
}

analyze_files() {
    LINES_A=$(wc -l <"$SOURCE_A")
    LINES_B=$(wc -l <"$SOURCE_B")

    DIFF_FILE=$(mktemp) ||
        die "Failed to create temporary diff file."
    REGION_FILE=$(mktemp) ||
        die "Failed to create temporary region file."
    PLAN_FILE=$(mktemp) ||
        die "Failed to create temporary plan file."

    local status=0

    diff -- "$SOURCE_A" "$SOURCE_B" >"$DIFF_FILE" ||
        status=$?
    debug "Diff status: $status"
    debug "Lines: A=$LINES_A B=$LINES_B"

    ((status > 1)) && die "Failed diff; status $status. "
    ((status == 1)) && FILES_DIFFER=true

    parse_diff_regions
    summarize_regions
    debug "Parsed regions: $(wc -l <"$REGION_FILE")"

    "$ASK" && ask_context
    "$ASK" && ask_verbose
    "$VERBOSE" && {
        printf '\nRaw diff:\n'
        cat "$DIFF_FILE"
        printf '\nParsed regions:\n'
        cat "$REGION_FILE"
    }

}

# ---------------------------------------------------------------------------
# Edit Files
# ---------------------------------------------------------------------------

ask_include() {
    while :; do
        "$YEET" && {
            PROMPT_INCLUDE=true
            return 0
        }

        printf 'Include this region? [y/N/q]: '
        read -r answer </dev/tty || cancel

        case "$answer" in
        y | Y | yes | YES)
            PROMPT_INCLUDE=true
            return
            ;;
        n | N | no | NO | "")
            PROMPT_INCLUDE=false
            return
            ;;
        q | Q | quit | QUIT)
            cancel
            ;;
        *)
            warn "Please enter y, n, or q."
            ;;
        esac
    done
}

make_merged_path() {
    local source=$1
    local filename
    local stem
    local extension
    local timestamp

    local output_dir="$XDG_STATE_HOME/splice"

    mkdir -p -- "$output_dir" ||
        die "Failed to create output directory: $output_dir."

    filename=$(basename -- "$source")
    stem=${filename%.*}
    extension=${filename##*.}

    timestamp=$(date '+%Y%m%d_%H%M%S')

    [[ "$filename" == "$extension" ]] &&
        { printf '%s/%s_merged_%s\n' \
            "$XDG_STATE_HOME/splice" \
            "$filename" \
            "$timestamp"; } ||
        { printf '%s/%s_merged_%s.%s\n' \
            "$XDG_STATE_HOME/splice" \
            "$stem" \
            "$timestamp" \
            "$extension"; }
}

ask_output_file() {
    DEFAULT_OUT_PATH="$XDG_STATE_HOME/splice/"

    while :; do
        printf 'Merge will be written to "%s". Copy to source directory? [a/b/N/q]: ' \
            "$DEFAULT_OUT_PATH"
        read -r copy_out </dev/tty || cancel

        case "$copy_out" in
        a | A)
            PROMPT_COPY_OUTA=true
            PROMPT_COPY_OUTB=false
            return
            ;;
        b | B)
            PROMPT_COPY_OUTA=false
            PROMPT_COPY_OUTB=true
            return
            ;;
        ab | AB | aB | Ab)
            PROMPT_COPY_OUTA=true
            PROMPT_COPY_OUTB=true
            return
            ;;
        n | N | no | NO | "")
            PROMPT_COPY_OUTA=false
            PROMPT_COPY_OUTB=false
            return
            ;;
        q | Q | quit | QUIT)
            cancel
            ;;
        *)
            warn "Please enter a, b, ab, n, or q."
            ;;
        esac
    done
}

ask_destination() {
    while :; do
        printf '%s\n' \
            'Write destination [a/b/m]?' \
            '' \
            "a) File A: $SOURCE_A" \
            "b) File B: $SOURCE_B" \
            'm) Create new merged file'
        read -r destination </dev/tty || cancel

        case "$destination" in
        a | A)
            PROMPT_DESTINATION="$SOURCE_A"
            REGION_SOURCE="$SOURCE_B"
            return
            ;;
        b | B)
            PROMPT_DESTINATION="$SOURCE_B"
            REGION_SOURCE="$SOURCE_A"
            return
            ;;
        m | M | merge | MERGE | merged | MERGED)
            MERGED_FILE=$(make_merged_path "$SOURCE_A") ||
                die "Failed to generate merged file path."
            cp -- "$SOURCE_A" "$MERGED_FILE" ||
                die "Failed to initialize merged file."
            PROMPT_DESTINATION="$MERGED_FILE"
            REGION_SOURCE="$SOURCE_B"
            MERGE_MODE=true
            ask_output_file
            return
            ;;
        *)
            warn "Invalid selection."
            ;;
        esac
    done
}

ask_operation() {
    local region_type=$1

    while :; do
        "$AUTO" && {
            PROMPT_OPERATION=$region_type
            return 0
        }

        printf '%s\n' \
            'Operation [r/i/d]?' \
            '' \
            'r) Replace' \
            'i) Insert' \
            'd) Delete'
        read -r operation </dev/tty || cancel

        case "$operation" in
        r | R | replace | REPLACE)
            PROMPT_OPERATION=replace
            return
            ;;
        i | I | insert | INSERT)
            PROMPT_OPERATION=insert
            return
            ;;
        d | D | delete | DELETE)
            PROMPT_OPERATION=delete
            return
            ;;
        *)
            warn "Invalid selection."
            ;;
        esac
    done
}

parse_line_range() {
    local input=$1
    local start
    local end

    [[ "$input" =~ ^([0-9]+)$ ]] &&
        {
            start=${BASH_REMATCH[1]}
            end=$start
        } ||
        [[ "$input" =~ ^([0-9]+),([0-9]+)$ ]] &&
        {
            start=${BASH_REMATCH[1]}
            end=${BASH_REMATCH[2]}
        } || { return 1; }

    ((start > 0)) || return 1
    ((end >= start)) || return 1

    printf '%s|%s\n' "$start" "$end"
}

preview_insert() {
    local destination=$1
    local source=$2
    local insert_line=$3
    local source_start=$4
    local source_end=$5
    local context_before=3
    local context_after=3
    local destination_lines
    local before_start
    local after_end

    destination_lines=$(wc -l <"$destination")

    before_start=$((insert_line - context_before))
    ((before_start < 1)) && before_start=1

    after_end=$((insert_line + context_after - 1))
    ((after_end > destination_lines)) && after_end=$destination_lines

    printf '\n'
    printf 'Insertion preview:\n'
    printf '%s\n\n' '--------------------'

    ((insert_line > 1)) &&
        sed -n "${before_start},$((insert_line - 1))p" "$destination"
    sed -n "${source_start},${source_end}p" "$source" |
        sed 's/^/>>> /'
    ((insert_line <= destination_lines)) &&
        sed -n "${insert_line},${after_end}p" "$destination"

    printf '%s\n' '--------------------'
}

preview_delete() {
    local destination=$1
    local delete_start=$2
    local delete_end=$3
    local context_before=3
    local context_after=3
    local destination_lines
    local before_start
    local after_end

    destination_lines=$(wc -l <"$destination")

    before_start=$((delete_start - context_before))
    ((before_start < 1)) && before_start=1

    after_end=$((delete_end + context_after - 1))
    ((after_end > destination_lines)) && after_end=$destination_lines

    printf '\n'
    printf 'Deletion preview:\n'
    printf '%s\n\n' '--------------------'

    ((delete_start > 1)) &&
        sed -n "${before_start},$((delete_start - 1))p" "$destination"
    sed -n "${delete_start},${delete_end}p" "$destination" |
        sed 's/^/>>> /'
    ((delete_end < destination_lines)) &&
        sed -n "$((delete_end + 1)),${after_end}p" "$destination"

    printf '%s\n' '--------------------'
}

preview_replace() {
    local destination=$1
    local source=$2
    local replace_start=$3
    local replace_end=$4
    local source_start=$5
    local source_end=$6
    local context_before=3
    local context_after=3
    local destination_lines
    local before_start
    local after_end

    destination_lines=$(wc -l <"$destination")

    before_start=$((replace_start - context_before))
    ((before_start < 1)) && before_start=1

    after_end=$((replace_end + context_after))
    ((after_end > destination_lines)) && after_end=$destination_lines

    printf '\n'
    printf 'Replacement preview:\n'
    printf '%s\n\n' '--------------------'

    ((replace_start > 1)) &&
        sed -n "${before_start},$((replace_start - 1))p" "$destination"
    sed -n "${source_start},${source_end}p" "$source" |
        sed 's/^/>>> /'
    ((replace_end < destination_lines)) &&
        sed -n "$((replace_end + 1)),${after_end}p" "$destination"

    printf '%s\n' '--------------------'
}

ask_insert_location() {
    local region_id=$1
    local a_start=$2
    local a_end=$3
    local b_start=$4
    local b_end=$5
    local suggested_line
    local source_start
    local source_end

    [[ "$REGION_SOURCE" == "$SOURCE_B" ]] &&
        {
            suggested_line=$((a_start + 1))
            source_start=$b_start
            source_end=$b_end
        } ||
        {
            suggested_line=$((b_start + 1))
            source_start=$a_start
            source_end=$a_end
        }

    debug "Insert region $region_id:"
    debug "  Destination: $PROMPT_DESTINATION"
    debug "  Source:      $REGION_SOURCE"
    debug "  A range:     $a_start-$a_end"
    debug "  B range:     $b_start-$b_end"
    debug "  Suggested:   before line $suggested_line"
    debug "  Source range: $source_start-$source_end"

    while :; do
        printf '\n'
        printf 'Region %s\n' "$region_id"
        printf 'Suggested insertion point: before line %s\n' "$suggested_line"
        "$AUTO" &&
            {
                PROMPT_INSERT_LINE=$suggested_line
                PROMPT_SOURCE_START=$source_start
                PROMPT_SOURCE_END=$source_end
                return 0
            } ||
            {
                printf 'Press Enter to use the suggested location.\n'
                printf 'Enter a line number for another location.\n'
                printf '[q] Cancel\n'
                printf '> '
            }

        read -r insert_line </dev/tty || cancel

        case "$insert_line" in
        "")
            PROMPT_INSERT_LINE=$suggested_line
            PROMPT_SOURCE_START=$source_start
            PROMPT_SOURCE_END=$source_end
            ;;
        q | Q | quit | QUIT)
            cancel
            ;;
        *)
            [[ "$insert_line" =~ ^[0-9]+$ ]] && ((insert_line > 0)) &&
                {
                    PROMPT_INSERT_LINE=$insert_line
                    PROMPT_SOURCE_START=$source_start
                    PROMPT_SOURCE_END=$source_end
                } ||
                {
                    warn "Please enter a positive line number, Enter, or q."
                    continue
                }
            ;;
        esac

        preview_insert \
            "$PROMPT_DESTINATION" \
            "$REGION_SOURCE" \
            "$PROMPT_INSERT_LINE" \
            "$PROMPT_SOURCE_START" \
            "$PROMPT_SOURCE_END"

        while :; do
            printf '\n'
            printf 'Use this insertion point? [Y/n/q]: '
            read -r confirm </dev/tty || cancel

            case "$confirm" in
            "" | y | Y | yes | YES)
                return
                ;;
            n | N | no | NO)
                break
                ;;
            q | Q | quit | QUIT)
                cancel
                ;;
            *)
                warn "Please enter y, n, or q."
                ;;
            esac
        done
    done
}

ask_delete_location() {
    local region_id=$1
    local a_start=$2
    local a_end=$3
    local b_start=$4
    local b_end=$5
    local context_before=3
    local context_after=3
    local destination_lines
    local before_start
    local after_end
    local delete_start
    local delete_end

    [[ "$REGION_SOURCE" == "$SOURCE_B" ]] &&
        {
            delete_start=$a_start
            delete_end=$a_end
        } ||
        {
            delete_start=$b_start
            delete_end=$b_end
        }

    while :; do
        printf '\n'
        printf 'Region %s\n' "$region_id"
        printf 'Suggested deletion: lines %s-%s\n' \
            "$delete_start" "$delete_end"
        "$AUTO" &&
            {
                PROMPT_DELETE_START=$delete_start
                PROMPT_DELETE_END=$delete_end
                return 0
            } ||
            {
                printf 'Press Enter to use the suggested region.\n'
                printf 'Enter a line number to choose another starting line.\n'
                printf '[q] Cancel\n'
                printf '> '
            }

        read -r delete_line </dev/tty || cancel

        case "$delete_line" in
        "")
            PROMPT_DELETE_START=$delete_start
            PROMPT_DELETE_END=$delete_end
            PROMPT_SOURCE_START=0
            PROMPT_SOURCE_END=0
            ;;
        q | Q | quit | QUIT)
            cancel
            ;;
        *)
            local range
            range=$(parse_line_range "$delete_line") ||
                {
                    warn "Please enter a positive line number (or range), Enter, or q."
                    continue
                }

            IFS='|' read -r \
                PROMPT_DELETE_START \
                PROMPT_DELETE_END <<<"$range"
            PROMPT_SOURCE_START=0
            PROMPT_SOURCE_END=0
            ;;
        esac

        preview_delete \
            "$PROMPT_DESTINATION" \
            "$PROMPT_DELETE_START" \
            "$PROMPT_DELETE_END"

        while :; do
            printf 'Use this deletion region? [Y/n/q]: '
            read -r confirm </dev/tty || cancel

            case "$confirm" in
            "" | y | Y | yes | YES)
                return
                ;;
            n | N | no | NO)
                break
                ;;
            q | Q | quit | QUIT)
                cancel
                ;;
            *)
                warn "Please enter y, n, or q."
                ;;
            esac
        done
    done
}

ask_replace_location() {
    local region_id=$1
    local a_start=$2
    local a_end=$3
    local b_start=$4
    local b_end=$5
    local context_before=3
    local context_after=3
    local destination_lines
    local before_start
    local after_end
    local source_start
    local source_end
    local replace_start
    local replace_end

    [[ "$REGION_SOURCE" == "$SOURCE_B" ]] &&
        {
            replace_start=$a_start
            replace_end=$a_end
            source_start=$b_start
            source_end=$b_end
        } ||
        {
            replace_start=$b_start
            replace_end=$b_end
            source_start=$a_start
            source_end=$a_end
        }

    while :; do
        printf '\n'
        printf 'Region %s\n' "$region_id"
        printf 'Suggested replacement: lines %s-%s\n' \
            "$replace_start" "$replace_end"
        "$AUTO" &&
            {
                PROMPT_REPLACE_START=$replace_start
                PROMPT_REPLACE_END=$replace_end
                PROMPT_SOURCE_START=$source_start
                PROMPT_SOURCE_END=$source_end
                return 0
            } ||
            {
                printf 'Press Enter to use the suggested region.\n'
                printf 'Enter a line number to choose another starting line.\n'
                printf '[q] Cancel\n'
                printf '> '
            }

        read -r replace_line </dev/tty || cancel

        case "$replace_line" in
        "")
            PROMPT_REPLACE_START=$replace_start
            PROMPT_REPLACE_END=$replace_end
            PROMPT_SOURCE_START=$source_start
            PROMPT_SOURCE_END=$source_end
            ;;
        q | Q | quit | QUIT)
            cancel
            ;;
        *)
            local range
            range=$(parse_line_range "$replace_line") ||
                {
                    warn "Please enter a positive line number (or range), Enter, or q."
                    continue
                }

            IFS='|' read -r \
                PROMPT_REPLACE_START \
                PROMPT_REPLACE_END <<<"$range"
            PROMPT_SOURCE_START=$source_start
            PROMPT_SOURCE_END=$source_end
            ;;
        esac

        preview_replace \
            "$PROMPT_DESTINATION" \
            "$REGION_SOURCE" \
            "$PROMPT_REPLACE_START" \
            "$PROMPT_REPLACE_END" \
            "$source_start" \
            "$source_end"

        while :; do
            printf 'Use this replacement region? [Y/n/q]: '
            read -r confirm </dev/tty || cancel

            case "$confirm" in
            "" | y | Y | yes | YES)
                return
                ;;
            n | N | no | NO)
                break
                ;;
            q | Q | quit | QUIT)
                cancel
                ;;
            *)
                warn "Please enter y, n, or q."
                ;;
            esac
        done
    done
}

append_plan() {
    printf 'Planning region...\n'

    printf '%s|%s|%s|%s|%s|%s|%s\n' \
        "$PLAN_OPERATION" \
        "$PLAN_DESTINATION" \
        "$PLAN_SOURCE" \
        "$PLAN_DEST_START" \
        "$PLAN_DEST_END" \
        "$PLAN_SOURCE_START" \
        "$PLAN_SOURCE_END" \
        >>"$PLAN_FILE"
    debug \
    "EXEC $op | dest=$destination:$dest_start-$dest_end | source=$source:$source_start-$source_end"
}

show_preview() {
    local file=$1

    printf '\n'
    printf '%s\n' '========================================'
    printf '%s\n' 'Edit Plan:'
    printf '%s\n' '========================================'
    printf '\n'

    cat -- "$PLAN_FILE"

    printf '%s\n' '========================================'
    printf '%s\n' 'Planned Result:'
    printf '%s\n' '========================================'

    cat -- "$file"

    printf '\n'
    printf '%s\n' '========================================'
}

execute_insert() {
    local destination=$1
    local source=$2
    local insert_line=$3
    local source_start=$4
    local source_end=$5

    ((insert_line > 1)) &&
        sed -n "1,$((insert_line - 1))p" "$destination"
    sed -n "${source_start},${source_end}p" "$source"
    sed -n "${insert_line},\$p" "$destination"
}

execute_delete() {
    local destination=$1
    local delete_start=$2
    local delete_end=$3

    ((delete_start > 1)) &&
        sed -n "1,$((delete_start - 1))p" "$destination"
    sed -n "$((delete_end + 1)),\$p" "$destination"
}

execute_replace() {
    local destination=$1
    local source=$2
    local replace_start=$3
    local replace_end=$4
    local source_start=$5
    local source_end=$6

    ((replace_start > 1)) &&
        sed -n "1,$((replace_start - 1))p" "$destination"
    sed -n "${source_start},${source_end}p" "$source"
    sed -n "$((replace_end + 1)),\$p" "$destination"
}

execute_plan() {
    local input=$1
    local output=$2
    local current
    local next

    current=$(mktemp) || die "Failed to create execution temporary file."
    TMP_FILES+=("$current")
    debug "Execution temporary: $current"

    cp -- "$input" "$current" ||
        die "Failed to initialize execution file."

    while IFS='|' read -r op destination source \
        dest_start dest_end source_start source_end; do

        next=$(mktemp) || die "Failed to create execution temporary file."
        TMP_FILES+=("$next")
        debug "Execution temporary: $next"

        case "$op" in
        insert)
            execute_insert \
                "$current" \
                "$source" \
                "$dest_start" \
                "$source_start" \
                "$source_end" \
                >"$next"
            ;;

        delete)
            execute_delete \
                "$current" \
                "$dest_start" \
                "$dest_end" \
                >"$next"
            ;;

        replace)
            execute_replace \
                "$current" \
                "$source" \
                "$dest_start" \
                "$dest_end" \
                "$source_start" \
                "$source_end" \
                >"$next"
            ;;

        *)
            rm -f -- "$next" "$current"
            die "Unknown plan operation: $op."
            ;;
        esac
        debug \
            "EXEC $op | dest=$destination:$dest_start-$dest_end | source=$source:$source_start-$source_end"

        rm -f -- "$current" ||
            die "Failed to remove execution temporary file."

        current=$next
    done < <(tac "$PLAN_FILE")

    mv -- "$current" "$output" ||
        die "Failed to write execution result."
}

confirm_plan() {
    local tmp

    tmp=$(mktemp) || die "Failed to create temporary preview file."
    TMP_FILES+=("$tmp")

    debug "Generating planned result: destination=$PROMPT_DESTINATION"
    execute_plan "$PROMPT_DESTINATION" "$tmp"

    show_preview "$tmp"
    "$PREVIEW" && {
        rm -f -- "$tmp"
        return 0
    }

    while :; do
        "$YEET" &&
            {
                "$BACKUP" && is_source_destination && {backup_file || die "Failed to create backup." ;}
                debug "Backup created: $BACKUP_FILE"
                printf '\n'
                debug "Committing planned result to $PROMPT_DESTINATION"
                printf 'Write successful.\n'
                printf '  File: %s\n' "$PROMPT_DESTINATION"
                return 0
            }

        printf 'Write these edits? [Y/n/q]: '
        read -r confirm </dev/tty || {
            rm -f -- "$tmp"
            cancel
        }

        case "$confirm" in
        "" | y | Y | yes | YES)
            "$BACKUP" && is_source_destination && { backup_file || die "Failed to create backup." ;}
            debug "Backup created: $BACKUP_FILE"
            mv -- "$tmp" "$PROMPT_DESTINATION" ||
                die "Failed to commit planned edits."
            printf '\n'
            debug "Committing planned result to $PROMPT_DESTINATION"
            printf 'Write successful.\n'
            printf '  File: %s\n' "$PROMPT_DESTINATION"

            return
            ;;
        n | N | no | NO)
            rm -f -- "$tmp"
            return
            ;;
        q | Q | quit | QUIT)
            rm -f -- "$tmp"
            cancel
            ;;
        *)
            warn "Please enter y, n, or q."
            ;;
        esac
    done
}

plan_edits() {
    ask_destination

    while IFS='|' read -r region_id type a_start a_end b_start b_end; do
        show_region \
            "$region_id" \
            "$type" \
            "$a_start" \
            "$a_end" \
            "$b_start" \
            "$b_end"

        ask_include

        "$PROMPT_INCLUDE" || {
            debug "Region $region_id skipped."
            continue ;}
        ask_operation "$type"
        validate_operation "$type" "$PROMPT_OPERATION" ||
            { debug "Region $region_id rejected operation: $PROMPT_OPERATION"
              continue ;}
        case "$PROMPT_OPERATION" in
        insert)
            ask_insert_location \
                "$region_id" \
                "$a_start" \
                "$a_end" \
                "$b_start" \
                "$b_end"
            ;;
        delete)
            ask_delete_location \
                "$region_id" \
                "$a_start" \
                "$a_end" \
                "$b_start" \
                "$b_end"
            ;;
        replace)
            ask_replace_location \
                "$region_id" \
                "$a_start" \
                "$a_end" \
                "$b_start" \
                "$b_end"
            ;;
        esac

        PLAN_OPERATION=$PROMPT_OPERATION
        PLAN_DESTINATION=$PROMPT_DESTINATION
        PLAN_SOURCE=$REGION_SOURCE

        case "$PROMPT_OPERATION" in
        insert)
            PLAN_DEST_START=$PROMPT_INSERT_LINE
            PLAN_DEST_END=$PROMPT_INSERT_LINE
            ;;
        delete)
            PLAN_DEST_START=$PROMPT_DELETE_START
            PLAN_DEST_END=$PROMPT_DELETE_END
            ;;
        replace)
            PLAN_DEST_START=$PROMPT_REPLACE_START
            PLAN_DEST_END=$PROMPT_REPLACE_END
            ;;
        esac

        PLAN_SOURCE_START=$PROMPT_SOURCE_START
        PLAN_SOURCE_END=$PROMPT_SOURCE_END

        append_plan

    done <"$REGION_FILE"
}

copy_merge_output() {
    "$MERGE_MODE" || return 0

    [[ "$PROMPT_COPY_OUTA" == true ]] &&
        { cp -- \
            "$PROMPT_DESTINATION" \
            "$SOURCE_A_PARENT/$(basename -- "$PROMPT_DESTINATION")" &&
            {   debug "Copying merged output to File A directory"
                printf '\n'
                printf 'Copy successful: %s\n' \
                    "$SOURCE_A_PARENT/$(basename -- "$PROMPT_DESTINATION")"
            } ||
            { die "Failed to copy merged file to File A directory."; }; }

    [[ "$PROMPT_COPY_OUTB" == true ]] &&
        { cp -- \
            "$PROMPT_DESTINATION" \
            "$SOURCE_B_PARENT/$(basename -- "$PROMPT_DESTINATION")" &&
            {   debug "Copying merged output to File A directory"
                printf '\n'
                printf 'Copy successful: %s\n' \
                    "$SOURCE_B_PARENT/$(basename -- "$PROMPT_DESTINATION")"
            } ||
            { die "Failed to copy merged file to File B directory."; }; }
}

# ---------------------------------------------------------------------------
# Clean
# ---------------------------------------------------------------------------

cleanup() {
    [[ -z "$DIFF_FILE" ]] ||
        rm -f -- "$DIFF_FILE" || :

    [[ -z "$REGION_FILE" ]] ||
        rm -f -- "$REGION_FILE" || :

    [[ -z "$PLAN_FILE" ]] ||
        rm -f -- "$PLAN_FILE" || :

    ((${#TMP_FILES[@]} == 0)) ||
        rm -f -- "${TMP_FILES[@]}" || :
}

# ---------------------------------------------------------------------------
# Main program
# ---------------------------------------------------------------------------

main() {
    parse_args "$@"
    init_log
    validate_files
    analyze_files

    printf 'splice %s\n' "$VERSION"
    printf 'File A: %s (%s lines)\n' "$SOURCE_A" "$LINES_A"
    printf 'File B: %s (%s lines)\n' "$SOURCE_B" "$LINES_B"

    "$VERBOSE" && printf 'Verbose mode: enabled\n'
    "$VERIFY" && printf 'Verify mode: enabled\n'
    "$PREVIEW" && printf 'Preview mode: enabled\n'
    "$BACKUP" && printf 'Backup mode: enabled\n'
    "$ASK" && printf 'Ask mode: enabled\n'
    "$AUTO" && printf 'Auto mode: enabled\n'
    "$YEET" && printf 'Yeet mode: enabled\n'

    print_summary

    plan_edits
    confirm_plan
    copy_merge_output
    "$ASK" && { "$VERIFY" || ask_verify; } || { "$VERIFY" && verify_output; }
}

trap cleanup EXIT
main "$@"
