from __future__ import annotations

import threading
import time
from typing import Callable


# ---------------------------------------------------------------------------
# Shared state for the parallel algorithm
# ---------------------------------------------------------------------------

class _State:
    """Mutable state shared across threads during one reduce_file pass."""

    def __init__(
        self,
        lines: list[str],
        offset: int,
        on_progress: Callable[[list[str]], None] | None = None,
        verbose: bool = False,
    ) -> None:
        self.lines = list(lines)
        n = len(lines)
        self.offset = offset
        self.chunk_size: int = max(1, n - offset)
        self.index: int = n - offset
        self.update_counter: int = 0
        self.done: bool = False
        self.successes_since_resize: int = 0
        self.lock = threading.Lock()
        self.made_progress: bool = False
        self.on_progress = on_progress
        self.verbose = verbose


def _do_advance(state: _State) -> None:
    """Advance index one stride; shrink chunk_size and restart if we hit the top.
    Must be called while holding state.lock."""
    state.index -= state.chunk_size
    if state.index <= 0:
        if state.chunk_size == 1:
            state.done = True
            return
        new_size = max(1, round(state.chunk_size * 0.8))
        if new_size == state.chunk_size:
            new_size = max(1, state.chunk_size - 1)
        state.chunk_size = new_size
        state.successes_since_resize = 0
        state.index = len(state.lines) - state.offset


def _pick_chunk(
    state: _State, local_counter: int
) -> tuple[int, int, list[str], int] | None:
    """Under lock: sync local state, then pick the next chunk.

    Returns (start, end, snapshot_lines, local_counter) or None when done.
    The caller must have already acquired state.lock.
    """
    # Sync from shared if file changed since we last checked
    if state.update_counter != local_counter:
        local_counter = state.update_counter

    # Skip empty chunks (can happen near end of short files)
    while not state.done:
        start = max(0, state.index - state.chunk_size)
        end = state.index
        if end > start:
            snapshot = list(state.lines)
            captured_counter = local_counter
            _do_advance(state)
            return start, end, snapshot, captured_counter
        _do_advance(state)
    return None


def _try_commit(state: _State, candidate: list[str], local_counter: int) -> bool:
    """Under lock: apply a successful reduction if state hasn't been updated.

    Returns True if the commit was applied.
    """
    if state.update_counter != local_counter:
        return False  # stale — another thread already updated the file

    state.lines = candidate
    state.update_counter += 1
    state.made_progress = True
    if state.on_progress is not None:
        state.on_progress(candidate)
    state.successes_since_resize += 1

    if state.successes_since_resize >= 4:
        new_size = round(state.chunk_size * 1.25)
        if new_size == state.chunk_size:
            new_size += 1
        state.chunk_size = new_size
        state.successes_since_resize = 0

    # Keep index pointing at the same position in the (now shorter) file
    state.index = min(state.index, max(0, len(state.lines) - state.offset))
    return True


# ---------------------------------------------------------------------------
# Worker thread
# ---------------------------------------------------------------------------

def _worker(state: _State, test_fn: Callable[[list[str], Callable[[], bool]], bool], thread_id: int = 0) -> None:
    local_counter = -1

    while True:
        with state.lock:
            result = _pick_chunk(state, local_counter)
            if result is None:
                return
            start, end, snapshot, local_counter = result

        candidate = snapshot[:start] + snapshot[end:]

        is_cancelled = lambda: state.update_counter != local_counter
        t0 = time.monotonic()
        passed = test_fn(candidate, is_cancelled)
        elapsed = time.monotonic() - t0

        if state.verbose:
            status = "ok" if passed else "fail"
            print(f"thread {thread_id}: tested deletion of lines {start + 1}-{end} (chunk {end - start}) [{elapsed:.1f}s] {status}", flush=True)

        if passed:
            with state.lock:
                if not _try_commit(state, candidate, local_counter):
                    # Stale; sync counter so next _pick_chunk will re-sync lines
                    local_counter = state.update_counter
# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------

def reduce_file(
    lines: list[str],
    test_fn: Callable[[list[str], Callable[[], bool]], bool],
    offset: int = 0,
    n_threads: int = 1,
    on_progress: Callable[[list[str]], None] | None = None,
    verbose: bool = False,
) -> tuple[list[str], bool]:
    """One pass of delta reduction over *lines*.

    Scans bottom-to-top in chunks of *chunk_size*, deleting any chunk whose
    removal still satisfies *test_fn*.  Returns (reduced_lines, made_progress).
    """
    state = _State(lines, offset, on_progress=on_progress, verbose=verbose)

    if n_threads == 1:
        _worker(state, test_fn, thread_id=0)
    else:
        threads = [
            threading.Thread(target=_worker, args=(state, test_fn, i), daemon=True)
            for i in range(n_threads)
        ]
        for t in threads:
            t.start()
        for t in threads:
            t.join()

    return state.lines, state.made_progress


def run_to_fixpoint(
    lines: list[str],
    test_fn: Callable[[list[str], Callable[[], bool]], bool],
    n_threads: int = 1,
    on_progress: Callable[[list[str]], None] | None = None,
    verbose: bool = False,
) -> list[str]:
    """Repeat reduce_file until no further progress, alternating offset 0/1."""
    offset = 0
    while True:
        lines, progress = reduce_file(
            lines, test_fn, offset=offset, n_threads=n_threads,
            on_progress=on_progress, verbose=verbose,
        )
        if not progress:
            break
        offset = 1 - offset  # alternate 0 → 1 → 0 → …
    return lines
