#                              Ravenbrook
#                     <http://www.ravenbrook.com/>
#
#                P4.PY -- PYTHON INTERFACE TO PERFORCE
#
#             Gareth Rees, Ravenbrook Limited, 2001-03-07

from collections import Iterator
from contextlib import contextmanager
import marshal
import os
import re
from subprocess import Popen, PIPE

class Error(Exception):
    pass

class Command(Iterator):
    """A Perforce command and its output.

    You can iterate over the Command object to yield the outputs of
    the command as dictionaries. The keys and values in these output
    dictionaries are decoded according to the connection's encoding.

        >>> conn = Connection(encoding='utf8')
        >>> for change in conn.run('changes', '-m', '2'):
        ...     print("{change} {desc}".format(**change))
        10021 Explaining how to use the autom
        10020 Archiving new mail

    Iteration raises Error if the output indicates that an error
    occurred.

        >>> print(next(conn.run('help', 'xyzzy'))['data'])
        Traceback (most recent call last):
            ...
        p4.Error: No help for xyzzy.

    Call the send() method to send input to commands like client -i.
    The keys and values in these input dictionaries are encoded
    according to encoding.

        >>> cmd = conn.run('client', '-i')
        >>> cmd.send({'Client': 'abc', 'Root': '/'})
        >>> print(next(cmd)['data'])
        'Client abc saved.'

    Call the done() method to finish executing the Perforce command,
    closing the input, discarding any output, and raising Error if it
    failed. For example:

        >>> conn.run('edit', filespec).done()

    The send() method returns the Command object, to allow method calls to
    be chained:

        >>> conn.run('client', '-i').send(client).done()

    In the common case where run() is immediately followed by done(),
    use the do() method:

        >>> conn.do('submit', '-d', description)

    """
    def __init__(self, *args, **kwargs):
        self.encoding = kwargs.pop('encoding', 'utf8')
        self.pipe = Popen([a.encode('utf8') for a in args],
                          stdin = PIPE, stdout = PIPE)

    def __next__(self):
        # Ensure that stdin is closed before attempting to read any
        # output, to avoid deadlock.
        self.pipe.stdin.close()
        try:
            data = marshal.load(self.pipe.stdout)
        except EOFError:
            raise StopIteration
        def decode(s):
            if isinstance(s, bytes):
                return s.decode(self.encoding, 'replace')
            else:
                return s
        data = {decode(k): decode(v) for k, v in data.items()}
        if data.get('code') == 'error':
            raise Error(data.get('data', ''))
        return data

    next = __next__             # for compatibility with Python 2

    def send(self, data):
        def encode(s):
            if isinstance(s, type(u'')):
                return s.encode(self.encoding)
            else:
                return s
        data = {encode(k): encode(v) for k, v in data.items()}
        marshal.dump(data, self.pipe.stdin, 0)
        return self

    def done(self):
        for _ in self:
            pass

class Connection(object):
    """A connection to a Perforce server.

    The constructor takes these keyword arguments:

    p4 -- the Perforce client executable (default 'p4')
    port -- the server's listen address
    client -- the client name
    user -- the user name
    encoding -- the encoding to use for text (default 'utf8')

    """
    def __init__(self, p4='p4', port=None, client=None, user=None,
                 encoding='utf8'):
        self.args = [p4, '-G']
        if port: self.args.extend(['-p', port])
        if user: self.args.extend(['-u', user])
        if client: self.args.extend(['-c', client])
        self.encoding = encoding
        self.kwargs = dict(p4=p4, port=port, client=client, user=user,
                           encoding=encoding)

    def run(self, *args):
        """Run a Perforce command.

            >>> conn = Connection()
            >>> conn.run('edit', filespec)
            >>> conn.run('submit', '-d', description, filespec)

        Returns a Command object.

        """
        return Command(*(self.args + list(args)), encoding=self.encoding)

    def do(self, *args):
        """Run a Perforce command and consume its output."""
        self.run(*args).done()

    def contents(self, filespec):
        """Return the contents of the file whose specification is given as a
        string. If the file does not exist, raise p4.Error.

        """
        return ''.join(t['data'] for t in self.run('print', filespec)
                       if 'data' in t)

    @contextmanager
    def temp_client(self, client_spec):
        """Return a context manager that creates a temporary client workspace
        on entry and deletes it on exit.

        The client specification should omit the Client and Root keys:
        these are added automatically. The workspace views should use
        __CLIENT__ and this is replaced by the chosen client name.

        This context manager yields a tuple of the new Connection
        object, and the root directory of the client workspace.

        """
        import shutil
        import tempfile
        import uuid
        name = 'tmp-{}'.format(uuid.uuid4())
        root = tempfile.mkdtemp()
        spec = {k: re.sub(r'__CLIENT__', name, v)
                if re.match(r'View\d+$', k) else v
                for k, v in client_spec.items()}
        spec.update(Client=name, Root=root)
        try:
            conn = Connection(**dict(self.kwargs, client=name))
            conn.run('client', '-i').send(spec).done()
            try:
                yield conn, root
            finally:
                try:
                    conn.do('revert', '-k', '//...')
                except Error:
                    pass
                conn.do('client', '-d', name)
        finally:
            shutil.rmtree(root)


# Convenience interface for the default connection.
_conn = Connection()
run = _conn.run
do = _conn.do
contents = _conn.contents
temp_client = _conn.temp_client


# A. REFERENCES
#
# [SUBPROCESS] Python Standard Library: "subprocess -- Subprocess
# management"; <http://docs.python.org/library/subprocess.html>.
#
#
# B. DOCUMENT HISTORY
#
# 2001-05-20 GDR Created.
#
# 2003-02-14 NB Changed os.wait to os.waitpid for Python 2.2.
#
# 2010-10-04 GDR Rewritten to use [SUBPROCESS] instead of os.pipe.
#
# 2010-10-05 GDR Raise an exception if Perforce returns an error. New
# function 'contents' for getting the contents of a file.
#
# 2010-10-06 GDR Move p4client, p4path, p4port, and p4user to global
# variables, to make testing easier. New function 'pipe' makes it
# possible to run the 'client -i' command.
#
# 2013-12-09 GDR Merge pipe and run functions into a Run class that
# also handles encoding and decoding.
#
# 2014-03-18 GDR Refactor into classes: Connection (holding the
# client/server configuration) and Command (a single command and its
# output).
#
# 2014-03-19 GDR New methods Connection.temp_client for creating a
# temporary client workspace, and Connection.do for encapsulating
# run().done().
#
#
# C. COPYRIGHT AND LICENCE
#
# Copyright 2001-2014 Ravenbrook Ltd.  All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the
#    distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
#
# $Id: //info.ravenbrook.com/project/mps/version/1.117/tool/p4.py#1 $