#!/usr/bin/env python
from __future__ import print_function

import os
from subprocess import check_call, CalledProcessError

import click
import sys

from mutmut import mutate, ALL, count_mutations, mutate_file, config
from shutil import move


# this function is stolen and modified from tqdm
def status_printer(file):
    """
    Manage the printing and in-place updating of a line of characters.
    Note that if the string is longer than a line, then in-place
    updating may not work (it will print a new line at each refresh).
    """
    fp = file

    last_len = [0]

    def print_status(s):
        len_s = len(s)
        fp.write('\r' + s + (' ' * max(last_len[0] - len_s, 0)))
        fp.flush()
        last_len[0] = len_s
    return print_status

print_status = status_printer(sys.stdout)


@click.command()
@click.argument('paths_to_mutate', nargs=-1)
@click.option('--apply', help='apply the mutation to the given file. Must be used in combination with --mutation_number', is_flag=True)
@click.option('--backup/--no-backup', default=False)
@click.option('--mutation', type=click.INT)
@click.option('--runner')
@click.option('--use-coverage', is_flag=True, default=False)
@click.option('--testsdir')
@click.option('-s', help='turn off output capture', is_flag=True)
def main(paths_to_mutate, apply, mutation, backup, runner, testsdir, s, use_coverage):
    # defaults from setup.cfg
    paths_to_mutate = paths_to_mutate or config.paths_to_mutate
    runner = runner or config.runner
    testsdir = testsdir or config.tests_dirs

    if not paths_to_mutate:
        print('You must specify a list of paths to mutate. Either as a command line argument, or by setting paths_to_mutate under the section [mutmut] in setup.cfg')
        return

    os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
    if apply:
        assert mutation is not None
        assert len(paths_to_mutate) == 1
        mutations_performed = mutate_file(backup, mutation, paths_to_mutate[0])
        if mutations_performed == 0:
            print('ERROR: no mutations performed. Are you sure the index is not too big?')
        return

    null_stdout = open('/dev/null', 'w') if not s else None
    null_stderr = open('/dev/null', 'w') if not s else None

    test_command = '%s %s' % (runner, testsdir)

    def run_tests():
        check_call(test_command, shell=True, stdout=null_stdout, stderr=null_stderr)

    try:
        run_tests()
    except CalledProcessError:
        print("Tests don't run cleanly without mutations. Test command was: %s" % test_command)
        return

    coverage_data = None
    if use_coverage:
        print('Using coverage data from .coverage file')
        import coverage
        coverage_data = coverage.CoverageData()
        coverage_data.read_file('.coverage')

    def exclude(context):
        if use_coverage:
            measured_lines = coverage_data.lines(os.path.abspath(context.filename))
            if measured_lines is None:
                return True
            if context.current_line not in measured_lines:
                return True

        return False

    mutations_by_file = {}

    for path in paths_to_mutate:
        for filename in python_source_files(path):
            mutations_by_file[filename] = count_mutations(open(filename).read(), context__filename=filename, context__exclude=exclude)

    total = sum(mutations_by_file.values())

    print('--- starting mutation ---')
    progress = 0
    for filename, mutations in mutations_by_file.items():
        # print filename
        for mutation_index in range(mutations):
            if mutation is not None and mutation != mutation_index:
                continue
            progress += 1
            print_status('%s out of %s  (file: %s, mutation: %s)' % (progress, total, filename, mutation_index))
            # result = check_call('py.test tests/ --mutate="%s:%s" -s' % (filename, mutation_index), shell=True)  # TODO: fix plugin, then use this instead
            try:
                assert mutate_file(backup=True, mutation=mutation_index, filename=filename, context__exclude=exclude)
                try:
                    run_tests()
                    print_status('')
                    print('\rFAILED: mutmut %s --mutation %s --apply' % (filename, mutation_index))
                except CalledProcessError as e:
                    pass
                os.remove(filename)
                try:
                    os.remove(filename+'c')  # remove .pyc file
                except OSError:
                    pass
            finally:
                try:
                    move(filename+'.bak', filename)
                except IOError:
                    pass


def python_source_files(path):
    for root, dirs, files in os.walk(path):
        for filename in files:
            if filename.endswith('.py'):
                yield os.path.join(root, filename)


def number_of_mutations(path):
    total = 0
    for filename in python_source_files(path):
        _, c = mutate(open(filename).read(), ALL)
        total += c
    return total

if __name__ == '__main__':
    main()
