# Copyright 2018 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import socket
import threading

# Importing from six to maintain compatibility with Python 2. Safe to
# `import queue` once Tauto transitions fully to Python 3.
from six.moves import queue

_BUF_SIZE = 4096

class FakePrinter():
    """
    A fake printer (server).

    It starts a thread that listens on given localhost's port and saves
    incoming documents in the internal queue. Documents can be fetched from
    the queue by calling the fetch_document() method. At the end, the printer
    must be stopped by calling the stop() method. The stop() method is called
    automatically when the object is managed by "with" statement.
    See test_fake_printer.py for examples.

    """

    def __init__(self, port):
        """
        Initialize fake printer.

        It configures the socket and starts the printer. If no exceptions
        are thrown (the method succeeded), the printer must be stopped by
        calling the stop() method.

        @param port: port number on which the printer is supposed to listen

        @raises socket or thread related exception in case of failure

        """
        # If set to True, the printer is stopped either by invoking stop()
        # method or by an internal error
        self._stopped = False
        # It is set when printer is stopped because of some internal error
        self._error_message = None
        # An internal queue with printed documents
        self._documents = queue.Queue()
        # Create a TCP/IP socket
        self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        try:
            # Bind the socket to the port
            self._socket.bind( ('localhost', port) )
            # Start thread
            self._thread = threading.Thread(target = self._thread_read_docs)
            self._thread.start();
        except:
            # failure - the socket must be closed before exit
            self._socket.close()
            raise


    # These methods allow to use the 'with' statement to automaticaly stop
    # the printer
    def __enter__(self):
        return self
    def __exit__(self, exc_type, exc_value, traceback):
        self.stop()


    def stop(self):
        """
        Stops the printer.

        """
        self._stopped = True
        self._thread.join()


    def fetch_document(self, timeout):
        """
        Fetches the next document from the internal queue.

        This method returns the next document and removes it from the internal
        queue. If there is no documents in the queue, it blocks until one
        arrives. If waiting time exceeds a given timeout, an exception is
        raised.

        @param timeout: max waiting time in seconds

        @returns next document from the internal queue

        @raises Exception if the timeout was reached

        """
        try:
            return self._documents.get(block=True, timeout=timeout)
        except queue.Empty:
            # Builds a message for the exception
            message = 'Timeout occured when waiting for the document. '
            if self._stopped:
                message += 'The fake printer was stopped '
                if self._error_message is None:
                    message += 'by the stop() method.'
                else:
                    message += 'because of the error: %s.' % self._error_message
            else:
                message += 'The fake printer is in valid state.'
            # Raises and exception
            raise Exception(message)


    def _read_whole_document(self):
        """
        Reads a document from the printer's socket.

        It assumes that operation on sockets may timeout.

        @returns whole document or None, if the printer was stopped

        """
        # Accepts incoming connection
        while True:
            try:
                (connection, client_address) = self._socket.accept()
                # success - exit the loop
                break
            except socket.timeout:
                # exit if the printer was stopped, else return to the loop
                if self._stopped:
                    return None

        # Reads document
        document = bytearray()
        while True:
            try:
                data = connection.recv(_BUF_SIZE)
                # success - check data and continue
                if not data:
                    # we got the whole document - exit the loop
                    break
                # save chunk of the document and return to the loop
                document.extend(data)
            except socket.timeout:
                # exit if the printer was stopped, else return to the loop
                if self._stopped:
                    connection.close()
                    return None

        # Closes connection & returns document
        connection.close()
        return bytes(document)


    def _thread_read_docs(self):
        """
        Reads documents from the printer's socket and adds them to the
        internal queue.

        It exits when the printer is stopped by the stop() method.
        In case of any error (exception) it stops the printer and exits.

        """
        try:
            # Listen for incoming printer request.
            self._socket.listen(1)
            # All following socket's methods throw socket.timeout after
            # 500 miliseconds
            self._socket.settimeout(0.5)

            while True:
                # Reads document from the socket
                document = self._read_whole_document()
                # 'None' means that the printer was stopped -> exit
                if document is None:
                    break
                # Adds documents to the internal queue
                self._documents.put(document)
        except BaseException as e:
            # Error occured, the printer must be stopped -> exit
            self._error_message = str(e)
            self._stopped = True

        # Closes socket before the exit
        self._socket.close()
