# -*- coding: utf-8 -*-
#
# This file is part of NINJA-IDE (http://ninja-ide.org).
#
# NINJA-IDE is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# any later version.
#
# NINJA-IDE is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with NINJA-IDE; If not, see <http://www.gnu.org/licenses/>.

import os
import time
from threading import Thread, Lock
from multiprocessing import Process, Queue

from ninja_ide.intellisensei.analyzer import model
from ninja_ide.intellisensei.analyzer import analyzer


try:
    unicode
except NameError:
    # Python 3
    basestring = unicode = str  # lint:ok

__completion_daemon_instance = None
WAITING_BEFORE_START = 5
PROJECTS = {}


def CompletionDaemon():
    global __completion_daemon_instance
    if __completion_daemon_instance is None:
        __completion_daemon_instance = __CompletionDaemon()
        __completion_daemon_instance.start()
    return __completion_daemon_instance


class __CompletionDaemon(Thread):

    def __init__(self):
        Thread.__init__(self)
        self.analyzer = analyzer.Analyzer()
        self.modules = {}
        self.projects_modules = {}
        self._relations = {}
        self.reference_counter = 0
        self.keep_alive = True
        self.lock = Lock()
        self.queue_receive = Queue()
        self.queue_send = Queue()
        self.daemon = _DaemonProcess(self.queue_send, self.queue_receive)
        self.daemon.start()

    def run(self):
        global WAITING_BEFORE_START
        time.sleep(WAITING_BEFORE_START)
        while self.keep_alive:
            path_id, module, resolve = self.queue_receive.get()
            if path_id is None:
                continue
            self.lock.acquire()
            self.modules[path_id] = module
            self.lock.release()
            if resolve:
                resolution = self._resolve_with_other_modules(resolve)
                self._relations[path_id] = []
                for package in resolution:
                    self._relations[path_id].append(resolution[package])
                self.queue_send.put((path_id, module, False, resolution))

    def _resolve_with_other_modules(self, packages):
        resolution = {}
        for package in packages:
            if package.find('(') != -1:
                package = package[:package.index('(')]
            if self.projects_modules.get(package, False):
                folder = self.projects_modules[package]
                filename = os.path.join(folder, '__init__.py')
                if self._analyze_file(filename):
                    resolution[package] = filename
            elif self.projects_modules.get(package.rsplit('.', 1)[0], False):
                name = package.rsplit('.', 1)
                folder = self.projects_modules[name[0]]
                filename = "%s.py" % os.path.join(folder, name[1])
                if os.path.isfile(filename):
                    if self._analyze_file(filename):
                        resolution[package] = filename
            elif self.projects_modules.get(package.rsplit('.', 2)[0], False):
                name = package.rsplit('.', 2)
                folder = self.projects_modules[name[0]]
                filename = "%s.py" % os.path.join(folder, name[1])
                if os.path.isfile(filename):
                    if self._analyze_file(filename):
                        resolution[package.rsplit('.', 1)[0]] = filename
        return resolution

    def _analyze_file(self, filename):
        try:
            if filename not in self.modules:
                source = ''
                with open(filename) as f:
                    source = f.read()
                module = self.analyzer.analyze(source)
                self.inspect_module(filename, module, False)
                return True
        except Exception as reason:
            print(reason)
        return False

    def unload_module(self, path_id):
        relations = self._relations.pop(path_id, None)
        if relations is not None:
            relations.append(path_id)
            for module in relations:
                valid = False
                for rel in self._relations:
                    other_modules = self._relations[rel]
                    if module in other_modules:
                        valid = True
                if not valid:
                    self.modules.pop(module, None)

    def process_path(self):
        for project in PROJECTS:
            if PROJECTS[project]:
                continue
            project = os.path.abspath(project)
            package = os.path.basename(project)
            self.projects_modules[package] = project
            for root, dirs, files in os.walk(project, followlinks=True):
                if '__init__.py' in files:
                    package = root[len(project) + 1:].replace(
                        os.path.sep, '.')
                    self.projects_modules[package] = root

    def inspect_module(self, path_id, module, recursive=True):
        self.lock.acquire()
        self.modules[path_id] = module
        self.lock.release()
        self.queue_send.put((path_id, module, recursive, None))

    def get_module(self, path_id):
        return self.modules.get(path_id, None)

    def _shutdown_process(self):
        self.queue_send.put((None, None, None, None))
        self.daemon.terminate()
        self.queue_receive.put((None, None, None))

    def force_stop(self):
        self.keep_alive = False
        self._shutdown_process()
        for project in PROJECTS:
            PROJECTS[project] = False
        if self.is_alive():
            self.join()


class _DaemonProcess(Process):

    def __init__(self, queue_receive, queue_send):
        super(_DaemonProcess, self).__init__()
        self.queue_receive = queue_receive
        self.queue_send = queue_send
        self.iteration = 0
        self.packages = []

    def run(self):
        while True:
            self.iteration = 0
            path_id, module, recursive, resolution = self.queue_receive.get()
            if path_id is None and module is None:
                break

            try:
                if resolution is not None:
                    self.packages = resolution
                    self.iteration = 2
                    self._resolve_module(module)
                elif module.need_resolution():
                    self._resolve_module(module)
                    self.iteration = 1
                    self._resolve_module(module)
                else:
                    continue
                if self.packages and recursive:
                    self.queue_send.put((path_id, module, self.packages))
                else:
                    self.queue_send.put((path_id, module, []))
            except Exception as reason:
                # Try to not die whatever happend
                message = 'Daemon Fail with: %r', reason
                print(message)
                raise
            finally:
                self.packages = []

    def _resolve_module(self, module):
        self._resolve_attributes(module, module)
        self._resolve_functions(module, module)
        for cla in module.classes:
            clazz = module.classes[cla]
            self._resolve_inheritance(clazz, module)
            self._resolve_attributes(clazz, module)
            self._resolve_functions(clazz, module)

    def _resolve_inheritance(self, clazz, module):
        for base in clazz.bases:
            name = base.split('.', 1)
            main_attr = name[0]
            child_attrs = ''
            if len(name) == 2:
                child_attrs = name[1]
            result = module.get_type(main_attr, child_attrs)
            data = model.late_resolution
            if result.get('found', True):
                data_type = module.imports[main_attr].get_data_type()
                if child_attrs:
                    child_attrs = '.%s' % child_attrs
                name = '%s%s().' % (data_type, child_attrs)
                imports = module.get_imports()
                imports = [imp.split('.')[0] for imp in imports]
                data = completer.get_all_completions(name, imports)
                data = (name, data)
            elif result.get('object', False).__class__ is model.Clazz:
                data = result['object']
            clazz.bases[base] = data
        clazz.update_with_parent_data()

    def _resolve_functions(self, structure, module):
        if structure.__class__ is model.Assign:
            return
        for func in structure.functions:
            function = structure.functions[func]
            self._resolve_attributes(function, module)
            self._resolve_functions(function, module)
            self._resolve_returns(function, module)

    def _resolve_returns(self, structure, module):
        if structure.__class__ is model.Assign:
            return
        self._resolve_types(structure.return_type, module, structure, 'return')

    def _resolve_attributes(self, structure, module):
        if structure.__class__ is model.Assign:
            return
        for attr in structure.attributes:
            assign = structure.attributes[attr]
            self._resolve_types(assign.data, module, assign)

    def _resolve_types(self, types, module, structure=None, split_by='='):
        if self.iteration == 0:
            self._resolve_with_imports(types, module, split_by)
            self._resolve_with_local_names(types, module, split_by)
        elif self.iteration == 1:
            self._resolve_with_local_vars(types, module, split_by, structure)
        else:
            self._resolve_with_linked_modules(types, module, structure)

    def _resolve_with_linked_modules(self, types, module, structure):
        for data in types:
            name = data.data_type
            if not isinstance(name, basestring):
                continue
            for package in self.packages:
                if name.startswith(package):
                    to_resolve = name[len(package):]
                    if to_resolve and to_resolve[0] == '.':
                        to_resolve = to_resolve[1:]
                    path = self.packages[package]
                    linked = model.LinkedModule(path, to_resolve)
                    data.data_type = linked
                    break

    def _resolve_with_imports(self, types, module, splitby):
        for data in types:
            if data.data_type != model.late_resolution:
                continue
            line = data.line_content
            value = line.split(splitby)[1].strip().split('.')
            name = value[0]
            extra = ''
            if name.find('(') != -1:
                extra = name[name.index('('):]
                name = name[:name.index('(')]
            if name in module.imports:
                value[0] = module.imports[name].data_type
                package = '.'.join(value)
                resolve = "%s%s" % (package, extra)
                data.data_type = resolve
                self.packages.append(package)

    def _resolve_with_local_names(self, types, module, splitby):
        #TODO: resolve with functions returns
        for data in types:
            if data.data_type != model.late_resolution:
                continue
            line = data.line_content
            value = line.split(splitby)[1].split('(')[0].strip()
            if value in module.classes:
                clazz = module.classes[value]
                data.data_type = clazz

    def _resolve_with_local_vars(self, types, module, splitby, structure=None):
        for data in types:
            if data.data_type != model.late_resolution:
                continue
            line = data.line_content
            value = line.split(splitby)[1].split('(')[0].strip()
            sym = value.split('.')
            if len(sym) != 0:
                main_attr = sym[0]
                if len(sym) > 2:
                    child_attr = '.'.join(sym[1:])
                elif len(sym) == 2:
                    child_attr = sym[1]
                else:
                    child_attr = ''
                scope = []
                self._get_scope(structure, scope)
                if structure.__class__ is model.Assign:
                    scope.pop(0)
                scope.reverse()
                result = module.get_type(main_attr, child_attr, scope)
                data_type = model.late_resolution
                if isinstance(result['type'], basestring) and len(result) < 3:
                    if child_attr and \
                       structure.__class__ is not model.Function:
                        data_type = "%s.%s" % (result['type'], child_attr)
                    else:
                        data_type = result['type']
                elif result.get('object', False):
                    data_type = result['object']
                if data is not None:
                    data.data_type = data_type

    def _get_scope(self, structure, scope):
        if structure.__class__ not in (None, model.Module):
            scope.append(structure.name)
            self._get_scope(structure.parent, scope)


def shutdown_daemon():
    daemon = CompletionDaemon()
    daemon.force_stop()
    global __completion_daemon_instance
    __completion_daemon_instance = None


def add_project_folder(project_path):
    global PROJECTS
    if project_path not in PROJECTS:
        PROJECTS[project_path] = False
        daemon = CompletionDaemon()
        daemon.process_path()
