import hashlib
import itertools
import os
import logging
import collections
import typing as tp
import warnings

import pkg_resources
from .satella import split, find_files
from setuptools import Extension
from .templating import HFILE_MAKO, INITPY_MAKO
from. bootstrap import get_file


CdefSection = collections.namedtuple('CdefSection', ('h_file_name', 'module_name', 'coded_module_name'))
GetDefinitionSection = collections.namedtuple('GetDefinitionSection', (
    'module_name', 'pyinit_name', 'coded_module_name'
))

logger = logging.getLogger(__name__)


def cull_path(path):
    if not path:
        return path
    if path[0] == os.path.sep:
        if len(path) > 1:
            if path[1] == os.path.sep:
                return path[2:]
        return path[1:]
    else:
        return path


LINES_IN_HFILE = len(HFILE_MAKO.split('\n'))


class find_all:
    """
    A directive for :class:`snakehouse.Multibuild` to locate all .pyx
    files, and possibly all the .c files depending on the switch

    :param directory: base directory to look for files in
    :param include_c_files: whether to also hook up the located .c files (default False)
    :param only_c_files: whether to look up only .c files (default False)
    """

    def __init__(self, directory: str, include_c_files: bool = False,
                 only_c_files: bool = False):
        self.dir = directory
        self.include_c_files = include_c_files
        self.only_c_files = only_c_files

    def __iter__(self):
        if self.only_c_files:
            pyx_files = []
        else:
            pyx_files = find_files(self.dir, r'(.*)\.pyx', scan_subdirectories=True)

        if self.include_c_files:
            pyx_files = itertools.chain(pyx_files,
                                        find_files(self.dir, r'(.*)\.c', scan_subdirectories=True))
        return pyx_files

class BreakException(Exception):
    pass
class Multibuild:
    """
    This specifies a single Cython extension, called {extension_name}.__bootstrap__

    All kwargs will be sent straight to Cython's Extension

    :param extension_name: the module name
    :param files: list of pyx and c files
    :param kwargs: extra arguments to be passed to Extension() object
    :param dont_snakehouse: snakehouse won't be enabled, each element will be built
      as a separate extension. It is for these cases when you're testing and something segfaults.
    """
    def __init__(self, extension_name: str, files: tp.Iterator[str],
                 dont_snakehouse: bool = False,
                 **kwargs):
        # sanitize path separators so that Linux-style paths are supported on Windows
        files = list(files)
        self.dont_snakehouse = dont_snakehouse
        self.kwargs = kwargs
        self.clear_all_bootstraps()
        if files:
            files = [os.path.join(*split(file)) for file in files]
            self.files = [file for file in files if not file.endswith('__bootstrap__.pyx') and not file.endswith('.pyc')]
            self.pyx_files = [file for file in self.files if file.endswith('.pyx')]
            self.c_files = [file for file in self.files if file.endswith('.c') or file.endswith('.cpp')]
        else:
            self.pyx_files = []
            self.c_files = []

        self.files = list(set(self.files))
        self.pyx_files = list(set(self.pyx_files))
        self.c_files = list(set(self.c_files))

        self.do_generate = True
        if not self.pyx_files:
            warnings.warn('No pyx files, probably installing from a source archive, skipping '
                          'generating files', RuntimeWarning)
            self.do_generate = False
        else:
            self.extension_name = extension_name        # type: str
            if len(self.files) == 1:
                self.bootstrap_directory, _ = os.path.split(self.files[0])      # type: str
            else:
                self.bootstrap_directory = os.path.commonpath(self.files)       # type: str
            self.modules = []    # type: tp.List[tp.Tuple[str, str, str]]
            self.module_name_to_loader_function = {}
            for filename in self.pyx_files:
                with open(filename, 'rb') as f_in:
                    self.module_name_to_loader_function[filename] = hashlib.sha256(f_in.read()).hexdigest()

    def fix_all_c_files(self):
        return
        for root, dirs, files in os.walk("."):
            try:
                for c_name in files:
                    for allowed in ['.h', '.c', '.pyx', '.pxd']:
                        if not c_name.endswith(allowed):
                            raise BreakException
                    name = os.path.join(root, c_name)
                    if c_name == '__bootstrap__.c':
                        continue

                    if not c_name.endswith('.c'):
                        continue
                    with open(name, 'r', encoding='utf-8') as fin:
                        data = fin.read()
                        is_windows = '\r\n' in data

                    with open(name, 'w', encoding='utf-8') as fout:
                        c_name = c_name.replace('.c', '.h')
                        fout.write('#include "%s"' % (c_name, ))
                        fout.write('\r\n' if is_windows else '\n')
                        fout.write(data)
            except BreakException:
                pass

    def generate_header_files(self):
        for filename in self.pyx_files:
            path, name, cmod_name_path, module_name, coded_module_name, complete_module_name = self.transform_module_name(filename)
            if not name.endswith('.pyx'):
                continue

            h_file = filename.replace('.pyx', '.h')

            data = HFILE_MAKO.format(initpy_name=coded_module_name)
            with open(h_file, 'w', encoding='utf-8') as f_out:
                f_out.write(data)

    def transform_module_name(self, filename):
        path, name = os.path.split(filename)
        module_name = name.replace('.pyx', '')
        if path.startswith(self.bootstrap_directory):
            cmod_name_path = path[len(self.bootstrap_directory):]
        else:
            cmod_name_path = path
        path = cull_path(path)
        cmod_name_path = cull_path(cmod_name_path)

        if path:
            intro = '.'.join((e for e in cmod_name_path.split(os.path.sep) if e))
            if not intro:
                complete_module_name = '%s.%s' % (self.extension_name, module_name)
            else:
                complete_module_name = '%s.%s.%s' % (self.extension_name,
                                                     intro,
                                                     module_name)
        else:
            complete_module_name = '%s.%s' % (self.extension_name, module_name)

        coded_module_name = self.module_name_to_loader_function[filename]
        return path, name, cmod_name_path, module_name, coded_module_name, complete_module_name

    def do_after_cython(self):
        if self.dont_snakehouse:
            return
        self.generate_header_files()
        for filename in self.pyx_files:
            path, name, cmod_name_path, module_name, coded_module_name, complete_module_name = self.transform_module_name(filename)
            to_replace = '__Pyx_PyMODINIT_FUNC PyInit_%s' % (module_name, )
            replace_with = '__Pyx_PyMODINIT_FUNC PyInit_%s' % (coded_module_name, )
            with open(filename.replace('.pyx', '.c'), 'r') as f_in:
                data_in = f_in.read()
                data = data_in.replace(to_replace, replace_with)
            with open(filename.replace('.pyx', '.c'), 'w') as f_out:
                f_out.write(data)

    def generate_bootstrap(self) -> str:

        cdef_section = []
        for filename in self.pyx_files:
            path, name, cmod_name_path, module_name, coded_module_name, complete_module_name = self.transform_module_name(filename)

            if os.path.exists(filename.replace('.pyx', '.c')):
                os.unlink(filename.replace('.pyx', '.c'))

            h_path_name = os.path.join(cmod_name_path, name.replace('.pyx', '.h')).replace('\\', '\\\\')

            cdef_section.append(CdefSection(h_path_name, module_name, coded_module_name))

            self.modules.append((complete_module_name, module_name, coded_module_name))

        get_definition = []
        for mod_name, init_fun_name, coded_module_name in self.modules:
            get_definition.append(GetDefinitionSection(mod_name, init_fun_name, coded_module_name))
        return get_file(cdef_sections=cdef_section, get_definition_sections=get_definition,
                        module_set=[x[0] for x in self.modules])

    def clear_all_bootstraps(self):
        for root, dirs, files in os.walk('.'):
            for file in files:
                c_name = os.path.join(root, file)
                if file.startswith('__bootstrap__'):
                    os.unlink(c_name)

    def write_bootstrap_file(self):
        with open(os.path.join(self.bootstrap_directory, '__bootstrap__.pyx'), 'w', encoding='utf-8') as f_out:
            f_out.write(self.generate_bootstrap())

    def alter_init(self):
        if os.path.exists(os.path.join(self.bootstrap_directory, '__init__.py')):
            with open(os.path.join(self.bootstrap_directory, '__init__.py'), 'r', encoding='utf-8') as f_in:
                data = f_in.read()
        else:
            data = ''

        if 'bootstrap_cython_submodules' not in data:
            data = INITPY_MAKO.format(module_name=self.extension_name) + data

        with open(os.path.join(self.bootstrap_directory, '__init__.py'), 'w', encoding='utf-8') as f_out:
            f_out.write(data)

    def generate(self):
        if not self.dont_snakehouse and self.do_generate:
            self.write_bootstrap_file()
            self.alter_init()

    def for_cythonize(self, debug: bool = False, *args, **kwargs):
        if self.dont_snakehouse:
            extensions = []
            len_to_sub = len(self.bootstrap_directory) + len(os.path.pathsep)
            for pyx_file in self.pyx_files:
                file_name = pyx_file[len_to_sub:-4].replace('\\', '.').replace('/', '.')
                ext = Extension(self.extension_name+'.'+file_name,
                                [pyx_file] + self.c_files, *args, **kwargs)
                extensions.append(ext)
            return extensions
        else:
            kwargs.update(self.kwargs)
            self.files = [file for file in self.files if file.endswith('.pyx')]
            self.files.extend(self.c_files)
            self.files.append(os.path.join(self.bootstrap_directory, '__bootstrap__.pyx'))
            return [Extension(self.extension_name+".__bootstrap__",
                              self.files,
                              *args,
                              **kwargs)]
