#!/usr/bin/env python

import argparse
import fnmatch
import os
import re
import sys

import autopep8
import editorconfig
import lib2to3.refactor


SOURCE_ROOTS = ['av', 'docs', 'examples', 'include', 'scratchpad', 'scripts', 'tests']
SOURCE_EXTS = set('.py .pyx .pxd .rst'.split())


def iter_source_paths():
    for root in SOURCE_ROOTS:
        for dir_path, _, file_names in os.walk(root):
            for file_name in file_names:
                base, ext = os.path.splitext(file_name)
                if base.startswith('.'):
                    continue
                if ext not in SOURCE_EXTS:
                    continue
                yield os.path.abspath(os.path.join(dir_path, file_name))


def apply_editorconfig(source, path):

    config = editorconfig.get_properties(path)

    soft_indent = config.get('indent_style', 'space') == 'space'
    indent_size = int(config.get('indent_size', 4))
    do_trim = config['trim_trailing_whitespace'] == 'true'
    do_final_newline = config['insert_final_newline'] == 'true'

    spaced_indent = ' ' * indent_size

    output = []

    for line in source.splitlines():

        # Apply trim_trailing_whitespace.
        if do_trim:
            line = line.rstrip()

        # Adapt tabs to/from spaces.
        m = re.match(r'(\s+)(.*)', line)
        if m:
            indent, content = m.groups()
            if soft_indent:
                indent.replace('\t', spaced_indent)
            else:
                indent.replace(indent, '\t')
            line = indent + content

        output.append(line)

    while output and not output[-1]:
        output.pop()

    if do_final_newline:
        output.append('')

    return '\n'.join(output)


pep8_fixes_by_ext = {
    pattern: tuple(filter(None, (x.split('#')[0].split('-')[0].strip() for x in value.splitlines())))
    for pattern, value in {
        '*': '''
            E121 - Fix indentation to be a multiple of four.
            E122 - Add absent indentation for hanging indentation.
        ''',
        '.py': '''
            E226 - Fix missing whitespace around arithmetic operator.
            E227 - Fix missing whitespace around bitwise/shift operator.
            E228 - Fix missing whitespace around modulo operator.
            E231 - Add missing whitespace.
            E242 - Remove extraneous whitespace around operator.
            W603 - Use "!=" instead of "<>"
            W604 - Use "repr()" instead of backticks.

            E22  - Fix extraneous whitespace around keywords. # Messes with Cython.
            E241 - Fix extraneous whitespace around keywords. # Messes with Cython.

        ''',
        '.py*': '''
            E224 - Remove extraneous whitespace around operator.
            # E301 - Add missing blank line.
            # E302 - Add missing 2 blank lines.
            # E303 - Remove extra blank lines.
            E251 - Remove whitespace around parameter '=' sign.
            E304 - Remove blank line following function decorator.
            E401 - Put imports on separate lines.
            E20  - Remove extraneous whitespace.
            E211 - Remove extraneous whitespace.
        ''',
    }.items()
}

def apply_autopep8(source, path):

    fixes = set()
    ext = os.path.splitext(path)[1]
    for pattern in pep8_fixes_by_ext:
        if fnmatch.fnmatch(ext, pattern):
            fixes.update(pep8_fixes_by_ext[pattern])

    source = autopep8.fix_code(source, options=dict(
        select=filter(None, fixes),
    ))

    return source



def apply_future(source, path):

    if os.path.splitext(path)[1] not in ('.py', ):
        return source

    m = re.search(r'^from __future__ import ([\w, \t]+)', source, flags=re.MULTILINE)
    if m:
        features = set(x.strip() for x in m.group(1).split(','))
    else:
        features = set()

    fixes = []

    if 'print_function' not in features and re.search(r'^\s*print\s+', source, flags=re.MULTILINE):
        fixes.append('lib2to3.fixes.fix_print')

    if not fixes:
        # Nothing to do.
        return source

    # The parser chokes if the last line is not empty.
    if not source.endswith('\n'):
        source += '\n'

    tool = lib2to3.refactor.RefactoringTool(fixes)
    tree = tool.refactor_string(source, path)
    source = str(tree)

    if 'print' in source:
        features.add('print_function')

    source = 'from __future__ import {}\n{}'.format(', '.join(sorted(features)), source)

    return source




def main():

    parser = argparse.ArgumentParser()
    parser.add_argument('-a', '--all', action='store_true')
    parser.add_argument('-e', '--editorconfig', action='store_true')
    parser.add_argument('-p', '--pep8', action='store_true')
    parser.add_argument('-f', '--future', action='store_true')
    args = parser.parse_args()

    if not (args.all or args.editorconfig or args.pep8 or args.future):
        print("Nothing to do.", file=sys.stderr)
        parser.print_usage()
        exit(1)

    for path in iter_source_paths():

        before = after = open(path).read()
        print(path)

        if args.all or args.pep8:
            after = apply_autopep8(after, path)

        if args.all or args.future:
            after = apply_future(after, path)

        if args.all or args.editorconfig:
            after = apply_editorconfig(after, path)

        if before == after:
            continue

        with open(path, 'w') as fh:
            fh.write(after)


if __name__ == '__main__':
    main()
