import re

from parallels.core.utils.entity import Entity


class Line(object):
    """Line in a string or a text file, with no separators"""
    def __init__(self, number, contents):
        self._number = number
        self._contents = contents

    @property
    def contents(self):
        """Contents of a line (no separators)

        :rtype: str | unicode
        """
        return self._contents

    @contents.setter
    def contents(self, contents):
        self._contents = contents

    @property
    def number(self):
        """Number of a line in a file

        :rtype: int
        """
        return self._number


class LineProcessor(object):
    """Abstraction to work with lines in a text file or a string

    Key feature is saving original line delimiters.
    """
    split_re = re.compile('(\r\n|\r|\n)')

    def __init__(self, content):
        self._items = []

        number = 1
        for item in re.split(self.split_re, content):
            if item not in {'\n', '\r', '\r\n'}:
                self._items.append(Line(number, item))
                number += 1
            else:
                self._items.append(item)

    def iter_lines(self):
        """Iterate over lines in a file, with no separators

        :rtype: list[parallels.core.utils.line_processor.Line]
        """
        for item in self._items:
            if isinstance(item, Line):
                yield item

    def serialize(self):
        """Serialize lines back to a string

        :rtype: str | unicode
        """
        item_str = []
        for item in self._items:
            if isinstance(item, Line):
                item_str.append(item.contents)
            else:
                item_str.append(item)

        return "".join(item_str)


class ChangedLineInfo(Entity):
    """Information about changed line - old and new contents, line number"""
    def __init__(self, old_contents, new_contents, line_number):
        """
        :type old_contents: str | unicode
        :type new_contents: str | unicode
        :type line_number: int
        """
        self._old_contents = old_contents
        self._new_contents = new_contents
        self._line_number = line_number

    @property
    def old_contents(self):
        """Old contents of the line

        :rtype: str | unicode
        """
        return self._old_contents

    @property
    def new_contents(self):
        """New contents of the line

        :rtype: str | unicode
        """
        return self._new_contents

    @property
    def line_number(self):
        """Line number

        :rtype: int
        """
        return self._line_number


class ReplaceResults(Entity):
    """Results of replacement function - new content as a string and list of changed lines"""

    def __init__(self, new_contents, changed_lines):
        """
        :type new_contents: str | unicode
        :type changed_lines: list[parallels.core.utils.line_processor.ChangedLineInfo]
        """
        self._new_contents = new_contents
        self._changed_lines = changed_lines

    @property
    def new_contents(self):
        """New contents, after replacement

        :rtype: str | unicode
        """
        return self._new_contents

    @property
    def changed_lines(self):
        """List of information about changed lines

        :rtype: list[parallels.core.utils.line_processor.ChangedLineInfo]
        """
        return self._changed_lines

    @property
    def has_changes(self):
        """Whether there were any changes

        :rtype: bool
        """
        return len(self.changed_lines) > 0


def match_line_replace(contents, condition_str, replacements):
    """Replace substrings (by replacements map) in a string only if it contains another substring (condition string)

    :type contents: str | unicode
    :type condition_str: str | unicode
    :type replacements: dict[str | unicode, str | unicode]
    :rtype: parallels.core.utils.line_processor.ReplaceResults
    """
    changed_lines = []
    processor = LineProcessor(contents)
    for line in processor.iter_lines():
        if condition_str in line.contents:
            old_line_contents = line.contents
            for match_str, replace_str in replacements.items():
                line.contents = line.contents.replace(match_str, replace_str)
            if line.contents != old_line_contents:
                changed_lines.append(ChangedLineInfo(old_line_contents, line.contents, line.number))

    return ReplaceResults(processor.serialize(), changed_lines)
