# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

"""A simple abstraction for editing text files."""

import re

from sigma.util.fuelset import FuelSet

class EditFile(object):

    """Text file editing object.

    Public variables (visible in Fuel):

    errors -- bool indicating whether any I/O errors were encountered
    changes -- bool indicating whether the file was changed since last I/O
    hits -- int giving the number of lines affected by last operation

    """

    def __init__(self, host, filename=None, perm=None, user=None, group=None):
        """Constructor.

        Arguments:

        host -- the Host object
        filename -- the file name, or None (for in-memory buffer behavior)
        perms -- a Permission object, or None
        user -- an Account object, or None
        group -- an AccountGroup object, or None

        The perms, user and group arguments are used to set the file's
        permissions and ownership when it is saved.

        This sets self.hits to the number of lines read

        """
        self.host = host
        self.filename = filename
        self.perm = perm
        self.user = user
        self.group = group
        self.errors = False
        self.changes = False
        self.lines = []
        self.extracted_lines = []
        if filename is None:
            self.file = None
        else:
            self.file = host.file(filename)
            if self.file.exists:
                try:
                    stream = self.file.open()
                except IOError as err:
                    self.host.report("can't read %r: %s", self.filename, err)
                    self.errors = True
                else:
                    try:
                        self.lines = stream.readlines()
                    finally:
                        stream.close()
        self.hits = len(self.lines)

    def save(self):
        """Save back to the file from which it was read.

        We only save if three conditions are true:

        - no I/O errors were encountered previously
        - host enforcement is on
        - any changes were made since last I/O

        This set self.hits to the number of lines saved, which will be
        0 of nothing was saved

        """
        self.hits = 0
        if (self.errors or not self.host.enforce or not self.changes or
            self.file is None):
            return
        try:
            self.file.save("".join(self.lines), "changed file contents",
                           perm=self.perm, user=self.user, group=self.group)
        except EnvironmentError as err:
            self.errors = True
            self.host.error(0, "can't save %r: %s",
                            self.file.filename, err)
        else:
            self.hits = len(self.lines)
            self.changes = False

    def replace(self, selector, matcher, replacement, count=0):
        """Global pattern replacement on selected lines.

        For all lines matching the selector regex, replace all
        occurrences of the matcher regex with the replacement.
        If count is given and nonzero, at most that many replacements
        will be made per line.

        This sets self.hits to the number of lines on which at least
        one replacement was made.

        """
        self.hits = 0
        rs = re.compile(selector)
        rm = re.compile(matcher)
        for i, line in enumerate(self.lines):
            if rs.search(line):
                newline = rm.sub(replacement, line, count)
                nl = newline.rstrip("\n")
                if "\n" in nl or "\r" in nl or "\0" in newline:
                    raise ValueError("replace() can't insert \\n, \\r or \\0")
                newline = nl + "\n" # Enforce invariant
                if newline != line:
                    self.changes = True
                    self.hits += 1
                    self.lines[i] = newline

    def delete(self, selector):
        """Delete selected lines.

        Delete all lines matching the selector regex.

        This sets self.hits to the number of lines deleted.

        """
        self.hits = 0
        rs = re.compile(selector)
        i = 0
        while i < len(self.lines):
            if not rs.search(self.lines[i]):
                i += 1
            else:
                self.hits += 1
                self.changes = True
                del self.lines[i]

    def extract(self, selector, matcher):
        """Extract text from selected lines.

        Return a set of strings whose elements are whatever matches
        the matcher regex first on all lines that match the selector
        regex.

        This sets self.hits to the number of lines from which
        something was selected; this is not always the same as the
        lenth of the result set, since sets ignore duplicates.

        If the matcher regex contains at least one (...) group,
        the contents of the first group is added to the set; otherwise,
        the substring matching the entire regex is added.

        """
        self.hits = 0
        self.extracted_lines = []
        rs = re.compile(selector)
        rm = re.compile(matcher)
        for line in self.lines:
            if rs.search(line):
                m = rm.search(line)
                if m:
                    g = m.groups()
                    if g:
                        match = g[0]
                    else:
                        match = m.group()
                    self.hits += 1
                    self.extracted_lines.append(match)
        return FuelSet(self.extracted_lines)

    def get_extracted_line(self, index):
        if ((index < 0) or (index >= len(self.extracted_lines))):
            return None
        else:
            return self.extracted_lines[index]

    def append(self, text):
        """Add one or more lines to the end of the file.

        If the text contains embedded newlines, multiple lines will be
        added.  A final newline is optional.

        This sets self.hits to the number of lines appended.

        As a special case, if the file originally didn't end in a
        newline, one is appended.  (The invariant is that all
        lines in self.lines must end with a newline, except possibly
        for the last one.)

        """
        self.hits = 0
        if text is None:
            return
        if self.lines and not self.lines[-1].endswith("\n"):
            self.changes = True
            # Don't count this as a hit
            self.lines[-1] += "\n"
        lines = text.splitlines()
        if not lines:
            # Always add at least one line
            lines.append("")
        for line in lines:
                self.changes = True
                self.hits += 1
                self.lines.append(line + "\n")
