from __future__ import unicode_literals

import os
import os.path
import subprocess


class CalledProcessError(RuntimeError):
    def __init__(self, returncode, cmd, expected_returncode, output=None):
        super(CalledProcessError, self).__init__(
            returncode, cmd, expected_returncode, output,
        )
        self.returncode = returncode
        self.cmd = cmd
        self.expected_returncode = expected_returncode
        self.output = output

    def __str__(self):
        return (
            'Command: {0!r}\n'
            'Return code: {1}\n'
            'Expected return code: {2}\n'
            'Output: {3!r}\n'.format(
                self.cmd,
                self.returncode,
                self.expected_returncode,
                self.output,
            )
        )


def _replace_cmd(cmd, **kwargs):
    return [part.format(**kwargs) for part in cmd]


class PrefixedCommandRunner(object):
    """A PrefixedCommandRunner allows you to run subprocess commands with
    comand substitution.

    For instance:
        PrefixedCommandRunner('/tmp/foo').run(['{prefix}foo.sh', 'bar', 'baz'])

    will run ['/tmp/foo/foo.sh', 'bar', 'baz']
    """

    def __init__(
            self,
            prefix_dir,
            popen=subprocess.Popen,
            makedirs=os.makedirs
    ):
        self.prefix_dir = prefix_dir.rstrip(os.sep) + os.sep
        self.__popen = popen
        self.__makedirs = makedirs

    def _create_path_if_not_exists(self):
        if not os.path.exists(self.prefix_dir):
            self.__makedirs(self.prefix_dir)

    def run(self, cmd, retcode=0, stdin=None, encoding='UTF-8', **kwargs):
        popen_kwargs = {
            'stdin': subprocess.PIPE,
            'stdout': subprocess.PIPE,
            'stderr': subprocess.PIPE,
        }
        if stdin is not None:
            stdin = stdin.encode('UTF-8')

        popen_kwargs.update(kwargs)
        self._create_path_if_not_exists()
        replaced_cmd = _replace_cmd(cmd, prefix=self.prefix_dir)
        proc = self.__popen(replaced_cmd, **popen_kwargs)
        stdout, stderr = proc.communicate(stdin)
        if encoding is not None:
            stdout = stdout.decode(encoding)
        if encoding is not None:
            stderr = stderr.decode(encoding)
        returncode = proc.returncode

        if retcode is not None and retcode != returncode:
            raise CalledProcessError(
                returncode, replaced_cmd, retcode, output=(stdout, stderr),
            )

        return proc.returncode, stdout, stderr

    def path(self, *parts):
        path = os.path.join(self.prefix_dir, *parts)
        return os.path.normpath(path)

    def exists(self, *parts):
        return os.path.exists(self.path(*parts))

    @classmethod
    def from_command_runner(cls, command_runner, path_end):
        """Constructs a new command runner from an existing one by appending
        `path_end` to the command runner's prefix directory.
        """
        return cls(
            command_runner.path(path_end),
            popen=command_runner.__popen,
            makedirs=command_runner.__makedirs,
        )
