# Copyright 2025 XMOS LIMITED.
# This Software is subject to the terms of the XMOS Public Licence: Version 1.

import sys
from typing import List

def is_blank(line: str) -> bool:
    """Return True if the given line is empty or contains only whitespace.

    Args:
        line: A single line string (may include the trailing newline).

    Returns:
        True if the line is blank (only whitespace or empty), otherwise False.
    """
    return line.strip() == ''

def strip_trailing_blank_lines(lines: List[str]) -> List[str]:
    """Return a new list with trailing blank lines removed.

    This does not modify the input list.
    Args:
        lines: List of lines (each may include a newline).

    Returns:
        A copy of lines with any blank lines removed from the end.
    """
    end = len(lines)
    while end > 0 and is_blank(lines[end - 1]):
        end -= 1
    return lines[:end]

def trim_one_matching_leading_newline(replacement: str, after: str) -> str:
    """If 'replacement' ends with a newline sequence and 'after' begins with the same
    sequence, remove one instance from the start of 'after' and return the result.

    Args:
        replacement: The replacement text that may end with '\n' or '\r\n'.
        after: The text following the search string in the target file.

    Returns:
        The possibly-trimmed 'after' string.
    """
    if replacement.endswith('\r\n'):
        nl = '\r\n'
    elif replacement.endswith('\n'):
        nl = '\n'
    else:
        return after
    if after.startswith(nl):
        return after[len(nl):]
    return after

def classify_lines(lines: List[str]):
    """Classify each line as blank or a whole-line comment.

    Scans 'lines' and returns a tuple (blank_flags, comment_flags), where each
    is a list of booleans of the same length as 'lines'. A line is considered
    blank if it contains only whitespace. A line is considered a whole-line
    comment if it starts with '//' or '/*', and lines inside a '/* ... */'
    block are marked as comments until the closing '*/' is encountered.

    Args:
        lines: List of lines from the input file.

    Returns:
        (blank_flags, comment_flags): two lists of booleans.
    """
    n = len(lines)
    blank = [is_blank(l) for l in lines]
    comment = [False] * n
    in_block = False
    for i, line in enumerate(lines):
        s = line.lstrip()
        if in_block:
            comment[i] = True
            if '*/' in s:
                in_block = False
            continue
        if s.startswith('//'):
            comment[i] = True
            continue
        if s.startswith('/*'):
            comment[i] = True
            if '*/' not in s:
                in_block = True
            continue
        # otherwise not a whole-line comment
    return blank, comment

def build_stored_lines(lines: List[str]) -> List[str]:
    """Build the list of stored lines to be used as the replacement text.

    Rules implemented:
      - Leading blank lines before the first source line are discarded.
      - All contiguous whole-line comment lines immediately preceding the first
        source line are kept if there is no intervening blank line.
      - Identify the first and last source-code lines (neither blank nor whole-line
        comment) and keep all lines (source, comment, blank) between them
        inclusive.
      - If no source code is present, an empty list is returned.

    Args:
        lines: List of lines read from the input file.

    Returns:
        A list of lines (including their original newline characters) to be
        concatenated and used as the replacement.
    """
    if not lines:
        return []
    blank, comment = classify_lines(lines)
    # find source indices
    source_idxs = [i for i in range(len(lines)) if not blank[i] and not comment[i]]
    if not source_idxs:
        return []  # no source code -> nothing stored
    first = source_idxs[0]
    last = source_idxs[-1]
    kept: List[str] = []
    # keep all contiguous comment lines immediately before first source (no intervening blank)
    pre_idx = first - 1
    if pre_idx >= 0 and comment[pre_idx] and not blank[pre_idx]:
        start = pre_idx
        while start - 1 >= 0 and comment[start - 1] and not blank[start - 1]:
            start -= 1
        # append in original order
        for i in range(start, pre_idx + 1):
            kept.append(lines[i])
    # keep all lines from first to last inclusive (source, comment, blank)
    kept.extend(lines[first:last + 1])
    return kept

def main():
    """Command-line entry point for the script.

    Usage: substitute.py <input-path> <target-path> "search_string"

    Behavior:
      - Reads the input file and extracts stored lines according to the rules.
      - Opens the target file, replaces the first occurrence of search_string
        with the concatenated stored lines, and overwrites the target file.
      - Exits with non-zero status and prints an error message on failure.

    Returns:
        None. Exits the process on error conditions.
    """
    if len(sys.argv) != 4:
        print("Usage: substitute.py <input-path> <target-path> \"search_string\"", file=sys.stderr)
        sys.exit(2)
    input_path = sys.argv[1]
    target_path = sys.argv[2]
    search_string = sys.argv[3]

    try:
        with open(input_path, 'r', encoding='utf-8') as f:
            lines = f.readlines()
    except Exception as e:
        print(f"Error reading input file: {e}", file=sys.stderr)
        sys.exit(3)

    stored_lines = build_stored_lines(lines)
    # defensive: remove any trailing blank lines from the stored lines
    stored_lines = strip_trailing_blank_lines(stored_lines)
    replacement = ''.join(stored_lines)

    try:
        with open(target_path, 'r', encoding='utf-8') as f:
            content = f.read()
    except Exception as e:
        print(f"Error reading target file: {e}", file=sys.stderr)
        sys.exit(4)

    # perform a single replacement while avoiding an accidental double-newline
    idx = content.find(search_string)
    if idx == -1:
        print("Search string not found in target file.", file=sys.stderr)
        sys.exit(5)

    before = content[:idx]
    after = content[idx + len(search_string):]

    # remove leading whitespace (spaces and tabs) on the line that contains the search string
    # preserve all other content in 'before'
    last_nl = before.rfind('\n')
    line_start = last_nl + 1  # 0 when no newline found
    prefix = before[:line_start]
    line_tail = before[line_start:]
    # strip only spaces and tabs that are the indentation before the search string
    before = prefix + line_tail.lstrip(' \t')

    # detect newline sequence at end of replacement and remove one instance
    # from 'after' if it begins with the same sequence to avoid a double blank line.
    after = trim_one_matching_leading_newline(replacement, after)

    new_content = before + replacement + after

    try:
        with open(target_path, 'w', encoding='utf-8') as f:
            f.write(new_content)
    except Exception as e:
        print(f"Error writing target file: {e}", file=sys.stderr)
        sys.exit(6)

if __name__ == '__main__':
    main()
