# -*- 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/>.

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


MODULES = None
late_resolution = 0


def filter_data_type(data_types):
    occurrences = {}
    for type_ in data_types:
        if isinstance(type_, basestring):
            item = occurrences.get(type_, [0, type_])
            item[0] += 1
            occurrences[type_] = item
        else:
            item = occurrences.get(type_, [0, type_])
            item[0] += 1
            occurrences[type_.name] = item
    values = [occurrences[key][0] for key in occurrences]
    maximum = max(values)
    data_type = [occurrences[key][1] for key in occurrences
                 if occurrences[key][0] == maximum]
    return data_type[0]


def remove_function_arguments(line):
    #TODO: improve line analysis using tokenizer to get the lines of the text
    while line.find('(') != -1:
        start = line.find('(')
        end = line.find(')') + 1
        if start == -1 or end == 0 or end <= start:
            break
        line = line[:start] + line[end:]
    return line


class _TypeData(object):

    def __init__(self, lineno, data_type, line_content, oper):
        self.lineno = lineno
        self.data_type = data_type
        self.line_content = line_content
        if data_type != late_resolution:
            oper = None
        self.operation = oper
        self.from_import = False
        if isinstance(data_type, str):
            self.is_native = True
        else:
            self.is_native = False
        self.can_resolve = True

    def get_data_type(self):
        return self.data_type

    def __eq__(self, other):
        return other.line_content == self.line_content

    def __repr__(self):
        return repr(self.data_type)


class Structure(object):

    def __init__(self):
        self.attributes = {}
        self.functions = {}
        self.parent = None

    def add_function(self, function):
        function.parent = self
        self.functions[function.name] = function

    def add_attributes(self, attributes):
        #attributes = {name: [(lineno, type),...]}
        for attribute in attributes:
            if attribute[0] in self.attributes:
                assign = self.attributes[attribute[0]]
            else:
                assign = Assign(attribute[0])
            assign.add_data(*attribute[1:])
            assign.parent = self
            self.attributes[assign.name] = assign

    def update_attributes(self, attributes):
        for name in self.attributes:
            if name in attributes:
                assign = self.attributes[name]
                old_assign = attributes[name]
                for type_data in assign.data:
                    if type_data in old_assign.data:
                        old_type = old_assign.data[
                            old_assign.data.index(type_data)]
                        if old_type.data_type.__class__ is not Clazz:
                            type_data.data_type = old_type.data_type

    def update_functions(self, functions):
        for func_name in self.functions:
            if func_name in functions:
                old_func = functions[func_name]
                if old_func.__class__ is Assign:
                    continue
                function = self.functions[func_name]
                function.update_functions(old_func.functions)
                function.update_attributes(old_func.attributes)
                # Function Arguments
                for arg in function.args:
                    if arg in old_func.args:
                        argument_type = function.args[arg].data[0]
                        old_type = old_func.args[arg].data[0]
                        argument_type.data_type = old_type.data_type
                # Function Returns
                for type_data in function.return_type:
                    if type_data in old_func.return_type:
                        old_type = old_func.return_type[
                            old_func.return_type.index(type_data)]
                        type_data.data_type = old_type.data_type

    def get_attribute_type(self, name):
        """Return a tuple with:(Found, Type)"""
        result = {'found': False, 'type': None}
        var_names = name.split('.')
        attr = self.attributes.get(var_names[0], None)
        if attr is not None:
            result['found'], result['type'] = True, attr.get_data_type()
        elif self.parent.__class__ is Function:
            result = self.parent.get_attribute_type(name)
        return result

    def _get_scope_structure(self, structure, scope):
        struct = structure
        if len(scope) > 0:
            scope_name = scope[0]
            new_struct = structure.functions.get(scope_name, None)
            struct = self._get_scope_structure(new_struct, scope[1:])
        return struct

    def _resolve_attribute(self, type_data, attrs):
        object_type = type_data.get_data_type()
        return (True, object_type)

    def recursive_search_type(self, structure, attrs, scope):
        result = {'found': False, 'type': None}
        structure = self._get_scope_structure(structure, scope)
        if structure and structure.__class__ is not Assign:
            attr_name = attrs[0]
            data_type = structure.get_attribute_type(attr_name)
            result = data_type
        return result


class Module(Structure):

    def __init__(self):
        super(Module, self).__init__()
        self.imports = {}
        self.classes = {}

    def add_imports(self, imports):
        for imp in imports:
            line_content = "import %s" % imp[1]
            info = _TypeData(None, imp[1], line_content, None)
            self.imports[imp[0]] = info

    def add_class(self, clazz):
        clazz.parent = self
        self.classes[clazz.name] = clazz

    def update_classes(self, classes):
        for clazz_name in self.classes:
            if clazz_name in classes:
                clazz = self.classes[clazz_name]
                clazz.update_functions(classes[clazz_name].functions)
                clazz.update_attributes(classes[clazz_name].attributes)

    def get_type(self, main_attr, child_attrs='', scope=None):
        result = {'found': False, 'type': None}
        canonical_attrs = remove_function_arguments(child_attrs)
        if not scope:
            value = self.imports.get(main_attr,
                                     self.attributes.get(main_attr,
                                                         self.functions.get(
                                                             main_attr,
                                                             self.classes.get(
                                                                 main_attr,
                                                                 None))))
            if value is not None and value.__class__ is not Clazz:
                data_type = value.get_data_type()
                result['found'], result['type'] = True, data_type
                if child_attrs or (isinstance(data_type, basestring) and
                   data_type.endswith(main_attr)):
                    result['main_attr_replace'] = True
            elif value.__class__ is Clazz:
                result['found'], result['type'] = False, value
        elif main_attr == 'self':
            clazz_name = scope[0]
            clazz = self.classes.get(clazz_name, None)
            if clazz is not None:
                result = clazz.get_attribute_type(canonical_attrs)
            if canonical_attrs == '' and clazz is not None:
                items = clazz.get_completion_items()
                result['found'], result['type'] = False, items
        elif scope:
            scope_name = scope[0]
            structure = self.classes.get(scope_name,
                                         self.functions.get(scope_name, None))
            if structure is not None:
                attrs = [main_attr] + canonical_attrs.split('.')
                if len(attrs) > 1 and attrs[1] == '':
                    del attrs[1]
                result = self.recursive_search_type(
                    structure, attrs, scope[1:])
                if not result['found']:
                    value = self.imports.get(main_attr,
                                             self.attributes.get(
                                                 main_attr,
                                                 self.functions.get(main_attr,
                                                                    None)))
                    if value is not None:
                        data_type = value.get_data_type()
                        result['found'], result['type'] = True, data_type

        if result['type'].__class__ is Clazz:
            if canonical_attrs:
                attrs = canonical_attrs.split('.')
                if attrs[-1] == '':
                    attrs.pop(-1)
                result = self._search_type(result['type'], attrs)
            else:
                result = {'found': False,
                          'type': result['type'].get_completion_items(),
                          'object': result['type']}
        elif result['type'].__class__ is LinkedModule:
            if main_attr == 'self':
                attrs = canonical_attrs.split('.', 1)
                canonical_attrs = ''
                if len(attrs) > 1:
                    canonical_attrs = attrs[1]
            result = result['type'].get_type(canonical_attrs)

        return result

    def _search_type(self, structure, attrs):
        result = {'found': False, 'type': None}
        if not attrs:
            return result
        attr = attrs[0]
        value = structure.attributes.get(attr,
                                         structure.functions.get(attr, None))
        if value is None:
            return result
        data_type = value.get_data_type()
        if data_type.__class__ is Clazz and len(attrs) > 1:
            result = self._search_type(data_type, attrs[1:])
        elif data_type.__class__ is Clazz:
            items = data_type.get_completion_items()
            result['found'], result['type'] = False, items
            result['object'] = data_type
        elif isinstance(data_type, basestring):
            result['found'], result['type'] = True, data_type
            result['object'] = data_type
        return result

    def get_imports(self):
        module_imports = ['import __builtin__']
        for name in self.imports:
            module_imports.append(self.imports[name].line_content)
        return module_imports

    def need_resolution(self):
        if self._check_attr_func_resolution(self):
            return True
        for cla in self.classes:
            clazz = self.classes[cla]
            for parent in clazz.bases:
                if clazz.bases[parent] is None:
                    return True
            if self._check_attr_func_resolution(clazz):
                return True
        return False

    def _check_attr_func_resolution(self, structure):
        for attr in structure.attributes:
            attribute = structure.attributes[attr]
            for d in attribute.data:
                if d.data_type == late_resolution:
                    return True
        for func in structure.functions:
            function = structure.functions[func]
            for attr in function.attributes:
                attribute = function.attributes[attr]
                for d in attribute.data:
                    if d.data_type == late_resolution:
                        return True
            for ret in function.return_type:
                if ret.data_type == late_resolution:
                    return True
        return False


class Clazz(Structure):

    def __init__(self, name):
        super(Clazz, self).__init__()
        self.name = name
        self.bases = {}
        self._update_bases = []
#        self.decorators = []

    def add_parent(self, parent):
        self._update_bases.append(parent)
        value = self.bases.get(parent, None)
        if value is None:
            self.bases[parent] = None

    def update_bases(self):
        to_remove = []
        for parent in self.bases:
            if parent not in self._update_bases:
                to_remove.append(parent)
        for remove in to_remove:
            self.bases.pop(parent)

    def get_completion_items(self):
        attributes = [a for a in self.attributes]
        functions = [f for f in self.functions]
        if attributes or functions:
            attributes.sort()
            functions.sort()
            return {'attributes': attributes, 'functions': functions}
        return None

    def update_with_parent_data(self):
        for base in self.bases:
            parent = self.bases[base]
            if parent.__class__ is Clazz:
                self.attributes.update(parent.attributes)
                self.functions.update(parent.functions)
                self.bases[base] = None
            elif isinstance(parent, tuple):
                parent_name = parent[0]
                data = parent[1]
                attributes = {}
                functions = {}
                for attr in data.get('attributes', []):
                    if attr[:2] == '__' and attr[-2:] == '__':
                        continue
                    assign = Assign(attr)
                    assign.add_data(0, parent_name + attr, '',
                                    parent_name + attr)
                    attributes[attr] = assign
                for func in data.get('functions', []):
                    if func[:2] == '__' and func[-2:] == '__':
                        continue
                    assign = Assign(func)
                    assign.add_data(0, parent_name + attr, '',
                                    parent_name + attr)
                    functions[func] = assign
                self.attributes.update(attributes)
                self.functions.update(functions)


class Function(Structure):

    def __init__(self, name):
        super(Function, self).__init__()
        self.name = name
        self.args = {}
        self.decorators = []
        self.return_type = []

    def add_return(self, lineno, data_type, line_content, oper):
        info = _TypeData(lineno, data_type, line_content, oper)
        if info not in self.return_type:
            self.return_type.append(info)

    def get_data_type(self):
        possible = [d.data_type for d in self.return_type
                    if d.data_type is not late_resolution and
                    d.data_type is not None]
        if possible:
            return filter_data_type(possible)
        else:
            return None


class Assign(object):

    def __init__(self, name):
        self.name = name
        self.data = []
        self.parent = None

    def add_data(self, lineno, data_type, line_content, oper):
        info = _TypeData(lineno, data_type, line_content, oper)
        if info not in self.data:
            self.data.append(info)

    def get_data_type(self):
        possible = [d.data_type for d in self.data
                    if d.data_type is not late_resolution]
        if possible:
            return filter_data_type(possible)
        else:
            return None


class LinkedModule(object):

    def __init__(self, path, attrs):
        self.name = path
        self.resolve_attrs = remove_function_arguments(attrs)

    def get_type(self, resolve=''):
        result = {'found': False, 'type': None}
        global MODULES
        module = MODULES.get(self.name, None)
        if module:
            if resolve:
                to_resolve = "%s.%s" % (self.resolve_attrs, resolve)
            else:
                to_resolve = self.resolve_attrs
            to_resolve = to_resolve.split('.', 1)
            main_attr = to_resolve[0]
            child_attr = ''
            if len(to_resolve) == 2:
                child_attr = to_resolve[1]
            result = module.get_type(main_attr, child_attr)
        return result
