"""CSC1002 - Console-Based Editor - Assignment 3"""

import re

# Global editor state
editor_lines = [""]
active_row = 0
active_col = 0
row_cursor_on = True
line_cursor_on = False


# ── Display ──────────────────────────────────────────────────────

def highlight_char(char):
    """Wrap a single character with green background escape codes."""
    return "\033[42m" + char + "\033[0m"


def build_prefix(is_active):
    """Return the line prefix based on line cursor state."""
    if not line_cursor_on:
        return ""
    return "*" if is_active else " "


def render_line(text, is_active):
    """Build display string for one line with optional row cursor."""
    prefix = build_prefix(is_active)
    if not text:
        return prefix
    if is_active and row_cursor_on:
        left = text[:active_col]
        mid = highlight_char(text[active_col])
        right = text[active_col + 1:]
        return prefix + left + mid + right
    return prefix + text


def display():
    """Print all editor lines to console."""
    for idx, line in enumerate(editor_lines):
        print(render_line(line, idx == active_row))


# ── Word helpers ─────────────────────────────────────────────────

def find_words(text):
    """Return list of (start, end) index tuples for each word."""
    return [(mat.start(), mat.end() - 1) for mat in re.finditer(r'\S+', text)]


def word_at_pos(text, pos):
    """Return (start, end) of word containing pos, or None."""
    for start, end in find_words(text):
        if start <= pos <= end:
            return (start, end)
    return None


def current_line():
    """Return the current active line text."""
    return editor_lines[active_row]


def set_line(text):
    """Set the current active line text."""
    editor_lines[active_row] = text


def clamp_col():
    """Ensure column cursor is within valid range."""
    global active_col
    line = current_line()
    if not line:
        active_col = 0
    elif active_col >= len(line):
        active_col = len(line) - 1


# ── Cursor motion ────────────────────────────────────────────────

def cmd_h():
    """Move cursor one position to the left."""
    global active_col
    if active_col > 0:
        active_col -= 1


def cmd_l():
    """Move cursor one position to the right."""
    global active_col
    line = current_line()
    if line and active_col < len(line) - 1:
        active_col += 1


def cmd_caret():
    """Move cursor to beginning of the line."""
    global active_col
    active_col = 0


def cmd_dollar():
    """Move cursor to end of the line."""
    global active_col
    active_col = max(len(current_line()) - 1, 0)


def cmd_w():
    """Move cursor forward to beginning of next word."""
    global active_col
    line = current_line()
    if not line:
        return
    for start, end in find_words(line):
        if start > active_col:
            active_col = start
            return
    active_col = len(line) - 1


def cmd_b():
    """Move cursor to beginning of current or previous word."""
    global active_col
    if not current_line():
        return
    active_col = find_db_target()


def cmd_e():
    """Move cursor to end of current or next word."""
    global active_col
    line = current_line()
    if not line:
        return
    for start, end in find_words(line):
        if end > active_col:
            active_col = end
            return
    active_col = len(line) - 1


# ── Text insertion ───────────────────────────────────────────────

def cmd_insert(text):
    """Insert text immediately before the cursor position."""
    line = current_line()
    set_line(line[:active_col] + text + line[active_col:])


def cmd_insert_head(text):
    """Insert text at the beginning of the current line."""
    global active_col
    set_line(text + current_line())
    active_col = 0


def cmd_append(text):
    """Append text immediately after the cursor position."""
    global active_col
    line = current_line()
    if not line:
        set_line(text)
        active_col = len(text) - 1
    else:
        set_line(line[:active_col + 1] + text + line[active_col + 1:])
        active_col = active_col + len(text)


def cmd_append_end(text):
    """Append text at the end of the current line."""
    global active_col
    set_line(current_line() + text)
    active_col = len(current_line()) - 1


# ── Delete commands ──────────────────────────────────────────────

def cmd_x():
    """Delete character at cursor position."""
    line = current_line()
    if not line:
        return
    set_line(line[:active_col] + line[active_col + 1:])
    clamp_col()


def cmd_upper_x():
    """Delete character immediately before cursor position."""
    global active_col
    line = current_line()
    if active_col == 0 or not line:
        return
    set_line(line[:active_col - 1] + line[active_col:])
    active_col -= 1


def find_next_word_pos(col):
    """Find start of next word after col, or line length."""
    line = current_line()
    for start, end in find_words(line):
        if start > col:
            return start
    return len(line)


def cmd_dw():
    """Delete from cursor to start of next word (exclusive)."""
    line = current_line()
    if not line:
        return
    target = find_next_word_pos(active_col)
    set_line(line[:active_col] + line[target:])
    clamp_col()


def find_word_end_pos(col):
    """Find end of word at or after col, or last index."""
    line = current_line()
    for start, end in find_words(line):
        if end >= col:
            return end
    return len(line) - 1


def cmd_de():
    """Delete from cursor to end of current or next word (inclusive)."""
    line = current_line()
    if not line:
        return
    target = find_word_end_pos(active_col)
    set_line(line[:active_col] + line[target + 1:])
    clamp_col()


def find_db_target():
    """Find the target position for delete-backward command."""
    line = current_line()
    cur_word = word_at_pos(line, active_col)
    if cur_word and active_col > cur_word[0]:
        return cur_word[0]
    for start, end in reversed(find_words(line)):
        if start < active_col:
            return start
    return 0


def cmd_db():
    """Delete from cursor backward to beginning of current/prev word."""
    global active_col
    line = current_line()
    if not line:
        return
    target = find_db_target()
    set_line(line[:target] + line[active_col + 1:])
    active_col = target
    clamp_col()


def find_space_range(pos):
    """Find contiguous space range around pos, return (start, end)."""
    line = current_line()
    start, end = pos, pos
    while start > 0 and line[start - 1] == ' ':
        start -= 1
    while end < len(line) - 1 and line[end + 1] == ' ':
        end += 1
    return start, end


def delete_word_at():
    """Delete the word at cursor position and update cursor."""
    global active_col
    line = current_line()
    cur_word = word_at_pos(line, active_col)
    set_line(line[:cur_word[0]] + line[cur_word[1] + 1:])
    active_col = cur_word[0]


def delete_spaces_at():
    """Delete contiguous spaces at cursor and update cursor."""
    global active_col
    line = current_line()
    start, end = find_space_range(active_col)
    set_line(line[:start] + line[end + 1:])
    active_col = start


def cmd_dc():
    """Delete entire word at cursor or all surrounding whitespace."""
    line = current_line()
    if not line:
        return
    if word_at_pos(line, active_col):
        delete_word_at()
    else:
        delete_spaces_at()
    clamp_col()


# ── Swap word commands ───────────────────────────────────────────

def get_word_index(words, cur_word):
    """Find the index of cur_word tuple in the words list."""
    for idx, wrd in enumerate(words):
        if wrd[0] == cur_word[0]:
            return idx
    return None


def get_word_text(words, idx):
    """Extract word text from current line at word index."""
    line = current_line()
    return line[words[idx][0]:words[idx][1] + 1]


def do_swap(words, idx1, idx2):
    """Swap two adjacent words in current line by their indices."""
    line = current_line()
    txt1 = get_word_text(words, idx1)
    txt2 = get_word_text(words, idx2)
    mid_text = line[words[idx1][1] + 1:words[idx2][0]]
    new_line = line[:words[idx1][0]] + txt2 + mid_text + txt1 + line[words[idx2][1] + 1:]
    set_line(new_line)
    adjust_col_after_swap(words[idx1][0], txt1, txt2, mid_text)


def adjust_col_after_swap(pos1, txt1, txt2, mid_text):
    """Adjust cursor column to stay on original word after swap."""
    global active_col
    old_pos2 = pos1 + len(txt1) + len(mid_text)
    if active_col < old_pos2:
        active_col = pos1 + len(txt2) + len(mid_text) + (active_col - pos1)
    else:
        active_col = pos1 + (active_col - old_pos2)


def cmd_sw():
    """Swap word at cursor with the next word."""
    line = current_line()
    cur_word = word_at_pos(line, active_col)
    if not cur_word:
        return
    words = find_words(line)
    idx = get_word_index(words, cur_word)
    if idx is not None and idx < len(words) - 1:
        do_swap(words, idx, idx + 1)


def cmd_sb():
    """Swap word at cursor with the previous word."""
    line = current_line()
    cur_word = word_at_pos(line, active_col)
    if not cur_word:
        return
    words = find_words(line)
    idx = get_word_index(words, cur_word)
    if idx is not None and idx > 0:
        do_swap(words, idx - 1, idx)


# ── Toggle commands ──────────────────────────────────────────────

def cmd_dot():
    """Toggle row cursor display on and off."""
    global row_cursor_on
    row_cursor_on = not row_cursor_on


def cmd_semicolon():
    """Toggle line cursor display on and off."""
    global line_cursor_on
    line_cursor_on = not line_cursor_on


# ── Multi-line commands ──────────────────────────────────────────

def cmd_j():
    """Move row cursor up one line."""
    global active_row
    if active_row > 0:
        active_row -= 1
        clamp_col()


def cmd_k():
    """Move row cursor down one line."""
    global active_row
    if active_row < len(editor_lines) - 1:
        active_row += 1
        clamp_col()


def cmd_o():
    """Insert an empty line below the current line."""
    global active_row, active_col
    editor_lines.insert(active_row + 1, "")
    active_row += 1
    active_col = 0


def cmd_upper_o():
    """Insert an empty line above the current line."""
    global active_col
    editor_lines.insert(active_row, "")
    active_col = 0


def cmd_dd():
    """Delete the entire current line."""
    global active_row, active_col
    if len(editor_lines) == 1:
        editor_lines[0] = ""
        active_col = 0
        return
    editor_lines.pop(active_row)
    if active_row >= len(editor_lines):
        active_row = len(editor_lines) - 1
    clamp_col()


def cmd_line_jump(num):
    """Jump to a specific line number (1-indexed)."""
    global active_row, active_col
    active_row = min(num - 1, len(editor_lines) - 1)
    active_col = 0


def cmd_upper_j():
    """Move the current line up by swapping with line above."""
    global active_row
    if active_row <= 0:
        return
    editor_lines[active_row], editor_lines[active_row - 1] = \
        editor_lines[active_row - 1], editor_lines[active_row]
    active_row -= 1


def cmd_upper_k():
    """Move the current line down by swapping with line below."""
    global active_row
    if active_row >= len(editor_lines) - 1:
        return
    editor_lines[active_row], editor_lines[active_row + 1] = \
        editor_lines[active_row + 1], editor_lines[active_row]
    active_row += 1


# ── View and help ────────────────────────────────────────────────

def cmd_v():
    """View editor content as plain text without any cursor."""
    global row_cursor_on, line_cursor_on
    saved_row, saved_line = row_cursor_on, line_cursor_on
    row_cursor_on, line_cursor_on = False, False
    display()
    row_cursor_on, line_cursor_on = saved_row, saved_line


HELP_TEXT = """? - display this help info
. - toggle row cursor on and off
h - move cursor left
l - move cursor right
^ - move cursor to beginning of the line
$ - move cursor to end of the line
w - move cursor to beginning of next word
b - move cursor to beginning of current or previous word
e - move cursor to end of the word
i - insert <text> before cursor
a - append <text> after cursor
I - insert <text> from beginning
A - append <text> at the end
x - delete character at cursor
X - delete character before cursor
dw - delete to start of next word
de - delete to end of next word
db - delete to start of current or previous word
dc - delete whitespaces or entire word at cursor
sw - swap word at cursor with next word
sb - swap word at cursor with previous word
; - toggle line cursor on and off
j - move cursor up
k - move cursor down
o - insert empty line below
O - insert empty line above
dd - delete line
K - move line down
J - move line up
Line No. - jump to specific line, first character
v - view editor content
q - quit program"""


def cmd_help():
    """Display the help information menu."""
    print(HELP_TEXT)


# ── Command dispatch tables ──────────────────────────────────────

SIMPLE_CMDS = {
    "?": cmd_help, ".": cmd_dot, "h": cmd_h, "l": cmd_l,
    "^": cmd_caret, "$": cmd_dollar, "w": cmd_w, "b": cmd_b,
    "e": cmd_e, "x": cmd_x, "X": cmd_upper_x, ";": cmd_semicolon,
    "j": cmd_j, "k": cmd_k, "o": cmd_o, "O": cmd_upper_o,
    "v": cmd_v, "J": cmd_upper_j, "K": cmd_upper_k,
}

TWO_CHAR_CMDS = {
    "dw": cmd_dw, "de": cmd_de, "db": cmd_db, "dc": cmd_dc,
    "sw": cmd_sw, "sb": cmd_sb, "dd": cmd_dd,
}

TEXT_CMDS = {
    'i': cmd_insert, 'a': cmd_append,
    'I': cmd_insert_head, 'A': cmd_append_end,
}

NO_DISPLAY = {"?", "v"}


# ── Command parsing and execution ────────────────────────────────

def execute_simple(cmd):
    """Execute a simple one-char command and display if needed."""
    SIMPLE_CMDS[cmd]()
    if cmd not in NO_DISPLAY:
        display()
    return True


def execute_two_char(cmd):
    """Execute a two-character command and display result."""
    TWO_CHAR_CMDS[cmd]()
    display()
    return True


def execute_text_cmd(user_input):
    """Execute a text insertion or append command."""
    TEXT_CMDS[user_input[0]](user_input[1:])
    display()
    return True


def try_line_jump(user_input):
    """Try to parse and execute a line jump command."""
    if user_input.isdigit() and int(user_input) > 0:
        cmd_line_jump(int(user_input))
        display()
        return True
    return True


def is_valid_text_cmd(user_input):
    """Check if user input is a valid text insertion command."""
    return len(user_input) >= 2 and user_input[0] in TEXT_CMDS


def parse_and_run(user_input):
    """Parse user input and execute matching command. Return False to quit."""
    if user_input == "q":
        return False
    if user_input in SIMPLE_CMDS:
        return execute_simple(user_input)
    if user_input in TWO_CHAR_CMDS:
        return execute_two_char(user_input)
    if is_valid_text_cmd(user_input):
        return execute_text_cmd(user_input)
    return try_line_jump(user_input)


# ── Main loop ────────────────────────────────────────────────────

def main():
    """Main editor loop: prompt, parse, execute, repeat."""
    while True:
        user_input = input(">")
        if not parse_and_run(user_input):
            break


if __name__ == "__main__":
    main()
