#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# xpraforwarder - A CUPS backend written in Python.
# Forwards the print job via xpra.
#
# It is based on pdf2email by Georde Notaras.
#
# Copyright (c) George Notaras <George [D.O.T.] Notaras [A.T.] gmail [D.O.T.] com>
# Copyright (C) 2014-2017 Antoine Martin <antoine@xpra.org>
#
# License: GPLv2
#
# This program is released with absolutely no warranty, expressed or implied,
# and absolutely no support.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the:
#
# Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston,
# MA 02111-1307  USA
#


import sys
import os
import syslog
import traceback
import subprocess
from urllib.parse import urlparse, parse_qs


__version__ = "6.4.4"


# Writes a syslog entry (msg) at the default facility:
def debug(msg):
    syslog.syslog(syslog.LOG_DEBUG, msg)


def info(msg):
    syslog.syslog(syslog.LOG_INFO, msg)


def err(msg):
    syslog.syslog(syslog.LOG_ERR, msg)


def noop(*_args):
    pass


write_back = noop


def init_write_back_fn():
    from ctypes.util import find_library
    soname = find_library("cups")
    if not soname:
        err("Error: failed to locate the `cups` shared library")
        return
    from ctypes import cdll
    try:
        cups_lib = cdll.LoadLibrary(soname)
    except OSError as e:
        err(f"Error: failed to load {soname!r}: {e}")
        return
    try:
        cupsBackChannelWrite = cups_lib.cupsBackChannelWrite
    except AttributeError as e:
        err(f"Error: {e}")
        return
    from ctypes import c_size_t, c_char_p, c_double
    cupsBackChannelWrite.argtypes = [c_char_p, c_size_t, c_double]
    cupsBackChannelWrite.restype = c_size_t

    def _write_back(msg):
        bmsg = msg.encode()
        r = cupsBackChannelWrite(bmsg, len(bmsg), 1.0)
        return r
    global write_back
    write_back = _write_back


def exec_command(command, env):
    info("running: %s" % command)
    PIPE = subprocess.PIPE
    proc = subprocess.Popen(command, stdout=PIPE, stderr=PIPE, env=env)
    out,err = proc.communicate()
    info("returncode=%s" % proc.returncode)
    if proc.returncode!=0:
        info("stdout=%s" % out)
        info("stderr=%s" % err)
    return proc.returncode


def xpra_print(socket_path, socket_dir, password_file, encryption, encryption_keyfile,
               display, filename, mimetype, source, title, printer, no_copies, print_options):
    if socket_path:
        # use direct connection to a given socket path:
        command = ["xpra", "print", "socket://%s" % socket_path]
    else:
        # use the display and hope we can find its corresponding socket:
        command = ["xpra", "print", display]
        if socket_dir:
            command.append("--socket-dir=%s" % socket_dir)
    if encryption:
        command += ["--encryption=%s" % encryption,
                    "--encryption-keyfile=%s" % encryption_keyfile]
    if password_file:
        command += ["--password-file=%s" % password_file]
    command += [filename, mimetype, source, title, printer, no_copies, print_options]
    # in this case, running as root cannot be avoided, so skip the warning:
    # (using "su" causes even more problems)
    env = os.environ.copy()
    env["XPRA_NO_ROOT_WARNING"] = "1"
    write_back("INFO: contacting xpra server")
    r = exec_command(command, env=env)
    if r==0:
        write_back("INFO: forwarding")
        return 0
    from xpra.exit_codes import exit_str
    write_back(f"ERROR: contacting the xpra server: {exit_str(r)!r}")
    return cups_backend_exit_status(r)


def cups_backend_exit_status(r: int) -> int:
    CUPS_BACKEND_OK = 0
    CUPS_BACKEND_FAILED = 1
    CUPS_BACKEND_AUTH_REQUIRED = 2
    CUPS_BACKEND_HOLD = 3
    CUPS_BACKEND_STOP = 4
    CUPS_BACKEND_CANCEL = 5
    CUPS_BACKEND_RETRY = 6
    # CUPS_BACKEND_RETRY_CURRENT = 7
    from xpra.exit_codes import ExitCode
    return {
        int(ExitCode.OK): CUPS_BACKEND_OK,
        int(ExitCode.TIMEOUT): CUPS_BACKEND_HOLD,
        int(ExitCode.AUTHENTICATION_FAILED): CUPS_BACKEND_AUTH_REQUIRED,
        int(ExitCode.PASSWORD_REQUIRED): CUPS_BACKEND_AUTH_REQUIRED,
        int(ExitCode.PASSWORD_FILE_ERROR): CUPS_BACKEND_AUTH_REQUIRED,
        int(ExitCode.ENCRYPTION): CUPS_BACKEND_AUTH_REQUIRED,
        int(ExitCode.PACKET_FAILURE): CUPS_BACKEND_STOP,
        int(ExitCode.INCOMPATIBLE_VERSION): CUPS_BACKEND_STOP,
        int(ExitCode.UNSUPPORTED): CUPS_BACKEND_STOP,
        int(ExitCode.INTERNAL_ERROR): CUPS_BACKEND_STOP,
        int(ExitCode.COMPONENT_MISSING): CUPS_BACKEND_STOP,
        int(ExitCode.IO_ERROR): CUPS_BACKEND_RETRY,
        int(ExitCode.FILE_TOO_BIG): CUPS_BACKEND_CANCEL,
    }.get(r, CUPS_BACKEND_FAILED)


def do_main():
    info(" ".join(["'%s'" % x for x in sys.argv]))
    init_write_back_fn()

    if len(sys.argv) == 1:
        # Without arguments should give backend info.
        # This is also used when lpinfo -v is issued, where it should include "direct this_backend"
        sys.stdout.write("direct %s \"Unknown\" \"Direct pdf/postscript printing/forwarding to host via xpra\"\n" % os.path.basename(sys.argv[0]))
        sys.stdout.flush()
        return 0
    if len(sys.argv) not in (6,7):
        sys.stdout.write("Usage: %s job-id user title copies options [file]\n" % os.path.basename(sys.argv[0]))
        sys.stdout.flush()
        err("Wrong number of arguments. Usage: %s job-id user title copies options [file]" % sys.argv[0])
        return 1
    write_back("INFO: parsing request")
    job_id, username, title, no_copies, print_options = sys.argv[1:6]
    if len(sys.argv)==7:
        filename = sys.argv[6]
    else:
        filename = "-"
    info("version %s, username: %s, title: %s, filename: %s, job_id: %s" % (__version__, username, title, filename, job_id))
    try:
        info("uid=%s, gid=%s" % (os.getresuid(), os.getresgid()))
    except Exception:
        #osx doesn't have getresuid or getresgid
        pass

    dev_uri = os.environ['DEVICE_URI']
    info("DEVICE_URI=%s" % dev_uri)
    parsed_url = urlparse(dev_uri)
    attributes = parse_qs(parsed_url.query)
    info("parsed attributes=%s" % attributes)

    def aget(k, default_value=""):
        v = attributes.get(k)
        if v is None:
            return default_value
        assert len(v) == 1
        return v[0]

    source = aget("source")
    if not source:
        raise ValueError("Device URI: client source uuid is missing")
    socket_path = aget("socket-path")
    display = aget("display", os.environ.get("DISPLAY"))
    socket_dir = aget("socket-dir")
    if not display and not socket_path:
        raise ValueError("Device URI: display number and socket path are not specified!")
    mimetype = aget("mimetype", "application/postscript")
    printer = aget("remote-printer")
    password_file = aget("password-file")
    encryption = aget("encryption")
    encryption_keyfile = aget("encryption-keyfile")

    info("xpra display: %s, socket-path: %s" % (display, socket_path))

    return xpra_print(socket_path, socket_dir, password_file, encryption, encryption_keyfile,
                      display, filename, mimetype, source, title, printer, no_copies, print_options)


def main():
    try:
        r = do_main()
    except Exception as e:
        write_back(f"ERROR: xpra print backend error: {e}")
        err(f"failure in xpraforwarder main: {e}")
        for x in traceback.format_tb(sys.exc_info()[2]):
            err(x)
        r = 1
    sys.exit(r)


if __name__=='__main__':
    main()
