# -*- coding: utf-8 -*-
import sys
from parglare.termui import s_attention as _a


class Location(object):
    """
    Represents a location (point or span) of the object in the source code.

    Args:
    context(Context): Parsing context used to populate this object.

    Attributes:
    input_str: The input string (from context) being parsed.
    file_name(str): The name (path) to the file this location refers to.
    start_position(int): The position of the span if applicable
    end_position(int): The end of the span if applicable.
    line, column (int): The line/column calculated from the position start and
        input_str.
    line_end, column_end (int): The line/column calculated from the position
        end and input_str.
    """

    __slots__ = ['context', 'file_name',
                 '_line', '_column',
                 '_line_end', '_column_end']

    def __init__(self, context=None, file_name=None):

        self.context = context
        self.file_name = file_name or context.file_name

        # Evaluate this only when string representation is needed.
        # E.g. during error reporting
        self._line = None
        self._column = None

        self._line_end = None
        self._column_end = None

    @property
    def line(self):
        if self._line is None:
            self.evaluate_line_col()
        return self._line

    @property
    def line_end(self):
        if self._line_end is None:
            self.evaluate_line_col_end()
        return self._line_end

    @property
    def column(self):
        if self._column is None:
            self.evaluate_line_col()
        return self._column

    @property
    def column_end(self):
        if self._column_end is None:
            self.evaluate_line_col_end()
        return self._column_end

    def evaluate_line_col(self):
        context = self.context
        self._line, self._column = pos_to_line_col(
            context.input_str, context.start_position)

    def evaluate_line_col_end(self):
        context = self.context
        if hasattr(context, 'end_position') \
                and context.end_position:
            self._line_end, self._column_end = \
                pos_to_line_col(context.input_str, context.end_position)

    def __getattr__(self, name):
        if self.context is not None:
            return getattr(self.context, name)
        else:
            raise AttributeError(name)

    def __str__(self):
        if self.context is None:
            line, column = None, None
        else:
            line, column = self.line, self.column
        context = self.context
        if line is not None:
            return ('{}{}:{}:"{}"'
                    .format("{}:".format(self.file_name)
                            if self.file_name else "",
                            line, column,
                            position_context(context.input_str,
                                             context.start_position)))
        elif self.file_name:
            return _a(self.file_name)
        else:
            return "<Unknown location>"

    def __repr__(self):
        return str(self)


def position_context(input_str, position):
    """
    Returns position context string.
    """
    start = max(position-10, 0)
    c = str(input_str[start:position]) + _a(" **> ") \
        + str(input_str[position:position+10])
    return replace_newlines(c)


def replace_newlines(in_str):
    try:
        return in_str.replace("\n", "\\n")
    except AttributeError:
        return in_str


def load_python_module(mod_name, mod_path):
    """
    Loads Python module from an arbitrary location.
    See https://stackoverflow.com/questions/67631/how-to-import-a-module-given-the-full-path  # noqa
    """
    if sys.version_info >= (3, 5):
        import importlib.util
        spec = importlib.util.spec_from_file_location(
            mod_name, mod_path)
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
    elif sys.version_info >= (3, 3):
        from importlib.machinery import SourceFileLoader
        module = SourceFileLoader(
            mod_name, mod_path).load_module()
    else:
        import imp
        module = imp.load_source(mod_name, mod_path)

    return module


def get_collector():
    """
    Produces action/recognizers collector/decorator that will collect all
    decorated objects under dictionary attribute `all`.
    """
    all = {}

    class Collector(object):
        def __call__(self, name_or_f):
            """
            If called with action/recognizer name return decorator.
            If called over function apply decorator.
            """
            is_name = type(name_or_f) is str

            def decorator(f):
                if is_name:
                    name = name_or_f
                else:
                    name = f.__name__
                objects = all.get(name, None)
                if objects:
                    if type(objects) is list:
                        objects.append(f)
                    else:
                        all[name] = [objects, f]
                else:
                    all[name] = f
                return f
            if is_name:
                return decorator
            else:
                return decorator(name_or_f)

    objects = Collector()
    objects.all = all
    return objects


def pos_to_line_col(input_str, position):
    """
    Returns position in the (line,column) form.
    """

    if position is None:
        return None, None

    if type(input_str) is not str:
        # If we are not parsing string
        return 1, position

    line = 1
    old_pos = 0
    try:
        cur_pos = input_str.index("\n")
        while cur_pos < position:
            line += 1
            old_pos = cur_pos + 1
            cur_pos = input_str.index("\n", cur_pos + 1)
    except ValueError:
        pass

    return line, position - old_pos


class ErrorContext(object):
    """
    Context for errors.  Errors are constructed from parsing heads and are
    represented as location span.  Initially, the start and end of the span are
    set to the position where the error is found but end of the span can be
    moved forward during error recovery.
    """

    __slots__ = ['input_str', 'file_name', 'start_position', 'end_position']

    def __init__(self, context):
        self.start_position = self.end_position = context.position
        self.input_str = context.input_str
        self.file_name = context.file_name