#!/usr/bin/env python3

import json
import pathlib
import shlex
import subprocess
import sys
import termios


DEFAULT_LINTERS = {
    'js': ('eslint', '--stdin'),
    'py': ('flake8', '--ignore=E121,E123,E126,E266,E501,E704,E731', '-'),
    'scss': ('scss-lint', '--stdin-file-path', '{filename}', '-'),
}


class InvalidLinterConfigError(Exception):
    def __init__(self, path):
        self.path = path


class NoLinterFound(Exception):
    pass


class Terminal:
    class ReadChar:
        def __init__(self, file):
            self.file = file

        def readchar(self, default=None):
            b = self.file.read(1)
            c = b.decode()

            if c.isprintable():
                return c
            return default

    def __init__(self, dev='/dev/tty'):
        self.dev = dev
        self.file = None
        self.tcattrs = None

    def __enter__(self):
        self.file = open(self.dev, 'rb', buffering=0)

        try:
            self.set_non_canon()
        except Exception as exc:
            self.reset()
            raise exc

        return self.ReadChar(self.file)

    def __exit__(self, exc_type, exc_value, exc_tb):
        self.reset()
        return False

    def reset(self):
        file = self.file
        tcattrs = self.tcattrs

        self.file = None
        self.tcattrs = None

        if file:
            if tcattrs:
                self.set_tcattrs(tcattrs, fd=file.fileno())
            file.close()

    def set_non_canon(self):
        fd = self.file.fileno()

        self.tcattrs = termios.tcgetattr(fd)
        iflag, oflag, cflag, lflag, ispeed, ospeed, cc = self.tcattrs
        lflag &= ~(termios.ECHO | termios.ICANON)

        self.set_tcattrs([iflag, oflag, cflag, lflag, ispeed, ospeed, cc])

    def set_tcattrs(self, attrs, fd=None):
        if fd is None:
            fd = self.file.fileno()

        termios.tcsetattr(fd, termios.TCSAFLUSH, attrs)


def find_git_project_root():
    path = pathlib.Path.cwd()

    while True:
        parent = path.parent
        if path == parent:
            break

        if (path / '.git').exists():
            return path

        path = parent

    return None


def get_files():
    files = []

    while True:
        line = sys.stdin.readline()
        if not line:
            break

        filename = line.strip()
        files.append(pathlib.Path(filename))

    return files


def get_linter(linters, filename):
    extension = filename.suffix[1:]
    try:
        linter_args = linters[extension]
    except KeyError:
        raise NoLinterFound

    return (arg.format(filename=filename) for arg in linter_args)


def get_linter_config(config_filename='.gitlint.json'):
    git_root = find_git_project_root()
    if git_root:
        config = git_root / config_filename
        if config.exists():
            return config

    config = pathlib.Path.home() / config_filename
    if config.exists():
        return config

    return None


def get_linters():
    config = get_linter_config()
    if config:
        try:
            with config.open() as f:
                return json.load(f)
        except (json.JSONDecodeError, OSError):
            raise InvalidLinterConfigError(config)

    return DEFAULT_LINTERS


def lint(linters, filename, require_linter=False):
    if not filename.exists():
        return True

    try:
        linter_args = get_linter(linters, filename)
    except NoLinterFound:
        print('No linter found for', filename, file=sys.stderr)
        return not require_linter

    print(filename)
    git_args = ('/usr/bin/git', 'show', shlex.quote(':%s' % filename))
    with subprocess.Popen(git_args, stdout=subprocess.PIPE) as pipe:
        ret = subprocess.call(linter_args, stdin=pipe.stdout)

    return ret == 0


def lint_all(linters, files):
    success = False

    while not success:
        success = True

        for filename in files:
            success &= lint(linters, filename)

        if not success:
            retry, skip = prompt_for_retry()

            if not retry:
                success = skip
                break

            print(file=sys.stderr)

    return success


def prompt_for_retry():
    with Terminal() as term:
        while True:
            print('(A)bort, (I)gnore, (R)etry: ',
                  end='', file=sys.stderr, flush=True)
            ans = term.readchar('r')
            print(ans, file=sys.stderr)

            try:
                return {
                    'a': (False, False),
                    'i': (False, True),
                    'r': (True, False),
                }[ans.lower()]
            except KeyError:
                pass


def main():
    from signal import signal, SIGINT, SIGTERM

    class Exit(Exception):
        pass

    exit_signals = (SIGTERM, SIGINT)
    def sig_handler(signum, frame):
        if signum in exit_signals:
            raise Exit

    for signum in exit_signals:
        signal(signum, sig_handler)

    try:
        linters = get_linters()
    except InvalidLinterConfigError as e:
        print('Cannot read linter config: %s' % e.path, file=sys.stderr)
        return 1

    try:
        files = get_files()
        success = lint_all(linters, files)
    except Exit:
        success = False

    return 1 ^ success


if __name__ == '__main__':
    sys.exit(main())
