# ============================================================================
# FILE: deoplete.py
# AUTHOR: Shougo Matsushita <Shougo.Matsu at gmail.com>
# License: MIT license
# ============================================================================

from deoplete import logger
from deoplete.parent import Parent
from deoplete.util import (error_tb, find_rplugins, error)


class Deoplete(logger.LoggingMixin):

    def __init__(self, vim):
        self.name = 'core'

        self._vim = vim
        self._runtimepath = ''
        self._custom = []
        self._loaded_paths = set()
        self._prev_merged_results = {}
        self._prev_pos = []

        self._parents = []
        self._parent_count = 0
        self._max_parents = max(
            [1, self._vim.vars['deoplete#num_processes']])

        if self._max_parents > 1 and not hasattr(self._vim, 'loop'):
            error(self._vim, 'neovim-python 0.2.4+ is required.')
            return

        # Enable logging before "Init" for more information, and e.g.
        # deoplete-jedi picks up the log filename from deoplete's handler in
        # its on_init.
        if self._vim.vars['deoplete#_logging']:
            self.enable_logging()

        # Init context
        context = self._vim.call('deoplete#init#_context', 'Init', [])
        context['rpc'] = 'deoplete_on_event'

        # Init processes
        for n in range(0, self._max_parents):
            self._parents.append(Parent(vim, context))
        if self._vim.vars['deoplete#_logging']:
            for parent in self._parents:
                parent.enable_logging()

        # on_init() call
        self.on_event(context)

        if hasattr(self._vim, 'channel_id'):
            self._vim.vars['deoplete#_channel_id'] = self._vim.channel_id
        self._vim.vars['deoplete#_initialized'] = True

    def enable_logging(self):
        logging = self._vim.vars['deoplete#_logging']
        logger.setup(self._vim, logging['level'], logging['logfile'])
        self.is_debug_enabled = True

    def completion_begin(self, context):
        self.debug('completion_begin: %s', context['input'])

        self.check_recache(context)

        try:
            is_async, position, candidates = self.merge_results(context)
        except Exception:
            error_tb(self._vim, 'Error while gathering completions')

            is_async = False
            position = -1
            candidates = []

        if is_async:
            self._vim.call('deoplete#handler#_async_timer_start')
        else:
            self._vim.call('deoplete#handler#_async_timer_stop')

        if not candidates and ('deoplete#_saved_completeopt'
                               in context['vars']):
            self._vim.call('deoplete#mapping#_restore_completeopt')

        # Async update is skipped if same.
        prev_completion = context['vars']['deoplete#_prev_completion']
        prev_candidates = prev_completion['candidates']
        prev_pos = prev_completion['complete_position']
        if (context['event'] == 'Async' and
                prev_pos == self._vim.call('getpos', '.') and
                prev_candidates and candidates == prev_candidates):
            return

        # error(self._vim, candidates)
        self._vim.vars['deoplete#_context'] = {
            'complete_position': position,
            'candidates': candidates,
            'event': context['event'],
            'input': context['input'],
            'is_async': is_async,
        }
        self._vim.call('deoplete#handler#_do_complete')

        self.debug('completion_end: %s', context['input'])

    def merge_results(self, context):
        use_prev = context['position'] == self._prev_pos
        if not use_prev:
            self._prev_merged_results = {}

        is_async = False
        merged_results = []
        for cnt, parent in enumerate(self._parents):
            if use_prev and cnt in self._prev_merged_results:
                # Use previous result
                merged_results += self._prev_merged_results[cnt]
            else:
                result = parent.merge_results(context)
                is_async = is_async or result[0]
                if not result[0]:
                    self._prev_merged_results[cnt] = result[1]
                merged_results += result[1]
        self._prev_pos = context['position']

        if not merged_results:
            return (is_async, -1, [])

        complete_position = min([x['complete_position']
                                 for x in merged_results])

        all_candidates = []
        for result in sorted(merged_results,
                             key=lambda x: x['rank'], reverse=True):
            candidates = result['candidates']
            prefix = context['input'][
                complete_position:result['complete_position']]

            mark = result['mark'] + ' '
            for candidate in candidates:
                # Add prefix
                candidate['word'] = prefix + candidate['word']

                # Set default menu and icase
                candidate['icase'] = 1
                if (mark != ' ' and
                        candidate.get('menu', '').find(mark) != 0):
                    candidate['menu'] = mark + candidate.get('menu', '')
                if result['dup']:
                    candidate['dup'] = 1

            all_candidates += candidates

        # self.debug(candidates)
        max_list = context['vars']['deoplete#max_list']
        if max_list > 0:
            all_candidates = all_candidates[: max_list]

        return (is_async, complete_position, all_candidates)

    def load_sources(self, context):
        # Load sources from runtimepath
        for path in find_rplugins(context, 'source'):
            if path in self._loaded_paths:
                continue
            self._loaded_paths.add(path)

            self._parents[self._parent_count].add_source(path)
            self.debug('Process %d: %s', self._parent_count, path)

            self._parent_count += 1
            self._parent_count %= self._max_parents

        self.set_source_attributes(context)
        self.set_custom(context)

    def load_filters(self, context):
        # Load filters from runtimepath
        for path in find_rplugins(context, 'filter'):
            if path in self._loaded_paths:
                continue
            self._loaded_paths.add(path)

            for parent in self._parents:
                parent.add_filter(path)

    def set_source_attributes(self, context):
        for parent in self._parents:
            parent.set_source_attributes(context)

    def set_custom(self, context):
        self._custom = context['custom']
        for parent in self._parents:
            parent.set_custom(self._custom)

    def check_recache(self, context):
        if context['runtimepath'] != self._runtimepath:
            self._runtimepath = context['runtimepath']
            self.load_sources(context)
            self.load_filters(context)

            if context['rpc'] != 'deoplete_on_event':
                self.on_event(context)
        elif context['custom'] != self._custom:
            self.set_source_attributes(context)
            self.set_custom(context)

    def on_event(self, context):
        self.debug('on_event: %s', context['event'])
        self.check_recache(context)

        for parent in self._parents:
            parent.on_event(context)
