from __future__ import unicode_literals

import contextlib
import io
import logging
import os.path
import time

from pre_commit import git
from pre_commit.util import CalledProcessError
from pre_commit.util import cmd_output
from pre_commit.util import mkdirp
from pre_commit.xargs import xargs


logger = logging.getLogger('pre_commit')


def _git_apply(patch):
    args = ('apply', '--whitespace=nowarn', patch)
    try:
        cmd_output('git', *args, encoding=None)
    except CalledProcessError:
        # Retry with autocrlf=false -- see #570
        cmd_output('git', '-c', 'core.autocrlf=false', *args, encoding=None)


@contextlib.contextmanager
def _intent_to_add_cleared():
    intent_to_add = git.intent_to_add_files()
    if intent_to_add:
        logger.warning('Unstaged intent-to-add files detected.')

        xargs(('git', 'rm', '--cached', '--'), intent_to_add)
        try:
            yield
        finally:
            xargs(('git', 'add', '--intent-to-add', '--'), intent_to_add)
    else:
        yield


@contextlib.contextmanager
def _unstaged_changes_cleared(patch_dir):
    tree = cmd_output('git', 'write-tree')[1].strip()
    retcode, diff_stdout_binary, _ = cmd_output(
        'git', 'diff-index', '--ignore-submodules', '--binary',
        '--exit-code', '--no-color', '--no-ext-diff', tree, '--',
        retcode=None,
        encoding=None,
    )
    if retcode and diff_stdout_binary.strip():
        patch_filename = 'patch{}'.format(int(time.time()))
        patch_filename = os.path.join(patch_dir, patch_filename)
        logger.warning('Unstaged files detected.')
        logger.info(
            'Stashing unstaged files to {}.'.format(patch_filename),
        )
        # Save the current unstaged changes as a patch
        mkdirp(patch_dir)
        with io.open(patch_filename, 'wb') as patch_file:
            patch_file.write(diff_stdout_binary)

        # Clear the working directory of unstaged changes
        cmd_output('git', 'checkout', '--', '.')
        try:
            yield
        finally:
            # Try to apply the patch we saved
            try:
                _git_apply(patch_filename)
            except CalledProcessError:
                logger.warning(
                    'Stashed changes conflicted with hook auto-fixes... '
                    'Rolling back fixes...',
                )
                # We failed to apply the patch, presumably due to fixes made
                # by hooks.
                # Roll back the changes made by hooks.
                cmd_output('git', 'checkout', '--', '.')
                _git_apply(patch_filename)
            logger.info('Restored changes from {}.'.format(patch_filename))
    else:
        # There weren't any staged files so we don't need to do anything
        # special
        yield


@contextlib.contextmanager
def staged_files_only(patch_dir):
    """Clear any unstaged changes from the git working directory inside this
    context.
    """
    with _intent_to_add_cleared(), _unstaged_changes_cleared(patch_dir):
        yield
