# -*- coding: utf-8 -*-
from difflib import Differ
from isort import SortImports
from os.path import expanduser
from testfixtures import OutputCapture

import os


try:
    from configparser import ConfigParser as SafeConfigParser
except ImportError:
    from ConfigParser import SafeConfigParser


class Flake8Isort(object):
    name = 'flake8_isort'
    version = '2.3'
    isort_unsorted = (
        'I001 isort found an import in the wrong position'
    )
    no_config_msg = (
        'I002 no configuration found (.isort.cfg or [isort] in configs)'
    )
    isort_blank_req = (
        'I003 isort expected 1 blank line in imports, found 0'
    )
    isort_blank_unexp = (
        'I004 isort found an unexpected blank line in imports'
    )
    isort_add_unexp = (
        'I005 isort found an unexpected missing import'
    )

    config_file = None
    show_traceback = False
    stdin_display_name = None

    def __init__(self, tree, filename, lines, search_current=True):
        self.filename = filename
        self.lines = lines
        self.search_current = search_current

    @classmethod
    def add_options(cls, parser):
        parser.add_option(
            '--no-isort-config',
            action='store_true',
            parse_from_config=True,
            help='Do not require explicit configuration to be found'
        )

        parser.add_option(
            '--isort-show-traceback',
            action='store_true',
            parse_from_config=True,
            help='Show full traceback with diff from isort'
        )

    @classmethod
    def parse_options(cls, options):
        if options.no_isort_config is None:
            cls.config_file = True
        else:
            cls.config_file = False

        cls.stdin_display_name = options.stdin_display_name
        cls.show_traceback = options.isort_show_traceback

    def run(self):
        settings_file = self.search_isort_config()
        if self.config_file and not settings_file:
            yield 0, 0, self.no_config_msg, type(self)
        else:
            if self.filename is not self.stdin_display_name:
                file_path = self.filename
            else:
                file_path = None
            with OutputCapture() as buffer:
                sort_result = SortImports(
                    file_path=file_path,
                    file_contents=''.join(self.lines),
                    check=True,
                    settings_path=settings_file,
                    show_diff=True,
                )
            traceback = self._format_isort_output(buffer)

            for line_num, message in self.sortimports_linenum_msg(sort_result):
                if self.show_traceback:
                    message += traceback
                yield line_num, 0, message, type(self)

    def search_isort_config(self):
        # type: () -> Optional[str]  # noqa: F821
        """Search for isort configuration all the way up to the root folder

        Looks for ``.isort.cfg``, ``.editorconfig`` or ``[isort]`` section in
        ``setup.cfg`` or ``tox.ini`` config files.
        """
        full_path = os.path.abspath(self.filename)
        split_path = (os.path.dirname(full_path), True)
        while split_path[1]:
            config_on_file = self._search_config_on_path(split_path[0])
            if config_on_file:
                return config_on_file
            split_path = os.path.split(split_path[0])

        if self.search_current:
            return self.search_isort_config_at_current()

        # last attempt, check home folder
        home = expanduser('~')
        config_on_home = self._search_config_on_path(home)
        if config_on_home:
            return config_on_home

        return None

    def search_isort_config_at_current(self):
        # type: () -> Optional[str]  # noqa: F821
        """Search for isort configuration at current directory"""
        return self._search_config_on_path(os.path.realpath('.'))

    def _search_config_on_path(self, path):
        # type: (str) -> Optional[str]  # noqa: F821
        """Search for isort configuration files at the specifed path.

        Args:
            path: The path to search for config files on.

        Return:
            str: the isort config if found otherwise, None
        """
        for config_file in ('.isort.cfg', '.editorconfig'):
            config_file_path = os.path.join(path, config_file)
            if os.path.isfile(config_file_path):
                return config_file_path

        # Check for '[isort]' section in other configuration files.
        for config_file in ('tox.ini', 'setup.cfg'):
            config_file_path = os.path.join(path, config_file)
            config = SafeConfigParser()
            config.read(config_file_path)
            if 'isort' in config.sections():
                return config_file_path

        return None

    def sortimports_linenum_msg(self, sort_result):
        """Parses isort.SortImports for line number changes and message

        Uses a diff.Differ comparison of SortImport `in_lines`:`out_lines` to
        yield the line numbers of import lines that have been moved or blank
        lines added.

        Args:
            sort_imports (isort.SortImports): The isorts results object.

        Yields:
            tuple: A tuple of the specific isort line number and message.
        """
        if sort_result.skipped:
            return

        self._fixup_sortimports_wrapped(sort_result)
        self._fixup_sortimports_eof(sort_result)

        differ = Differ()
        diff = differ.compare(sort_result.in_lines, sort_result.out_lines)

        line_num = 0
        additions = {
            '+ {}'.format(add_import) for add_import in sort_result.add_imports
        }
        for line in diff:
            if line.startswith('  ', 0, 2):
                line_num += 1  # Ignore unchanged lines but increment line_num.
            elif line.startswith('- ', 0, 2):
                line_num += 1
                if line.strip() == '-':
                    yield line_num, self.isort_blank_unexp
                else:
                    yield line_num, self.isort_unsorted
            elif line.strip() == '+':
                # Include newline additions but do not increment line_num.
                yield line_num + 1, self.isort_blank_req
            elif line.strip() in additions:
                yield line_num + 1, self.isort_add_unexp

    def _format_isort_output(self, isort_buffer):
        filtering_out = ('+++', '---', '@@', 'ERROR:')

        valid_lines = ['']
        valid_lines += [
            line
            for line in isort_buffer.output.getvalue().splitlines()
            if line.strip().split(' ', 1)[0] not in filtering_out
        ]

        # Normalizing newlines:
        if len(valid_lines) > 1:
            valid_lines.insert(1, '')
        valid_lines.append('')

        return '\n'.join(valid_lines)

    @staticmethod
    def _fixup_sortimports_eof(sort_imports):
        """Ensure single end-of-file newline in `isort.SortImports.in_lines`

        isort fixes EOF blank lines but this change should be suppressed as
        Flake8 will also flag them.

        Args:
            sort_imports (isort.SortImports): The isorts results object.

        Returns:
            isort.SortImports: The modified isort results object.
        """
        to_remove = {''} | set(sort_imports.add_imports)
        for line in reversed(sort_imports.in_lines):
            if line.strip() in to_remove:
                # If single empty line in in_lines, do nothing.
                if len(sort_imports.in_lines) > 1:
                    sort_imports.in_lines.pop()
            else:
                sort_imports.in_lines.append('')
                break

    @staticmethod
    def _fixup_sortimports_wrapped(sort_imports):
        """Split-up wrapped imports newlines in `SortImports.out_lines`

        isort combines wrapped lines into a single list entry string in
        `out_lines` whereas `in_lines` are separate strings so for diff
        comparison these need to be comparable.

        Args:
            sort_imports (isort.SortImports): The isorts results object.

        Returns:
            isort.SortImports: The modified isort results object.
        """
        for idx, line in enumerate(sort_imports.out_lines):
            if '\n' in line:
                for new_idx, new_line in enumerate(
                        sort_imports.out_lines.pop(idx).splitlines()):
                    sort_imports.out_lines.insert(idx + new_idx, new_line)
