# -*- coding: utf-8 -*-
"""
This module contains the worker functions/classes used on the server side.

A worker is a function or a callable which receive one single argument (the
decoded json object) and returns a tuple made up of a status (bool) and a
response object (json serializable).

A worker is always tightly coupled with its caller, so are the data.

.. warning::
    This module should keep its dependencies as low as possible and fully
    supports python2 syntax. This is badly needed since the server might be run
    with a python2 interpreter. We don't want to force the user to install all
    the pyqode dependencies twice (if the user choose to run the server with
    python2, which might happen in pyqode.python to support python2 syntax).

"""
import logging
import re
import sys
import traceback


def echo_worker(data):
    """
    Example of worker that simply echoes back the received data.

    :param data: Request data dict.
    :returns: True, data
    """
    print('echo worker running')
    return data


class CodeCompletionWorker:
    """
    This is the worker associated with the code completion mode.

    The worker does not actually do anything smart, the real work of collecting
    code completions is accomplished by the completion providers (see the
    :class:`pyqode.core.backend.workers.CodeCompletionWorker.Provider`
    interface) listed in
    :attr:`pyqode.core.backend.workers.CompletionWorker.providers`.

    Completion providers must be installed on the CodeCompletionWorker
    at the beginning of the main server script, e.g.::

        from pyqode.core.backend import CodeCompletionWorker
        CodeCompletionWorker.providers.insert(0, MyProvider())
    """
    #: The list of code completion provider to run on each completion request.
    providers = []

    class Provider:
        """
        This class describes the expected interface for code completion
        providers.

        You can inherit from this class but this is not required as long as you
        implement a ``complete`` method which returns the list of completions
        and have the expected signature::

            def complete(self, code, line, column, path, encoding, prefix):
                pass

        """

        def complete(self, code, line, column, path, encoding, prefix):
            """
            Returns a list of completions.

            A completion is dictionary with the following keys:

                - 'name': name of the completion, this the text displayed and
                  inserted when the user select a completion in the list
                - 'icon': an optional icon file name
                - 'tooltip': an optional tooltip string

            :param code: code string
            :param line: line number (0 based)
            :param column: column number (0 based)
            :param path: file path
            :param encoding: file encoding
            :param prefix: completion prefix (text before cursor)

            :returns: A list of completion dicts as described above.
            :rtype: list
            """
            raise NotImplementedError()

    def __call__(self, data):
        """
        Do the work (this will be called in the child process by the
        SubprocessServer).
        """
        code = data['code']
        line = data['line']
        column = data['column']
        path = data['path']
        encoding = data['encoding']
        prefix = data['prefix']
        req_id = data['request_id']
        completions = []
        for prov in CodeCompletionWorker.providers:
            try:
                results = prov.complete(
                    code, line, column, path, encoding, prefix)
                completions.append(results)
                if len(completions):
                    break
            except:
                sys.stderr.write('Failed to get completions from provider %r'
                                 % prov)
                exc1, exc2, exc3 = sys.exc_info()
                traceback.print_exception(exc1, exc2, exc3, file=sys.stderr)
        return [(line, column, req_id)] + completions


class DocumentWordsProvider:
    """
    Provides completions based on the document words
    """
    words = {}

    # word separators
    separators = [
        '~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '+', '{',
        '}', '|', ':', '"', "'", "<", ">", "?", ",", ".", "/", ";", '[',
        ']', '\\', '\n', '\t', '=', '-', ' '
    ]

    @staticmethod
    def split(txt, seps):
        """
        Splits a text in a meaningful list of words based on a list of word
        separators (define in pyqode.core.settings)

        :param txt: Text to split
        :param seps: List of words separators
        :return: A **set** of words found in the document (excluding
            punctuations, numbers, ...)
        """
        # replace all possible separators with a default sep
        default_sep = seps[0]
        for sep in seps[1:]:
            if sep:
                txt = txt.replace(sep, default_sep)
        # now we can split using the default_sep
        raw_words = txt.split(default_sep)
        words = set()
        for word in raw_words:
            # w = w.strip()
            if word.replace('_', '').isalpha():
                words.add(word)
        return sorted(words)

    def complete(self, code, *args):
        """
        Provides completions based on the document words.

        :param code: code to complete
        :param args: additional (unused) arguments.
        """
        completions = []
        for word in self.split(code, self.separators):
            completions.append({'name': word})
        return completions


def finditer_noregex(string, sub, whole_word):
    """
    Search occurrences using str.find instead of regular expressions.

    :param string: string to parse
    :param sub: search string
    :param whole_word: True to select whole words only
    """
    start = 0
    while True:
        start = string.find(sub, start)
        if start == -1:
            return
        if whole_word:
            if start:
                pchar = string[start - 1]
            else:
                pchar = ' '
            try:
                nchar = string[start + len(sub)]
            except IndexError:
                nchar = ' '
            if nchar in DocumentWordsProvider.separators and \
                    pchar in DocumentWordsProvider.separators:
                yield start
            start += len(sub)
        else:
            yield start
            start += 1


def findalliter(string, sub, regex=False, case_sensitive=False,
                whole_word=False):
    """
    Generator that finds all occurrences of ``sub`` in  ``string``
    :param string: string to parse
    :param sub: string to search
    :param regex: True to search using regex
    :param case_sensitive: True to match case, False to ignore case
    :param whole_word: True to returns only whole words
    :return:
    """
    if not sub:
        return
    if regex:
        flags = re.MULTILINE
        if not case_sensitive:
            flags |= re.IGNORECASE
        for val in re.finditer(sub, string, flags):
            yield val.span()
    else:
        if not case_sensitive:
            string = string.lower()
            sub = sub.lower()
        for val in finditer_noregex(string, sub, whole_word):
            yield val, val + len(sub)


def findall(data):
    """
    Worker that finds all occurrences of a given string (or regex)
    in a given text.

    :param data: Request data dict::
        {
            'string': string to search in text
            'sub': input text
            'regex': True to consider string as a regular expression
            'whole_word': True to match whole words only.
            'case_sensitive': True to match case, False to ignore case
        }
    :return: list of occurrence positions in text
    """
    return list(findalliter(
        data['string'], data['sub'], regex=data['regex'],
        whole_word=data['whole_word'], case_sensitive=data['case_sensitive']))
