import os
import sys
import subprocess

# Если ОС Windows и модуль curses не установлен – пытаемся установить windows-curses
if os.name == 'nt':
    try:
        import curses
    except ImportError:
        print("Устанавливаем windows-curses, так как вы на Windows и библиотека curses недоступна...")
        subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'windows-curses'])
        import curses
else:
    import curses

import re
import signal

# Пытаемся импортировать pyperclip, если не удалось – работаем без него
try:
    import pyperclip
except ImportError:
    pyperclip = None

##############################
# Editor Settings
##############################

TAB_SPACES = " " * 4  # 4 пробела вместо табуляции

##############################
# Синтаксическая подсветка
##############################

# --- Python ---
PYTHON_KEYWORDS = {
    'False', 'None', 'True', 'and', 'as', 'assert', 'async', 'await',
    'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except',
    'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda',
    'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield'
}
RE_PY_COMMENT = re.compile(r"#.*")
RE_PY_STRING = re.compile(r'(\'[^\']*\'|"[^"]*")')
RE_PY_KEYWORD = re.compile(r'\b(' + '|'.join(PYTHON_KEYWORDS) + r')\b')
RE_PY_NUMBER  = re.compile(r'\b\d+(\.\d+)?\b')

def highlight_python_line(line, colors):
    """Подсветка для Python с устранением наложения токенов."""
    if not line:
        return [(line, colors["default"])]
    priority_default = 5
    attr_array = [(priority_default, colors["default"]) for _ in range(len(line))]
    token_specs = [
        ("comment", RE_PY_COMMENT, 1, colors.get("comment", colors["default"])),
        ("string", RE_PY_STRING, 2, colors.get("string", colors["default"])),
        ("keyword", RE_PY_KEYWORD, 3, colors.get("keyword", colors["default"])),
        ("number", RE_PY_NUMBER, 4, colors.get("number", colors["default"]))
    ]
    for ttype, regex, prio, attr in token_specs:
        for m in regex.finditer(line):
            s, e = m.start(), m.end()
            for i in range(s, min(e, len(line))):
                if prio < attr_array[i][0]:
                    attr_array[i] = (prio, attr)
    segments = []
    current_attr = attr_array[0][1]
    segment_start = 0
    for i in range(1, len(line)):
        if attr_array[i][1] != current_attr:
            segments.append((line[segment_start:i], current_attr))
            segment_start = i
            current_attr = attr_array[i][1]
    segments.append((line[segment_start:], current_attr))
    return segments

# --- JavaScript, C, Java и Markdown ---
def highlight_line_generic(line, comment_re, string_re, keyword_re, number_re, colors):
    if not line:
        return [(line, colors["default"])]
    priority_default = 5
    attr_array = [(priority_default, colors["default"]) for _ in range(len(line))]
    token_specs = [
        ("comment", comment_re, 1, colors.get("comment", colors["default"])),
        ("string", string_re, 2, colors.get("string", colors["default"])),
        ("keyword", keyword_re, 3, colors.get("keyword", colors["default"])),
        ("number", number_re, 4, colors.get("number", colors["default"]))
    ]
    for ttype, regex, prio, attr in token_specs:
        for m in regex.finditer(line):
            s, e = m.start(), m.end()
            for i in range(s, min(e, len(line))):
                if prio < attr_array[i][0]:
                    attr_array[i] = (prio, attr)
    segments = []
    current_attr = attr_array[0][1]
    segment_start = 0
    for i in range(1, len(line)):
        if attr_array[i][1] != current_attr:
            segments.append((line[segment_start:i], current_attr))
            segment_start = i
            current_attr = attr_array[i][1]
    segments.append((line[segment_start:], current_attr))
    return segments

def highlight_js_line(line, colors):
    return highlight_line_generic(
        line,
        re.compile(r"(//.*|/\*.*?\*/)", re.DOTALL),
        re.compile(r'(".*?"|\'.*?\'|`.*?`)', re.DOTALL),
        re.compile(r'\b(' + '|'.join({
            'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger',
            'default', 'delete', 'do', 'else', 'export', 'extends', 'finally', 'for',
            'function', 'if', 'import', 'in', 'instanceof', 'let', 'new', 'return',
            'super', 'switch', 'this', 'throw', 'try', 'typeof', 'var', 'void',
            'while', 'with', 'yield'
        }) + r')\b'),
        re.compile(r'\b\d+(\.\d+)?\b'),
        colors
    )

def highlight_c_line(line, colors):
    return highlight_line_generic(
        line,
        re.compile(r"(//.*|/\*.*?\*/)", re.DOTALL),
        re.compile(r'(".*?"|\'.*?\')', re.DOTALL),
        re.compile(r'\b(' + '|'.join({
            'auto','break','case','char','const','continue','default','do','double',
            'else','enum','extern','float','for','goto','if','inline','int','long',
            'register','restrict','return','short','signed','sizeof','static','struct',
            'switch','typedef','union','unsigned','void','volatile','while'
        }) + r')\b'),
        re.compile(r'\b\d+(\.\d+)?\b'),
        colors
    )

def highlight_java_line(line, colors):
    return highlight_line_generic(
        line,
        re.compile(r"(//.*|/\*.*?\*/)", re.DOTALL),
        re.compile(r'(".*?"|\'.*?\')', re.DOTALL),
        re.compile(r'\b(' + '|'.join({
            'abstract','assert','boolean','break','byte','case','catch','char','class',
            'const','continue','default','do','double','else','enum','extends','final',
            'finally','float','for','goto','if','implements','import','instanceof','int',
            'interface','long','native','new','package','private','protected','public',
            'return','short','static','strictfp','super','switch','synchronized','this',
            'throw','throws','transient','try','void','volatile','while'
        }) + r')\b'),
        re.compile(r'\b\d+(\.\d+)?\b'),
        colors
    )

RE_MD_HEADER = re.compile(r'^(#{1,6})\s*(.*)$')
RE_MD_LINK = re.compile(r'(\[.*?\]\(.*?\))')

def highlight_markdown_line(line, colors):
    segments = []
    m = RE_MD_HEADER.match(line)
    if m:
        header_marks, header_text = m.groups()
        segments.append((header_marks + " ", colors["default"]))
        segments.append((header_text, colors["md_header"]))
        return segments
    parts = []
    last = 0
    for m in RE_MD_LINK.finditer(line):
        start, end = m.span()
        if start > last:
            parts.append((line[last:start], "default"))
        parts.append((line[start:end], "md_link"))
        last = end
    if last < len(line):
        parts.append((line[last:], "default"))
    for text, typ in parts:
        segments.append((text, colors.get(typ, colors["default"])))
    if not segments:
        segments = [(line, colors["default"])]
    return segments

def default_highlight_line(line, colors):
    return [(line, colors["default"])]

HIGHLIGHT_FUNCTIONS = {
    ".py": highlight_python_line,
    ".js": highlight_js_line,
    ".ts": highlight_js_line,
    ".jsx": highlight_js_line,
    ".tsx": highlight_js_line,
    ".c": highlight_c_line,
    ".cpp": highlight_c_line,
    ".cc": highlight_c_line,
    ".cxx": highlight_c_line,
    ".h": highlight_c_line,
    ".hpp": highlight_c_line,
    ".java": highlight_java_line,
    ".md": highlight_markdown_line,
    ".html": default_highlight_line,
    ".htm": default_highlight_line,
    ".css": default_highlight_line,
    ".json": default_highlight_line,
    ".xml": default_highlight_line,
    ".rb": default_highlight_line,
    ".php": default_highlight_line,
    ".go": default_highlight_line,
    ".rs": default_highlight_line,
    ".sh": default_highlight_line,
    ".sql": default_highlight_line,
    ".ini": default_highlight_line,
    ".cfg": default_highlight_line,
    ".yaml": default_highlight_line,
    ".yml": default_highlight_line,
    ".bat": default_highlight_line,
    ".ps1": default_highlight_line,
    ".swift": default_highlight_line,
    ".kt": default_highlight_line,
    ".lua": default_highlight_line,
    ".pl": default_highlight_line,
    ".r": default_highlight_line,
    ".scala": default_highlight_line,
    ".vb": default_highlight_line,
    ".fs": default_highlight_line,
    ".erl": default_highlight_line,
    ".ex": default_highlight_line,
    ".exs": default_highlight_line,
}

##############################
# Другие функции редактора
##############################

def load_file(filename):
    try:
        with open(filename, 'r', encoding='utf-8') as f:
            lines = f.read().splitlines()
    except Exception:
        lines = [""]
    if not lines:
        lines = [""]
    return lines

def save_file(filename, lines):
    with open(filename, 'w', encoding='utf-8') as f:
        f.write("\n".join(lines))

def prompt_user(stdscr, prompt):
    curses.echo()
    height, width = stdscr.getmaxyx()
    stdscr.move(height - 1, 0)
    stdscr.clrtoeol()
    stdscr.addstr(height - 1, 0, prompt)
    stdscr.refresh()
    inp = stdscr.getstr(height - 1, len(prompt), width - len(prompt) - 1)
    curses.noecho()
    try:
        return inp.decode("utf-8").strip()
    except Exception:
        return ""

def prompt_user_cancelable(stdscr, prompt):
    height, width = stdscr.getmaxyx()
    input_str = ""
    pos = len(prompt)
    stdscr.move(height - 1, 0)
    stdscr.clrtoeol()
    stdscr.addstr(height - 1, 0, prompt)
    stdscr.refresh()
    while True:
        ch = stdscr.get_wch()
        if ch == "\x1b":  # ESC
            return None
        elif ch in ("\n", "\r") or (isinstance(ch, int) and ch in (curses.KEY_ENTER, 10, 13)):
            break
        elif ch in ("\x7f", "\b") or (isinstance(ch, int) and ch in (curses.KEY_BACKSPACE, 127, 8)):
            if len(input_str) > 0:
                input_str = input_str[:-1]
                pos -= 1
                stdscr.move(height - 1, pos)
                stdscr.delch(height - 1, pos)
        else:
            if isinstance(ch, str) and ch.isprintable():
                input_str += ch
                stdscr.addstr(height - 1, pos, ch)
                pos += 1
        stdscr.refresh()
    return input_str.strip()

ASCII_ART = r"""
  _    _       _     _       _       
 | |  | |     (_)   | |     | |      
 | |  | |_ __  _ ___| | __ _| |_ ___ 
 | |  | | '_ \| / __| |/ _` | __/ _ \
 | |__| | | | | \__ \ | (_| | ||  __/
  \____/|_| |_|_|___/_|\__,_|\__\___|
                                                   
developed by qwez with ♥️
"""

def draw_welcome(stdscr, colors):
    stdscr.clear()
    height, width = stdscr.getmaxyx()
    ascii_lines = ASCII_ART.strip("\n").splitlines()
    total_lines = len(ascii_lines) + 3
    start_y = max((height - total_lines) // 2, 0)
    for idx, line in enumerate(ascii_lines):
        x = max((width - len(line)) // 2, 0)
        try:
            stdscr.addstr(start_y + idx, x, line, colors["default"])
        except curses.error:
            pass
    prompt_msg = "Press any key to start..."
    try:
        stdscr.addstr(start_y + len(ascii_lines) + 2, max((width - len(prompt_msg)) // 2, 0),
                      prompt_msg, colors["default"])
    except curses.error:
        pass
    stdscr.refresh()
    key = stdscr.get_wch()
    if key == "\x11":  # Ctrl+Q
        curses.endwin()
        sys.exit(0)

##############################
# Функции для выделения
##############################

def get_line_selection_range(line_no, sel_start, sel_end, line_length):
    if sel_start is None or sel_end is None:
        return None
    (sy, sx) = sel_start
    (ey, ex) = sel_end
    if (sy, sx) > (ey, ex):
        sy, sx, ey, ex = ey, ex, sy, sx
    if line_no < sy or line_no > ey:
        return None
    if sy == ey:
        return (min(sx, ex), max(sx, ex))
    if line_no == sy:
        return (sx, line_length)
    elif line_no == ey:
        return (0, ex)
    else:
        return (0, line_length)

def apply_selection_to_segment(text, seg_start, seg_end, sel_start, sel_end, base_attr):
    result = []
    if seg_end <= sel_start or seg_start >= sel_end:
        return [(text, base_attr)]
    if seg_start < sel_start:
        prefix_len = sel_start - seg_start
        result.append((text[:prefix_len], base_attr))
        text = text[prefix_len:]
        seg_start = sel_start
    overlap = min(seg_end, sel_end) - seg_start
    if overlap > 0:
        result.append((text[:overlap], base_attr | curses.A_REVERSE))
        text = text[overlap:]
        seg_start += overlap
    if seg_start < seg_end:
        result.append((text, base_attr))
    return result

def get_selected_text(lines, sel_start, sel_end):
    (sy, sx) = sel_start
    (ey, ex) = sel_end
    if (sy, sx) > (ey, ex):
        sy, sx, ey, ex = ey, ex, sy, sx
    if sy == ey:
        return lines[sy][sx:ex]
    result = []
    result.append(lines[sy][sx:])
    for i in range(sy + 1, ey):
        result.append(lines[i])
    result.append(lines[ey][:ex])
    return "\n".join(result)

def remove_selected_text(lines, sel_start, sel_end):
    (sy, sx) = sel_start
    (ey, ex) = sel_end
    if (sy, sx) > (ey, ex):
        sy, sx, ey, ex = ey, ex, sy, sx
    if sy == ey:
        lines[sy] = lines[sy][:sx] + lines[sy][ex:]
        return lines
    first_part = lines[sy][:sx]
    last_part = lines[ey][ex:]
    new_line = first_part + last_part
    new_lines = lines[:sy] + [new_line] + lines[ey+1:]
    return new_lines

##############################
# Функция для поиска – подсветка найденных вхождений
##############################

def apply_overlays(segment_text, seg_start, seg_end, overlays, current_search_index, base_attr, colors):
    """
    overlays: список кортежей (o_start, o_end, match_index)
    Возвращает список (текст, атрибут) для данного сегмента с наложенной подсветкой.
    """
    result = []
    pos = seg_start
    text = segment_text
    for (o_start, o_end, match_index) in overlays:
        if o_end <= pos or o_start >= seg_end:
            continue
        if pos < o_start:
            length = o_start - pos
            result.append((text[:length], base_attr))
            text = text[length:]
            pos = o_start
        overlap = min(o_end, seg_end) - pos
        if overlap > 0:
            attr = colors["current_search"] if match_index == current_search_index else colors["search"]
            result.append((text[:overlap], attr))
            text = text[overlap:]
            pos += overlap
    if pos < seg_end:
        result.append((text, base_attr))
    return result

##############################
# Отрисовка редактора с номерами строк, выделением и подсветкой результатов поиска
##############################

def draw_editor(stdscr, lines, cursor_y, cursor_x, offset_y, offset_x, filename, colors, sel_start, sel_end,
                search_query=None, search_matches=None, current_search_index=None):
    stdscr.clear()
    height, width = stdscr.getmaxyx()
    ext = os.path.splitext(filename)[1].lower() if filename else ""
    highlighter = HIGHLIGHT_FUNCTIONS.get(ext, default_highlight_line)
    
    line_num_width = len(str(len(lines))) + 2
    
    for i, line in enumerate(lines[offset_y: offset_y + height - 1]):
        actual_line = i + offset_y
        line_number = f"{actual_line+1}".rjust(line_num_width - 1) + " "
        try:
            stdscr.addstr(i, 0, line_number, curses.A_DIM)
        except curses.error:
            pass
        
        segments = highlighter(line, colors)
        x = line_num_width
        sel_range = get_line_selection_range(actual_line, sel_start, sel_end, len(line))
        
        overlays = []
        if search_query and search_matches:
            for idx, match in enumerate(search_matches):
                if match[0] == actual_line:
                    overlays.append((match[1], match[1] + len(search_query), idx))
            overlays.sort(key=lambda t: t[0])
        
        current_index = 0
        for text, attr in segments:
            seg_start = current_index
            seg_end = current_index + len(text)
            current_index = seg_end
            pieces = [(text, attr)]
            if sel_range is not None:
                new_pieces = []
                for piece_text, piece_attr in pieces:
                    new_pieces.extend(apply_selection_to_segment(piece_text, seg_start, seg_end, sel_range[0], sel_range[1], piece_attr))
                pieces = new_pieces
            if overlays:
                new_pieces = []
                running_index = seg_start
                for piece_text, piece_attr in pieces:
                    relevant = []
                    for o_start, o_end, match_idx in overlays:
                        if o_end <= running_index or o_start >= seg_end:
                            continue
                        relevant.append((max(o_start, running_index), min(o_end, seg_end), match_idx))
                    if relevant:
                        new_pieces.extend(apply_overlays(piece_text, running_index, seg_end, relevant, current_search_index, piece_attr, colors))
                    else:
                        new_pieces.append((piece_text, piece_attr))
                    running_index += len(piece_text)
                pieces = new_pieces
            for sub_text, sub_attr in pieces:
                if x + len(sub_text) > width:
                    sub_text = sub_text[:max(width - x, 0)]
                try:
                    stdscr.addstr(i, x, sub_text, sub_attr)
                except curses.error:
                    pass
                x += len(sub_text)
                    
    if filename is None:
        status_filename = "untitled (new)"
    elif not os.path.exists(filename):
        status_filename = f"{filename} (new)"
    else:
        status_filename = filename

    status = f"{status_filename} | Ln {cursor_y+1}/{len(lines)} | Col {cursor_x+1} | EDIT"
    try:
        stdscr.addstr(height - 1, 0, status[:width].ljust(width), colors["default"])
    except curses.error:
        pass
    scr_y = cursor_y - offset_y
    line_num_offset = line_num_width
    scr_x = (cursor_x - offset_x) + line_num_offset
    if 0 <= scr_y < height - 1 and 0 <= scr_x < width:
        try:
            stdscr.move(scr_y, scr_x)
        except curses.error:
            pass
    stdscr.refresh()

##############################
# Функция справки (интерактивная подсказка)
##############################

def show_help(stdscr, colors):
    help_lines = [
     "Help - Basic Commands:",
     "",
     "Ctrl+S  - Save file",
     "Ctrl+Q  - Exit editor",
     "Ctrl+Z  - Undo",
     "Ctrl+Y  - Redo",
     "Ctrl+C  - Copy",
     "Ctrl+X  - Cut",
     "Ctrl+V  - Paste",
     "Ctrl+F  - Search",
     "Ctrl+R  - Replace",
     "Arrows  - Move cursor (non-shift cancels selection)",
     "Shift+Arrows - Extend selection",
     "Home/End - Move to start/end of line",
     "PageUp/PageDown - Scroll page",
     "Tab     - Insert indent (4 spaces)",
     "Enter   - New line",
     "F1      - Show help",
     "",
     "Press any key to close help..."
    ]

    height, width = stdscr.getmaxyx()
    win_height = len(help_lines) + 2
    win_width = max(len(line) for line in help_lines) + 4
    start_y = (height - win_height) // 2
    start_x = (width - win_width) // 2
    help_win = curses.newwin(win_height, win_width, start_y, start_x)
    help_win.bkgd(' ', colors["default"])
    help_win.border()
    for idx, line in enumerate(help_lines):
        try:
            help_win.addstr(1 + idx, 2, line, colors["default"])
        except curses.error:
            pass
    help_win.refresh()
    help_win.get_wch()
    help_win.clear()
    stdscr.touchwin()
    stdscr.refresh()

##############################
# Main Editor Function
##############################

def main(stdscr):
    curses.noecho()
    curses.raw()
    stdscr.keypad(True)
    curses.curs_set(1)
    curses.start_color()
    curses.use_default_colors()
    # Если в вашей версии curses не заданы клавиши Shift+стрелок – определяем их сами:
    if not hasattr(curses, 'KEY_SLEFT'):
        curses.KEY_SLEFT = 393
    if not hasattr(curses, 'KEY_SRIGHT'):
        curses.KEY_SRIGHT = 402
    if not hasattr(curses, 'KEY_SUP'):
        curses.KEY_SUP = 337
    if not hasattr(curses, 'KEY_SDOWN'):
        curses.KEY_SDOWN = 336

    stdscr.bkgd(' ', curses.A_NORMAL)
    
    signal.signal(signal.SIGINT, lambda sig, frame: None)

    # Инициализация цветовых пар
    curses.init_pair(1, curses.COLOR_WHITE, -1)    # default
    curses.init_pair(2, curses.COLOR_CYAN, -1)       # keyword (жирный)
    curses.init_pair(3, curses.COLOR_YELLOW, -1)     # string
    curses.init_pair(4, curses.COLOR_GREEN, -1)      # comment
    curses.init_pair(5, curses.COLOR_MAGENTA, -1)    # number
    curses.init_pair(6, curses.COLOR_BLUE, -1)       # md_header
    curses.init_pair(7, curses.COLOR_CYAN, -1)       # md_link
    # Цвета для поиска
    curses.init_pair(8, curses.COLOR_BLACK, curses.COLOR_YELLOW)  # подсветка найденного
    curses.init_pair(9, curses.COLOR_WHITE, curses.COLOR_RED)     # текущее найденное (выделенное)

    colors = {
        "default": curses.A_NORMAL,
        "keyword": curses.color_pair(2) | curses.A_BOLD,
        "string": curses.color_pair(3),
        "comment": curses.color_pair(4),
        "number": curses.color_pair(5),
        "md_header": curses.color_pair(6) | curses.A_BOLD,
        "md_link": curses.color_pair(7),
        "search": curses.color_pair(8),
        "current_search": curses.color_pair(9) | curses.A_BOLD
    }

    if len(sys.argv) > 1:
        filename = sys.argv[1]
        lines = load_file(filename)
    else:
        draw_welcome(stdscr, colors)
        filename = None
        lines = [""]

    cursor_y = 0
    cursor_x = 0
    offset_y = 0
    offset_x = 0
    modified = False
    undo_stack = []
    redo_stack = []
    clipboard = ""  # Внутренний буфер

    # Для выделения
    selection_start = None
    selection_end = None

    def get_state():
        return (list(lines), cursor_y, cursor_x, offset_y, offset_x, modified)

    def set_state(state):
        nonlocal lines, cursor_y, cursor_x, offset_y, offset_x, modified
        lines, cursor_y, cursor_x, offset_y, offset_x, modified = state

    def record_undo():
        undo_stack.append(get_state())
        redo_stack.clear()

    def cancel_selection():
        nonlocal selection_start, selection_end
        selection_start = None
        selection_end = None

    # Функция для поиска всех вхождений запроса
    def find_all_occurrences(lines, query):
        matches = []
        for y, line in enumerate(lines):
            start = 0
            while True:
                pos = line.find(query, start)
                if pos == -1:
                    break
                matches.append((y, pos))
                start = pos + 1
        return matches

    height, width = stdscr.getmaxyx()

    # Функция для корректировки горизонтального сдвига, чтобы курсор был виден.
    def adjust_horizontal_offset():
        nonlocal offset_x
        line_num_width = len(str(len(lines))) + 2
        available_width = width - line_num_width
        if cursor_x < offset_x:
            offset_x = cursor_x
        elif cursor_x >= offset_x + available_width:
            offset_x = cursor_x - available_width + 1

    while True:
        draw_editor(stdscr, lines, cursor_y, cursor_x, offset_y, offset_x,
                    filename, colors, selection_start, selection_end)
        try:
            key = stdscr.get_wch()
        except curses.error:
            continue

        # Обработка escape-последовательностей для Shift+стрелок
        if isinstance(key, str) and key.startswith("\x1b["):
            m = re.match(r'\x1b\[(\d+);(\d+)([A-D])', key)
            if m:
                num, modifier, direction = m.groups()
                if modifier == "2":
                    if direction == "A":
                        key = curses.KEY_SUP
                    elif direction == "B":
                        key = curses.KEY_SDOWN
                    elif direction == "C":
                        key = curses.KEY_SRIGHT
                    elif direction == "D":
                        key = curses.KEY_SLEFT

        if isinstance(key, str):
            key_ord = ord(key)
        else:
            key_ord = key

        if key == curses.KEY_RESIZE:
            stdscr.clear()
            offset_y = 0
            offset_x = 0
            continue

        if key == curses.KEY_F1:
            show_help(stdscr, colors)
            continue

        # Выход: Ctrl+Q (код 17 или символ \x11)
        if key_ord == 17:
            if modified:
                stdscr.move(height - 1, 0)
                stdscr.clrtoeol()
                stdscr.addstr(height - 1, 0, "Unsaved changes! Quit without saving? (y/n): ", colors["default"])
                stdscr.refresh()
                try:
                    confirm = stdscr.get_wch()
                except curses.error:
                    continue
                if isinstance(confirm, str):
                    confirm_ord = ord(confirm)
                else:
                    confirm_ord = confirm
                if confirm_ord not in (ord('y'), ord('Y')):
                    continue
            break

        elif key_ord == 19:  # Ctrl+S - Сохранение
            if not filename:
                name = prompt_user_cancelable(stdscr, "Save as: ")
                if name is None or name == "":
                    continue
                filename = name
            try:
                save_file(filename, lines)
                modified = False
                msg = f"Saved: {filename}"
                try:
                    stdscr.addstr(height - 1, 0, msg[:width].ljust(width), colors["default"])
                except curses.error:
                    pass
                stdscr.refresh()
                curses.napms(500)
            except Exception:
                pass

        elif key_ord == 26:  # Ctrl+Z - Undo
            if undo_stack:
                redo_stack.append(get_state())
                set_state(undo_stack.pop())
                cancel_selection()
            else:
                curses.beep()

        elif key_ord == 25:  # Ctrl+Y - Redo
            if redo_stack:
                undo_stack.append(get_state())
                set_state(redo_stack.pop())
                cancel_selection()
            else:
                curses.beep()

        # Режим поиска (Ctrl+F)
        elif key_ord == 6:
            query = prompt_user_cancelable(stdscr, "Find: ")
            if query is None or query == "":
                continue
            matches = find_all_occurrences(lines, query)
            if not matches:
                curses.beep()
                stdscr.move(height - 1, 0)
                stdscr.clrtoeol()
                try:
                    stdscr.addstr(height - 1, 0, "Not found.", colors["default"])
                except curses.error:
                    pass
                stdscr.refresh()
                curses.napms(800)
                continue
            initial_index = 0
            for i, (y, pos) in enumerate(matches):
                if y > cursor_y or (y == cursor_y and pos >= cursor_x):
                    initial_index = i
                    break
            current_search_index = initial_index
            cursor_y, cursor_x = matches[current_search_index]
            while True:
                draw_editor(stdscr, lines, cursor_y, cursor_x, offset_y, offset_x,
                            filename, colors, selection_start, selection_end,
                            search_query=query, search_matches=matches, current_search_index=current_search_index)
                status = f"Search: {query} | Match {current_search_index+1} of {len(matches)} | Use arrow keys to navigate, Esc to exit"
                stdscr.move(height - 1, 0)
                stdscr.clrtoeol()
                try:
                    stdscr.addstr(height - 1, 0, status[:width].ljust(width), colors["default"])
                except curses.error:
                    pass
                stdscr.refresh()
                key2 = stdscr.get_wch()
                if key2 == "\x1b":
                    break
                elif key2 in (curses.KEY_LEFT, curses.KEY_UP):
                    current_search_index = (current_search_index - 1) % len(matches)
                elif key2 in (curses.KEY_RIGHT, curses.KEY_DOWN):
                    current_search_index = (current_search_index + 1) % len(matches)
                cursor_y, cursor_x = matches[current_search_index]
                if cursor_y < offset_y:
                    offset_y = cursor_y
                elif cursor_y >= offset_y + height - 1:
                    offset_y = max(cursor_y - (height - 2), 0)
                adjust_horizontal_offset()
            cancel_selection()

        # Замена (Ctrl+R)
        elif key_ord == 18:
            query = prompt_user_cancelable(stdscr, "Find (for replace): ")
            if query is None or query == "":
                continue
            replacement = prompt_user_cancelable(stdscr, "Replace with: ")
            if replacement is None:
                continue
            record_undo()
            count_replaced = 0
            for i in range(len(lines)):
                occurrences = lines[i].count(query)
                if occurrences > 0:
                    count_replaced += occurrences
                    lines[i] = lines[i].replace(query, replacement)
            modified = True
            cancel_selection()
            msg = f"Replaced {count_replaced} occurrence(s)."
            stdscr.move(height - 1, 0)
            stdscr.clrtoeol()
            try:
                stdscr.addstr(height - 1, 0, msg, colors["default"])
            except curses.error:
                pass
            stdscr.refresh()
            curses.napms(800)

        # Обработка обычных стрелок (без Shift) – всегда сбрасываем выделение
        elif key == curses.KEY_UP:
            cancel_selection()
            if cursor_y > 0:
                cursor_y -= 1
                cursor_x = min(cursor_x, len(lines[cursor_y]))
                if cursor_y < offset_y:
                    offset_y = cursor_y
            else:
                curses.beep()
        elif key == curses.KEY_DOWN:
            cancel_selection()
            if cursor_y < len(lines) - 1:
                cursor_y += 1
                cursor_x = min(cursor_x, len(lines[cursor_y]))
                if cursor_y >= offset_y + height - 1:
                    offset_y = cursor_y - (height - 2)
            else:
                curses.beep()
        elif key == curses.KEY_LEFT:
            cancel_selection()
            if cursor_x > 0:
                cursor_x -= 1
            elif cursor_y > 0:
                cursor_y -= 1
                cursor_x = len(lines[cursor_y])
                if cursor_y < offset_y:
                    offset_y = cursor_y
            else:
                curses.beep()
        elif key == curses.KEY_RIGHT:
            cancel_selection()
            if cursor_x < len(lines[cursor_y]):
                cursor_x += 1
            elif cursor_y < len(lines) - 1:
                cursor_y += 1
                cursor_x = 0
                if cursor_y >= offset_y + height - 1:
                    offset_y = cursor_y - (height - 2)
            else:
                curses.beep()
        elif key == curses.KEY_HOME:
            cancel_selection()
            cursor_x = 0
        elif key == curses.KEY_END:
            cancel_selection()
            cursor_x = len(lines[cursor_y])
        elif key == curses.KEY_PPAGE:  # Page Up
            cancel_selection()
            cursor_y = max(cursor_y - (height - 1), 0)
            cursor_x = min(cursor_x, len(lines[cursor_y]))
            offset_y = max(offset_y - (height - 1), 0)
        elif key == curses.KEY_NPAGE:  # Page Down
            cancel_selection()
            cursor_y = min(cursor_y + (height - 1), len(lines) - 1)
            cursor_x = min(cursor_x, len(lines[cursor_y]))
            offset_y = min(offset_y + (height - 1), max(len(lines) - (height - 1), 0))

        # Обработка Shift+стрелок для выделения
        elif key in (getattr(curses, "KEY_SLEFT", None), "\x1b[1;2D"):
            if selection_start is None:
                selection_start = (cursor_y, cursor_x)
            if cursor_x > 0:
                cursor_x -= 1
            elif cursor_y > 0:
                cursor_y -= 1
                cursor_x = len(lines[cursor_y])
            else:
                curses.beep()
            selection_end = (cursor_y, cursor_x)
        elif key in (getattr(curses, "KEY_SRIGHT", None), "\x1b[1;2C"):
            if selection_start is None:
                selection_start = (cursor_y, cursor_x)
            if cursor_x < len(lines[cursor_y]):
                cursor_x += 1
            elif cursor_y < len(lines) - 1:
                cursor_y += 1
                cursor_x = 0
            else:
                curses.beep()
            selection_end = (cursor_y, cursor_x)
        elif key in (getattr(curses, "KEY_SUP", None), "\x1b[1;2A"):
            if selection_start is None:
                selection_start = (cursor_y, cursor_x)
            if cursor_y > 0:
                cursor_y -= 1
                cursor_x = min(cursor_x, len(lines[cursor_y]))
            else:
                curses.beep()
            selection_end = (cursor_y, cursor_x)
        elif key in (getattr(curses, "KEY_SDOWN", None), 525, "\x1b[1;2B"):
            if selection_start is None:
                selection_start = (cursor_y, cursor_x)
            if cursor_y < len(lines) - 1:
                cursor_y += 1
                cursor_x = min(cursor_x, len(lines[cursor_y]))
            else:
                curses.beep()
            selection_end = (cursor_y, cursor_x)

        # Клавиша Delete (KEY_DC)
        elif key == curses.KEY_DC:
            record_undo()
            if selection_start is not None and selection_end is not None and selection_start != selection_end:
                lines = remove_selected_text(lines, selection_start, selection_end)
                cursor_y, cursor_x = selection_start
                cancel_selection()
                modified = True
            else:
                line = lines[cursor_y]
                if cursor_x < len(line):
                    lines[cursor_y] = line[:cursor_x] + line[cursor_x+1:]
                    modified = True
                elif cursor_y < len(lines) - 1:
                    next_line = lines[cursor_y+1]
                    lines[cursor_y] = line + next_line
                    del lines[cursor_y+1]
                    modified = True
                else:
                    curses.beep()

        # Копирование: Ctrl+C
        elif key_ord == 3:
            if selection_start is not None and selection_end is not None and selection_start != selection_end:
                clipboard = get_selected_text(lines, selection_start, selection_end)
                cancel_selection()
            else:
                clipboard = lines[cursor_y]
            if pyperclip:
                try:
                    pyperclip.copy(clipboard)
                except Exception:
                    pass

        # Вырезание: Ctrl+X
        elif key_ord == 24:
            record_undo()
            if selection_start is not None and selection_end is not None and selection_start != selection_end:
                clipboard = get_selected_text(lines, selection_start, selection_end)
                lines = remove_selected_text(lines, selection_start, selection_end)
                (cursor_y, cursor_x) = selection_start
                cancel_selection()
                modified = True
            else:
                clipboard = lines[cursor_y]
                if len(lines) > 1:
                    del lines[cursor_y]
                    if cursor_y >= len(lines):
                        cursor_y = len(lines) - 1
                    cursor_x = min(cursor_x, len(lines[cursor_y]))
                else:
                    lines[0] = ""
                    cursor_x = 0
                modified = True
            if pyperclip:
                try:
                    pyperclip.copy(clipboard)
                except Exception:
                    pass

        # Вставка: Ctrl+V
        elif key_ord == 22:
            if pyperclip:
                try:
                    clipboard = pyperclip.paste()
                except Exception:
                    pass
            if clipboard:
                record_undo()
                if selection_start is not None and selection_end is not None and selection_start != selection_end:
                    lines = remove_selected_text(lines, selection_start, selection_end)
                    cursor_y, cursor_x = selection_start
                    cancel_selection()
                if "\n" in clipboard:
                    clipboard_lines = clipboard.split("\n")
                else:
                    clipboard_lines = [clipboard]
                if len(clipboard_lines) == 1:
                    line = lines[cursor_y]
                    lines[cursor_y] = line[:cursor_x] + clipboard_lines[0] + line[cursor_x:]
                    cursor_x += len(clipboard_lines[0])
                else:
                    line = lines[cursor_y]
                    before = line[:cursor_x]
                    after = line[cursor_x:]
                    new_lines = [before + clipboard_lines[0]] + clipboard_lines[1:-1] + [clipboard_lines[-1] + after]
                    lines[cursor_y:cursor_y+1] = new_lines
                    cursor_y = cursor_y + len(new_lines) - 1
                    cursor_x = len(clipboard_lines[-1])
                modified = True

        # Backspace
        elif key in (curses.KEY_BACKSPACE, "\x7f", "\b"):
            if selection_start is not None and selection_end is not None and selection_start != selection_end:
                record_undo()
                lines = remove_selected_text(lines, selection_start, selection_end)
                cursor_y, cursor_x = selection_start
                cancel_selection()
                modified = True
            elif cursor_x > 0:
                record_undo()
                line = lines[cursor_y]
                lines[cursor_y] = line[:cursor_x - 1] + line[cursor_x:]
                cursor_x -= 1
                modified = True
                cancel_selection()
            elif cursor_y > 0:
                record_undo()
                prev_line = lines[cursor_y - 1]
                curr_line = lines[cursor_y]
                cursor_x = len(prev_line)
                lines[cursor_y - 1] = prev_line + curr_line
                del lines[cursor_y]
                cursor_y -= 1
                modified = True
                cancel_selection()

        # Enter
        elif key in (curses.KEY_ENTER, "\n", "\r"):
            record_undo()
            line = lines[cursor_y]
            indent_match = re.match(r'(\s*)', line)
            indent = indent_match.group(1) if indent_match else ""
            new_line = indent + line[cursor_x:]
            lines[cursor_y] = line[:cursor_x]
            lines.insert(cursor_y + 1, new_line)
            cursor_y += 1
            cursor_x = len(indent)
            if cursor_y >= offset_y + height - 1:
                offset_y = cursor_y - (height - 2)
            modified = True
            cancel_selection()

        # Tab
        elif key == "\t" or key_ord == 9:
            record_undo()
            line = lines[cursor_y]
            lines[cursor_y] = line[:cursor_x] + TAB_SPACES + line[cursor_x:]
            cursor_x += len(TAB_SPACES)
            modified = True
            cancel_selection()

        # Печатаемые символы
        elif isinstance(key, str) and key.isprintable():
            if selection_start is not None and selection_end is not None and selection_start != selection_end:
                record_undo()
                lines = remove_selected_text(lines, selection_start, selection_end)
                cursor_y, cursor_x = selection_start
                cancel_selection()
                modified = True
            record_undo()
            line = lines[cursor_y]
            lines[cursor_y] = line[:cursor_x] + key + line[cursor_x:]
            cursor_x += 1
            if cursor_x >= offset_x + (width - (len(str(len(lines))) + 2)):
                offset_x = cursor_x - (width - (len(str(len(lines))) + 2)) + 1
            modified = True
            cancel_selection()
        # После обработки каждого ключа корректируем горизонтальный offset
        adjust_horizontal_offset()
    stdscr.clear()
    stdscr.refresh()

def main_wrapper():
    curses.wrapper(main)

if __name__ == '__main__':
    main_wrapper()
