from typing import Tuple, Union, Dict, Any

from antlr4.Token import CommonToken
from antlr4 import ParserRuleContext
from antlr4.tree.Tree import TerminalNodeImpl

from .preprocessor.stream import PreprocessedInputStream
from .preprocessor.segment_map import SegmentMap

#-------------------------------------------------------------------------------
# Generic Base Classes
#-------------------------------------------------------------------------------
class SourceRefBase:
    """
    Base class for all source references
    """

    def derive_coordinates(self) -> None:
        """
        DEPRECATED
        Some dependents may call this if using the old SourceRef API
        """

    def __deepcopy__(self, memo: Dict[int, Any]) -> 'SourceRefBase':
        # Don't deepcopy source refs
        return self


class FileSourceRef(SourceRefBase):
    """
    Simple file source reference that is only capable of providing the path to
    the originating file.
    """
    def __init__(self, path: str):
        super().__init__()
        self._path = path

    @property
    def path(self) -> str:
        """
        Path of the originating file
        """
        return self._path

    @property
    def filename(self) -> str:
        """
        DEPRECATED
        Alias to ``path``.
        Some dependents may call this if using the old SourceRef API
        """
        return self.path

class DetailedFileSourceRef(FileSourceRef):
    """
    Detailed source reference that is capable of providing a context snippet
    from the originating source.
    """

    @property
    def path(self) -> str:
        """
        Path of the originating file
        """
        raise NotImplementedError

    @property
    def line(self) -> int:
        """
        Line number within the originating file
        """
        raise NotImplementedError

    @property
    def line_text(self) -> str:
        """
        Source text of the originating line
        """
        raise NotImplementedError

    @property
    def line_selection(self) -> Tuple[int, int]:
        """
        Start/end coordinates of the selection within line_text.
        If the actual selection spans multiple lines, it is clamped to the end
        of this line.
        """
        raise NotImplementedError

#-------------------------------------------------------------------------------
class DirectSourceRef(DetailedFileSourceRef):
    """
    Source reference that points directly to a file's coordinates.
    """
    def __init__(self, path: str, start_idx: int, end_idx: int):
        super().__init__(path)
        self._start_idx = start_idx
        self._end_idx = end_idx

        self._line = None # type: int
        self._line_text = None # type: str
        self._line_selection = None # type: Tuple[int, int]

    def _extract_line_info(self) -> None:
        idx = 0
        lineno = 1
        line_start_idx = 0

        with open(self.path, 'r', newline='', encoding='utf_8') as fp:
            while True:
                line_text = fp.readline()
                assert line_text != ""

                idx += len(line_text)

                # This line contains the start_idx
                if self._start_idx < idx:
                    line_text = line_text.rstrip("\n").rstrip("\r")
                    self._line = lineno
                    self._line_text = line_text
                    start_column = self._start_idx - line_start_idx

                    # Get the end of the selection
                    end_column = self._end_idx - line_start_idx
                    # Clamp it to the end of the current line
                    if end_column >= len(line_text):
                        end_column = len(line_text) - 1

                    self._line_selection = (start_column, end_column)

                    break

                lineno += 1
                line_start_idx = idx

    @property
    def path(self) -> str:
        return self._path

    @property
    def line(self) -> int:
        if self._line is None:
            self._extract_line_info()
        return self._line

    @property
    def line_text(self) -> str:
        if self._line_text is None:
            self._extract_line_info()
        return self._line_text

    @property
    def line_selection(self) -> Tuple[int, int]:
        if self._line_selection is None:
            self._extract_line_info()
        return self._line_selection



class SegmentedSourceRef(DirectSourceRef):
    """
    Source reference that requires coordinate transformation through one or more
    segment maps
    """

    def __init__(self, seg_map: SegmentMap, start_idx: int, end_idx: int):
        super().__init__(None, None, None)

        self._seg_map = seg_map
        self._seg_start_idx = start_idx
        self._seg_end_idx = end_idx

    @property
    def path(self) -> str:
        if self._path is None:
            self._resolve_seg_map()
        return self._path

    def _resolve_seg_map(self) -> None:
        self._start_idx, self._end_idx, self._path = self._seg_map.get_selection(
            self._seg_start_idx, self._seg_end_idx
        )


#-------------------------------------------------------------------------------
def src_ref_from_antlr(antlr_ref: Union[CommonToken, TerminalNodeImpl, ParserRuleContext]) -> 'SourceRefBase':
    # Normalize to pair of CmmonToken objects
    if isinstance(antlr_ref, CommonToken):
        token = antlr_ref
        end_token = None
    elif isinstance(antlr_ref, TerminalNodeImpl):
        token = antlr_ref.symbol
        end_token = None
    elif isinstance(antlr_ref, ParserRuleContext):
        # antlr_ref is an entire context (token range)
        token = antlr_ref.start
        end_token = antlr_ref.stop
    else:
        print(antlr_ref)
        raise NotImplementedError

    # Extract selection coordinates
    start = token.start
    if end_token is None:
        end = token.stop
    else:
        end = end_token.stop

    inputStream = token.getInputStream()
    if not isinstance(inputStream, PreprocessedInputStream):
        # This originated from a non-file, so the full src_ref is not known
        # TODO: Eventually extend this to a stream source ref
        return None

    return SegmentedSourceRef(inputStream.seg_map, start, end)
