# -*- coding: utf-8 -*-
#
# Copyright © Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see spyder/__init__.py for details)

"""
Source code analysis utilities
"""

import sys
import re
import os
import tempfile
import traceback

# Local import
from spyder.config.base import _, DEBUG
from spyder.utils import programs, encoding
from spyder.py3compat import to_text_string, to_binary_string, PY3
from spyder import dependencies
DEBUG_EDITOR = DEBUG >= 3

#==============================================================================
# Pyflakes/pep8 code analysis
#==============================================================================
TASKS_PATTERN = r"(^|#)[ ]*(TODO|FIXME|XXX|HINT|TIP|@todo|" \
                r"HACK|BUG|OPTIMIZE|!!!|\?\?\?)([^#]*)"

#TODO: this is a test for the following function
def find_tasks(source_code):
    """Find tasks in source code (TODO, FIXME, XXX, ...)"""
    results = []
    for line, text in enumerate(source_code.splitlines()):
        for todo in re.findall(TASKS_PATTERN, text):
            todo_text = (todo[-1].strip(' :').capitalize() if todo[-1]
                         else todo[-2])
            results.append((todo_text, line + 1))
    return results


def check_with_pyflakes(source_code, filename=None):
    """Check source code with pyflakes
    Returns an empty list if pyflakes is not installed"""
    try:
        if filename is None:
            filename = '<string>'
        try:
            source_code += '\n'
        except TypeError:
            # Python 3
            source_code += to_binary_string('\n')
            
        import _ast
        from pyflakes.checker import Checker
        # First, compile into an AST and handle syntax errors.
        try:
            tree = compile(source_code, filename, "exec", _ast.PyCF_ONLY_AST)
        except SyntaxError as value:
            # If there's an encoding problem with the file, the text is None.
            if value.text is None:
                results = []
            else:
                results = [(value.args[0], value.lineno)]
        except (ValueError, TypeError):
            # Example of ValueError: file contains invalid \x escape character
            # (see https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=674797)
            # Example of TypeError: file contains null character
            # (see https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=674796)
            results = []
        else:
            # Okay, it's syntactically valid.  Now check it.
            w = Checker(tree, filename)
            w.messages.sort(key=lambda x: x.lineno)
            results = []
            coding = encoding.get_coding(source_code)
            lines = source_code.splitlines()
            for warning in w.messages:
                if 'analysis:ignore' not in \
                   to_text_string(lines[warning.lineno-1], coding):
                    results.append((warning.message % warning.message_args,
                                    warning.lineno))
    except Exception:
        # Never return None to avoid lock in spyder/widgets/editor.py
        # See Issue 1547
        results = []
        if DEBUG_EDITOR:
            traceback.print_exc()  # Print exception in internal console
    return results

# Required version:
# Why 0.5 (Python2)? Because it's based on _ast (thread-safe)
PYFLAKES_REQVER = '>=0.6.0' if PY3 else '>=0.5.0'
dependencies.add("pyflakes", _("Real-time code analysis on the Editor"),
                 required_version=PYFLAKES_REQVER)

PYCODESTYLE_REQVER = '>=2.3'
dependencies.add("pycodestyle", _("Real-time code style analysis on the Editor"),
                 required_version=PYCODESTYLE_REQVER)


def is_pyflakes_installed():
    """Return True if pyflakes required version is installed"""
    return programs.is_module_installed('pyflakes', PYFLAKES_REQVER)


def get_checker_executable(name):
    """Return checker executable in the form of a list of arguments
    for subprocess.Popen"""
    if programs.is_program_installed(name):
        # Checker is properly installed
        return [name]
    else:
        path1 = programs.python_script_exists(package=None,
                                              module=name+'_script')
        path2 = programs.python_script_exists(package=None, module=name)
        if path1 is not None:  # checker_script.py is available
            # Checker script is available but has not been installed
            # (this may work with pyflakes)
            return [sys.executable, path1]
        elif path2 is not None:  # checker.py is available
            # Checker package is available but its script has not been
            # installed (this works with pycodestyle but not with pyflakes)
            return [sys.executable, path2]


def check(args, source_code, filename=None, options=None):
    """Check source code with checker defined with *args* (list)
    Returns an empty list if checker is not installed"""
    if args is None:
        return []
    if options is not None:
        args += options
    if any(['pyflakes' in arg for arg in args]):
        #  Pyflakes requires an ending new line (pycodestyle don't! -- see Issue 1123)
        #  Note: this code is not used right now as it is faster to invoke 
        #  pyflakes in current Python interpreter (see `check_with_pyflakes` 
        #  function above) than calling it through a subprocess
        source_code += '\n'
    if filename is None:
        # Creating a temporary file because file does not exist yet 
        # or is not up-to-date
        tempfd = tempfile.NamedTemporaryFile(suffix=".py", delete=False)
        tempfd.write(source_code)
        tempfd.close()
        args.append(tempfd.name)
    else:
        args.append(filename)
    cmd = args[0]
    cmdargs = args[1:]
    proc = programs.run_program(cmd, cmdargs)
    output = proc.communicate()[0].strip().decode().splitlines()
    if filename is None:
        os.unlink(tempfd.name)
    results = []
    coding = encoding.get_coding(source_code)
    lines = source_code.splitlines()
    for line in output:
        lineno = int(re.search(r'(\:[\d]+\:)', line).group()[1:-1])
        try:
            text = to_text_string(lines[lineno-1], coding)
        except TypeError:
            text = to_text_string(lines[lineno-1])
        except UnicodeDecodeError:
            # Needed to handle UnicodeDecodeError and force the use
            # of chardet to detect enconding. See issue 6970
            coding = encoding.get_coding(source_code, force_chardet=True)
            text = to_text_string(lines[lineno-1], coding)
        if 'analysis:ignore' not in text:
            message = line[line.find(': ')+2:]
            results.append((message, lineno))
    return results


def check_with_pep8(source_code, filename=None):
    """Check source code with pycodestyle"""
    try:
        args = get_checker_executable('pycodestyle')
        results = check(args, source_code, filename=filename, options=['-r'])
    except Exception:
        # Never return None to avoid lock in spyder/widgets/editor.py
        # See Issue 1547
        results = []
        if DEBUG_EDITOR:
            traceback.print_exc()  # Print exception in internal console
    return results


if __name__ == '__main__':
#    fname = __file__
    fname = os.path.join(os.path.dirname(__file__),
                         os.pardir, os.pardir, 'bootstrap.py')
    code = open(fname).read()
    check_results = check_with_pyflakes(code, fname)+\
                    check_with_pep8(code, fname)+find_tasks(code)
#    check_results = check_with_pep8(code, fname)
    for message, line in check_results:
        sys.stdout.write("Message: %s -- Line: %s\n" % (message, line))
