/*
 * Copyright (C) 2016 The Android Open Source Project
 * Copyright (C) 2016 Mopria Alliance, Inc.
 * Copyright (C) 2013 Hewlett-Packard Development Company, L.P.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#ifndef __USE_UNIX98
#define __USE_UNIX98
#endif

#include <pthread.h>

#include <semaphore.h>
#include <printer_capabilities_types.h>

#include "ifc_print_job.h"
#include "wprint_debug.h"
#include "plugin_db.h"

#include "ifc_status_monitor.h"

#include "ippstatus_monitor.h"
#include "ippstatus_capabilities.h"
#include "ipp_print.h"
#include "ipphelper.h"

#include "lib_printable_area.h"
#include "wprint_io_plugin.h"
#include "../plugins/media.h"

#define TAG "lib_wprint"

/* As expected by target devices */
#define USERAGENT_PREFIX "wPrintAndroid"

#define USE_PWG_OVER_PCLM 0

#if (USE_PWG_OVER_PCLM != 0)
#define _DEFAULT_PRINT_FORMAT  PRINT_FORMAT_PWG
#define _DEFAULT_PCL_TYPE      PCLPWG
#else // (USE_PWG_OVER_PCLM != 0)
#define _DEFAULT_PRINT_FORMAT  PRINT_FORMAT_PCLM
#define _DEFAULT_PCL_TYPE      PCLm
#endif // (USE_PWG_OVER_PCLM != 0)

#define _MAX_SPOOLED_JOBS     100
#define _MAX_MSGS             (_MAX_SPOOLED_JOBS * 5)

#define _MAX_PAGES_PER_JOB   1000

#define MAX_IDLE_WAIT        (5 * 60)

#define DEFAULT_RESOLUTION   (300)

// When searching for a supported resolution this is the max resolution we will consider.
#define MAX_SUPPORTED_RESOLUTION (720)

#define MAX_DONE_WAIT (5 * 60)
#define MAX_START_WAIT (45)

#define IO_PORT_FILE   0

/*
 * The following macros allow for up to 8 bits (256) for spooled job id#s and
 * 24 bits (16 million) of a running sequence number to provide a reasonably
 * unique job handle
 */

// _ENCODE_HANDLE() is only called from _get_handle()
#define _ENCODE_HANDLE(X) ( (((++_running_number) & 0xffffff) << 8) | ((X) & 0xff) )
#define _DECODE_HANDLE(X) ((X) & 0xff)

#undef snprintf
#undef vsnprintf

typedef enum {
    JOB_STATE_FREE, // queue element free
    JOB_STATE_QUEUED, // job queued and waiting to be run
    JOB_STATE_RUNNING, // job running (printing)
    JOB_STATE_BLOCKED, // print job blocked due to printer stall/error
    JOB_STATE_CANCEL_REQUEST, // print job cancelled by user,
    JOB_STATE_CANCELLED, // print job cancelled by user, waiting to be freed
    JOB_STATE_COMPLETED, // print job completed successfully, waiting to be freed
    JOB_STATE_ERROR, // job could not be run due to error
    JOB_STATE_CORRUPTED, // job could not be run due to error

    NUM_JOB_STATES
} _job_state_t;

typedef enum {
    TOP_MARGIN = 0,
    LEFT_MARGIN,
    RIGHT_MARGIN,
    BOTTOM_MARGIN,

    NUM_PAGE_MARGINS
} _page_margins_t;

typedef enum {
    MSG_RUN_JOB, MSG_QUIT,
} wprint_msg_t;

typedef struct {
    wprint_msg_t id;
    wJob_t job_id;
} _msg_t;

/*
 * Define an entry in the job queue
 */
typedef struct {
    wJob_t job_handle;
    _job_state_t job_state;
    unsigned int blocked_reasons;
    wprint_status_cb_t cb_fn;
    char *printer_addr;
    port_t port_num;
    wprint_plugin_t *plugin;
    ifc_print_job_t *print_ifc;
    char *mime_type;
    char *pathname;
    bool is_dir;
    bool last_page_seen;
    int num_pages;
    msg_q_id pageQ;
    msg_q_id saveQ;

    wprint_job_params_t job_params;
    bool cancel_ok;
    bool use_secure_uri;

    const ifc_status_monitor_t *status_ifc;
    char debug_path[MAX_PATHNAME_LENGTH + 1];
    char printer_uri[1024];
    int job_debug_fd;
    int page_debug_fd;

    /* A buffer of bytes containing the certificate received while setting up this job, if any. */
    uint8 *certificate;
    int certificate_len;
} _job_queue_t;

/*
 * An entry for queued pages
 */
typedef struct {
    int page_num;
    bool pdf_page;
    bool last_page;
    bool corrupted;
    char filename[MAX_PATHNAME_LENGTH + 1];
    unsigned int top_margin;
    unsigned int left_margin;
    unsigned int right_margin;
    unsigned int bottom_margin;
} _page_t;

/*
 * Entry for a registered plugin
 */
typedef struct {
    port_t port_num;
    const wprint_io_plugin_t *io_plugin;
} _io_plugin_t;

static _job_queue_t _job_queue[_MAX_SPOOLED_JOBS];
static msg_q_id _msgQ;

static pthread_t _job_status_tid;
static pthread_t _job_tid;

static pthread_mutex_t _q_lock;
static pthread_mutexattr_t _q_lock_attr;

static sem_t _job_end_wait_sem;
static sem_t _job_start_wait_sem;

static _io_plugin_t _io_plugins[2];

char g_osName[MAX_ID_STRING_LENGTH + 1] = {0};
char g_appName[MAX_ID_STRING_LENGTH + 1] = {0};
char g_appVersion[MAX_ID_STRING_LENGTH + 1] = {0};

/*
 * Convert a pcl_t type to a human-readable string
 */
static char *getPCLTypeString(pcl_t pclenum) {
    switch (pclenum) {
        case PCLNONE:
            return "PCL_NONE";
        case PCLm:
            return "PCLm";
        case PCLJPEG:
            return "PCL_JPEG";
        case PCLPWG:
            return "PWG-Raster";
        default:
            return "unkonwn PCL Type";
    }
}

/*
 * Return a _job_queue_t item by its job_handle or NULL if not found.
 */
static _job_queue_t *_get_job_desc(wJob_t job_handle) {
    unsigned long index;
    if (job_handle == WPRINT_BAD_JOB_HANDLE) {
        return NULL;
    }
    index = _DECODE_HANDLE(job_handle);
    if ((index < _MAX_SPOOLED_JOBS) && (_job_queue[index].job_handle == job_handle) &&
            (_job_queue[index].job_state != JOB_STATE_FREE)) {
        return (&_job_queue[index]);
    } else {
        return NULL;
    }
}

/*
 * Functions below to fill out the _debug_stream_ifc interface
 */

static void _stream_dbg_end_job(wJob_t job_handle) {
    _job_queue_t *jq = _get_job_desc(job_handle);
    if (jq && (jq->job_debug_fd >= 0)) {
        close(jq->job_debug_fd);
        jq->job_debug_fd = -1;
    }
}

static void _stream_dbg_start_job(wJob_t job_handle, const char *ext) {
    _stream_dbg_end_job(job_handle);
    _job_queue_t *jq = _get_job_desc(job_handle);
    if (jq && jq->debug_path[0]) {
        char filename[MAX_PATHNAME_LENGTH + 1];
        snprintf(filename, MAX_PATHNAME_LENGTH, "%s/jobstream.%s", jq->debug_path, ext);
        filename[MAX_PATHNAME_LENGTH] = 0;
        jq->job_debug_fd = open(filename, O_CREAT | O_TRUNC | O_WRONLY, 0600);
    }
}

static void _stream_dbg_job_data(wJob_t job_handle, const unsigned char *buff,
        unsigned long nbytes) {
    _job_queue_t *jq = _get_job_desc(job_handle);
    ssize_t bytes_written;
    if (jq && (jq->job_debug_fd >= 0)) {
        while (nbytes > 0) {
            bytes_written = write(jq->job_debug_fd, buff, nbytes);
            if (bytes_written < 0) {
                return;
            }
            nbytes -= bytes_written;
            buff += bytes_written;
        }
    }
}

static void _stream_dbg_end_page(wJob_t job_handle) {
    _job_queue_t *jq = _get_job_desc(job_handle);
    if (jq && (jq->page_debug_fd >= 0)) {
        close(jq->page_debug_fd);
        jq->page_debug_fd = -1;
    }
}

static void _stream_dbg_page_data(wJob_t job_handle, const unsigned char *buff,
        unsigned long nbytes) {
    _job_queue_t *jq = _get_job_desc(job_handle);
    ssize_t bytes_written;
    if (jq && (jq->page_debug_fd >= 0)) {
        while (nbytes > 0) {
            bytes_written = write(jq->page_debug_fd, buff, nbytes);
            if (bytes_written < 0) {
                return;
            }
            nbytes -= bytes_written;
            buff += bytes_written;
        }
    }
}

#define PPM_IDENTIFIER "P6\n"
#define PPM_HEADER_LENGTH 128

static void _stream_dbg_start_page(wJob_t job_handle, int page_number, int width, int height) {
    _stream_dbg_end_page(job_handle);
    _job_queue_t *jq = _get_job_desc(job_handle);
    if (jq && jq->debug_path[0]) {
        union {
            char filename[MAX_PATHNAME_LENGTH + 1];
            char ppm_header[PPM_HEADER_LENGTH + 1];
        } buff;
        snprintf(buff.filename, MAX_PATHNAME_LENGTH, "%s/page%4.4d.ppm", jq->debug_path,
                page_number);
        buff.filename[MAX_PATHNAME_LENGTH] = 0;
        jq->page_debug_fd = open(buff.filename, O_CREAT | O_TRUNC | O_WRONLY, 0600);
        int length = snprintf(buff.ppm_header, sizeof(buff.ppm_header), "%s\n#%*c\n%d %d\n%d\n",
                PPM_IDENTIFIER, 0, ' ', width, height, 255);
        int padding = sizeof(buff.ppm_header) - length;
        snprintf(buff.ppm_header, sizeof(buff.ppm_header), "%s\n#%*c\n%d %d\n%d\n",
                PPM_IDENTIFIER, padding, ' ', width, height, 255);
        _stream_dbg_page_data(job_handle, (const unsigned char *) buff.ppm_header,
                PPM_HEADER_LENGTH);
    }
}

static const ifc_wprint_debug_stream_t _debug_stream_ifc = {
        .debug_start_job = _stream_dbg_start_job, .debug_job_data = _stream_dbg_job_data,
        .debug_end_job = _stream_dbg_end_job, .debug_start_page = _stream_dbg_start_page,
        .debug_page_data = _stream_dbg_page_data, .debug_end_page = _stream_dbg_end_page
};

/*
 * Return the debug stream interface corresponding to the specified job handle
 */
const ifc_wprint_debug_stream_t *getDebugStreamIfc(wJob_t handle) {
    _job_queue_t *jq = _get_job_desc(handle);
    if (jq) {
        return (jq->debug_path[0] == 0) ? NULL : &_debug_stream_ifc;
    }
    return NULL;
}

const ifc_wprint_t _wprint_ifc = {
        .msgQCreate = msgQCreate, .msgQDelete = msgQDelete,
        .msgQSend = msgQSend, .msgQReceive = msgQReceive, .msgQNumMsgs = msgQNumMsgs,
        .get_debug_stream_ifc = getDebugStreamIfc
};

static pcl_t _default_pcl_type = _DEFAULT_PCL_TYPE;

static const ifc_print_job_t *_printer_file_connect(const ifc_wprint_t *wprint_ifc) {
    return printer_connect(IO_PORT_FILE);
}

static const ifc_printer_capabilities_t *_get_caps_ifc(port_t port_num) {
    int i;
    for (i = 0; i < ARRAY_SIZE(_io_plugins); i++) {
        if (_io_plugins[i].port_num == port_num) {
            if (_io_plugins[i].io_plugin == NULL) {
                return NULL;
            }
            if (_io_plugins[i].io_plugin->getCapsIFC == NULL) {
                return NULL;
            } else {
                return (_io_plugins[i].io_plugin->getCapsIFC(&_wprint_ifc));
            }
        }
    }
    return NULL;
}

static const ifc_status_monitor_t *_get_status_ifc(port_t port_num) {
    int i;
    for (i = 0; i < ARRAY_SIZE(_io_plugins); i++) {
        if (_io_plugins[i].port_num == port_num) {
            if (_io_plugins[i].io_plugin == NULL) {
                return NULL;
            }
            if (_io_plugins[i].io_plugin->getStatusIFC == NULL) {
                return NULL;
            } else {
                return (_io_plugins[i].io_plugin->getStatusIFC(&_wprint_ifc));
            }
        }
    }
    return NULL;
}

static const ifc_print_job_t *_get_print_ifc(port_t port_num) {
    int i;
    for (i = 0; i < ARRAY_SIZE(_io_plugins); i++) {
        if (_io_plugins[i].port_num == port_num) {
            if (_io_plugins[i].io_plugin == NULL) {
                return NULL;
            }
            if (_io_plugins[i].io_plugin->getPrintIFC == NULL) {
                return NULL;
            } else {
                return (_io_plugins[i].io_plugin->getPrintIFC(&_wprint_ifc));
            }
        }
    }
    return NULL;
}

/*
 * Lock the semaphore for this module
 */
static void _lock(void) {
    pthread_mutex_lock(&_q_lock);
}

/*
 * Unlock the semaphore for this module
 */
static void _unlock(void) {
    pthread_mutex_unlock(&_q_lock);
}

static wJob_t _get_handle(void) {
    static unsigned long _running_number = 0;
    wJob_t job_handle = WPRINT_BAD_JOB_HANDLE;
    int i, index, size;
    char *ptr;

    for (i = 0; i < _MAX_SPOOLED_JOBS; i++) {
        index = (i + _running_number) % _MAX_SPOOLED_JOBS;

        if (_job_queue[index].job_state == JOB_STATE_FREE) {
            size = MAX_MIME_LENGTH + MAX_PRINTER_ADDR_LENGTH + MAX_PATHNAME_LENGTH + 4;
            ptr = malloc(size);
            if (ptr) {
                memset(&_job_queue[index], 0, sizeof(_job_queue_t));
                memset(ptr, 0, size);

                _job_queue[index].job_debug_fd = -1;
                _job_queue[index].page_debug_fd = -1;
                _job_queue[index].printer_addr = ptr;

                ptr += (MAX_PRINTER_ADDR_LENGTH + 1);
                _job_queue[index].mime_type = ptr;
                ptr += (MAX_MIME_LENGTH + 1);
                _job_queue[index].pathname = ptr;

                _job_queue[index].job_state = JOB_STATE_QUEUED;
                _job_queue[index].job_handle = _ENCODE_HANDLE(index);

                job_handle = _job_queue[index].job_handle;
            }
            break;
        }
    }
    return job_handle;
}

static int _recycle_handle(wJob_t job_handle) {
    _job_queue_t *jq = _get_job_desc(job_handle);

    if (jq == NULL) {
        return ERROR;
    } else if ((jq->job_state == JOB_STATE_CANCELLED) || (jq->job_state == JOB_STATE_ERROR) ||
            (jq->job_state == JOB_STATE_CORRUPTED) || (jq->job_state == JOB_STATE_COMPLETED)) {
        if (jq->print_ifc != NULL) {
            jq->print_ifc->destroy(jq->print_ifc);
        }

        jq->print_ifc = NULL;
        if (jq->status_ifc != NULL) {
            jq->status_ifc->destroy(jq->status_ifc);
        }
        jq->status_ifc = NULL;
        if (jq->job_params.useragent != NULL) {
            free((void *) jq->job_params.useragent);
        }
        free(jq->printer_addr);
        jq->job_state = JOB_STATE_FREE;
        if (jq->job_debug_fd != -1) {
            close(jq->job_debug_fd);
        }
        jq->job_debug_fd = -1;
        if (jq->page_debug_fd != -1) {
            close(jq->page_debug_fd);
        }
        jq->page_debug_fd = -1;
        jq->debug_path[0] = 0;
        if (jq->certificate) {
            free(jq->certificate);
            jq->certificate = NULL;
        }
        return OK;
    } else {
        return ERROR;
    }
}

/*
 * Stops the job status thread if it exists
 */
static int _stop_status_thread(_job_queue_t *jq) {
    if (!pthread_equal(_job_status_tid, pthread_self()) && (jq && jq->status_ifc)) {
        (jq->status_ifc->stop)(jq->status_ifc);
        _unlock();
        pthread_join(_job_status_tid, 0);
        _lock();
        _job_status_tid = pthread_self();
        return OK;
    } else {
        return ERROR;
    }
}

/*
 * Handles a new status message from the printer. Based on the status of wprint and the printer,
 * this function will start/end a job, send another page, or return blocking errors.
 */
static void _job_status_callback(const printer_state_dyn_t *new_status,
        const printer_state_dyn_t *old_status, void *param) {
    wprint_job_callback_params_t cb_param;
    _job_queue_t *jq = (_job_queue_t *) param;
    unsigned int i, blocked_reasons;
    print_status_t statusnew, statusold;

    statusnew = new_status->printer_status & ~PRINTER_IDLE_BIT;
    statusold = old_status->printer_status & ~PRINTER_IDLE_BIT;
    cb_param.certificate = jq->certificate;
    cb_param.certificate_len = jq->certificate_len;

    LOGD("_job_status_callback(): current printer state: %d", statusnew);
    blocked_reasons = 0;
    for (i = 0; i <= PRINT_STATUS_MAX_STATE; i++) {
        if (new_status->printer_reasons[i] == PRINT_STATUS_MAX_STATE) {
            break;
        }
        LOGD("_job_status_callback(): blocking reason %d: %d", i, new_status->printer_reasons[i]);
        blocked_reasons |= (1 << new_status->printer_reasons[i]);
    }

    switch (statusnew) {
        case PRINT_STATUS_UNKNOWN:
            if ((new_status->printer_reasons[0] == PRINT_STATUS_OFFLINE)
                    || (new_status->printer_reasons[0] == PRINT_STATUS_UNKNOWN)) {
                sem_post(&_job_start_wait_sem);
                sem_post(&_job_end_wait_sem);
                _lock();
                if ((new_status->printer_reasons[0] == PRINT_STATUS_OFFLINE)
                        && ((jq->print_ifc != NULL) && (jq->print_ifc->enable_timeout != NULL))) {
                    jq->print_ifc->enable_timeout(jq->print_ifc, 1);
                }
                _unlock();
            }
            break;

        case PRINT_STATUS_IDLE:
            if ((statusold > PRINT_STATUS_IDLE) || (statusold == PRINT_STATUS_CANCELLED)) {
                // Print is over but the job wasn't ended correctly
                if (jq->is_dir && !jq->last_page_seen) {
                    wprintPage(jq->job_handle, jq->num_pages + 1, NULL, true, false, 0, 0, 0, 0);
                }
                sem_post(&_job_end_wait_sem);
            }
            break;

        case PRINT_STATUS_CANCELLED:
            sem_post(&_job_start_wait_sem);
            if ((jq->print_ifc != NULL) && (jq->print_ifc->enable_timeout != NULL)) {
                jq->print_ifc->enable_timeout(jq->print_ifc, 1);
            }
            if (statusold != PRINT_STATUS_CANCELLED) {
                LOGI("status requested job cancel");
                if (new_status->printer_reasons[0] == PRINT_STATUS_OFFLINE) {
                    sem_post(&_job_start_wait_sem);
                    sem_post(&_job_end_wait_sem);
                    if ((jq->print_ifc != NULL) && (jq->print_ifc->enable_timeout != NULL)) {
                        jq->print_ifc->enable_timeout(jq->print_ifc, 1);
                    }
                }
                _lock();
                jq->job_params.cancelled = true;
                _unlock();
            }
            if (new_status->printer_reasons[0] == PRINT_STATUS_OFFLINE) {
                sem_post(&_job_start_wait_sem);
                sem_post(&_job_end_wait_sem);
            }
            break;

        case PRINT_STATUS_PRINTING:
            sem_post(&_job_start_wait_sem);
            _lock();
            if ((jq->job_state != JOB_STATE_RUNNING) || (jq->blocked_reasons != blocked_reasons)) {
                jq->job_state = JOB_STATE_RUNNING;
                jq->blocked_reasons = blocked_reasons;
                if (jq->cb_fn) {
                    cb_param.state = JOB_RUNNING;
                    cb_param.blocked_reasons = jq->blocked_reasons;
                    cb_param.job_done_result = OK;

                    jq->cb_fn(jq->job_handle, (void *) &cb_param);
                }
            }
            _unlock();
            break;

        case PRINT_STATUS_UNABLE_TO_CONNECT:
            sem_post(&_job_start_wait_sem);
            _lock();
            _stop_status_thread(jq);

            jq->blocked_reasons = blocked_reasons;
            jq->job_params.cancelled = true;
            jq->job_state = JOB_STATE_ERROR;
            if (jq->cb_fn) {
                cb_param.state = JOB_DONE;
                cb_param.blocked_reasons = blocked_reasons;
                cb_param.job_done_result = ERROR;

                jq->cb_fn(jq->job_handle, (void *) &cb_param);
            }

            if (jq->print_ifc != NULL) {
                jq->print_ifc->destroy(jq->print_ifc);
                jq->print_ifc = NULL;
            }

            if (jq->status_ifc != NULL) {
                jq->status_ifc->destroy(jq->status_ifc);
                jq->status_ifc = NULL;
            }

            _unlock();
            sem_post(&_job_end_wait_sem);
            break;

        default:
            // an error has occurred, report it back to the client
            sem_post(&_job_start_wait_sem);
            _lock();

            if ((jq->job_state != JOB_STATE_BLOCKED) || (jq->blocked_reasons != blocked_reasons)) {
                jq->job_state = JOB_STATE_BLOCKED;
                jq->blocked_reasons = blocked_reasons;
                if (jq->cb_fn) {
                    cb_param.state = JOB_BLOCKED;
                    cb_param.blocked_reasons = blocked_reasons;
                    cb_param.job_done_result = OK;

                    jq->cb_fn(jq->job_handle, (void *) &cb_param);
                }
            }
            _unlock();
            break;
    }
}

static void *_job_status_thread(void *param) {
    _job_queue_t *jq = (_job_queue_t *) param;
    (jq->status_ifc->start)(jq->status_ifc, _job_status_callback, param);
    return NULL;
}

static int _start_status_thread(_job_queue_t *jq) {
    sigset_t allsig, oldsig;
    int result = ERROR;

    if ((jq == NULL) || (jq->status_ifc == NULL)) {
        return result;
    }

    result = OK;
    sigfillset(&allsig);
#if CHECK_PTHREAD_SIGMASK_STATUS
    result = pthread_sigmask(SIG_SETMASK, &allsig, &oldsig);
#else // else CHECK_PTHREAD_SIGMASK_STATUS
    pthread_sigmask(SIG_SETMASK, &allsig, &oldsig);
#endif // CHECK_PTHREAD_SIGMASK_STATUS
    if (result == OK) {
        result = pthread_create(&_job_status_tid, 0, _job_status_thread, jq);
        if ((result == ERROR) && (_job_status_tid != pthread_self())) {
#if USE_PTHREAD_CANCEL
            pthread_cancel(_job_status_tid);
#else // else USE_PTHREAD_CANCEL
            pthread_kill(_job_status_tid, SIGKILL);
#endif // USE_PTHREAD_CANCEL
            _job_status_tid = pthread_self();
        }
    }

    if (result == OK) {
        sched_yield();
#if CHECK_PTHREAD_SIGMASK_STATUS
        result = pthread_sigmask(SIG_SETMASK, &oldsig, 0);
#else // else CHECK_PTHREAD_SIGMASK_STATUS
        pthread_sigmask(SIG_SETMASK, &oldsig, 0);
#endif // CHECK_PTHREAD_SIGMASK_STATUS
    }
    return result;
}

/*
 * Return true unless the server gave an unexpected certificate
 */
static bool _is_certificate_allowed(_job_queue_t *jq) {
    int result = true;

    // Compare certificates if both are known
    if (jq->job_params.certificate && jq->certificate) {
        if (jq->job_params.certificate_len != jq->certificate_len) {
            LOGD("_is_certificate_allowed: certificate length mismatch allowed=%d, received=%d",
                jq->job_params.certificate_len, jq->certificate_len);
            result = false;
        } else if (0 != memcmp(jq->job_params.certificate, jq->certificate, jq->certificate_len)) {
            LOGD("_is_certificate_allowed: certificate content mismatch");
            result = false;
        } else {
            LOGD("_is_certificate_allowed: certificate match, len=%d",
                jq->job_params.certificate_len);
        }
    }

    return result;
}

/*
 * Callback from lower layers containing certificate data, if any.
 */
static int _validate_certificate(wprint_connect_info_t *connect_info, uint8 *data, int data_len) {
    _job_queue_t *jq = connect_info->user;
    LOGD("_validate_certificate: %s://%s:%d%s handling server cert len=%d for job %ld",
        connect_info->uri_scheme, connect_info->printer_addr, connect_info->port_num,
        connect_info->uri_path, data_len, jq->job_handle);

    // Free any old certificate we have and save new certificate data
    if (jq->certificate) {
        free(jq->certificate);
        jq->certificate = NULL;
    }
    jq->certificate = (uint8 *)malloc(data_len);
    int error = 0;
    if (jq->certificate == NULL) {
        LOGD("_validate_certificate: malloc failed");
        error = -1;
    } else {
        memcpy(jq->certificate, data, data_len);
        jq->certificate_len = data_len;
        if (!_is_certificate_allowed(jq)) {
            LOGD("_validate_certificate: received certificate disallowed.");
            error = -1;
        }
    }
    return error;
}

/*
 * Initialize the status interface (so we can use it to query for printer status.
 */
static void _initialize_status_ifc(_job_queue_t *jq) {
    wprint_connect_info_t connect_info;
    connect_info.printer_addr = jq->printer_addr;
    connect_info.uri_path = jq->printer_uri;
    connect_info.port_num = jq->port_num;
    if (jq->use_secure_uri) {
        connect_info.uri_scheme = IPPS_PREFIX;
        connect_info.user = jq;
        connect_info.validate_certificate = _validate_certificate;
    } else {
        connect_info.uri_scheme = IPP_PREFIX;
        connect_info.validate_certificate = NULL;
    }
    connect_info.timeout = DEFAULT_IPP_TIMEOUT;

    // Initialize the status interface with this connection info
    jq->status_ifc->init(jq->status_ifc, &connect_info);
}

/*
 * Runs a print job. Contains logic for what to do given different printer statuses.
 */
static void *_job_thread(void *param) {
    wprint_job_callback_params_t cb_param = { 0 };
    _msg_t msg;
    wJob_t job_handle;
    _job_queue_t *jq;
    _page_t page;
    int i;
    status_t job_result;
    int corrupted = 0;

    while (OK == msgQReceive(_msgQ, (char *) &msg, sizeof(msg), WAIT_FOREVER)) {
        if (msg.id == MSG_RUN_JOB) {
            LOGI("_job_thread(): Received message: MSG_RUN_JOB");
        } else {
            LOGI("_job_thread(): Received message: MSG_QUIT");
        }

        if (msg.id == MSG_QUIT) {
            break;
        }

        job_handle = msg.job_id;

        //  check if this is a valid job_handle that is still active
        _lock();

        jq = _get_job_desc(job_handle);

        //  set state to running and invoke the plugin, there is one
        if (jq) {
            if (jq->job_state != JOB_STATE_QUEUED) {
                _unlock();
                continue;
            }
            corrupted = 0;
            job_result = OK;
            jq->job_params.plugin_data = NULL;

            // clear out the semaphore just in case
            while (sem_trywait(&_job_start_wait_sem) == OK) {
            }
            while (sem_trywait(&_job_end_wait_sem) == OK) {
            }

            // initialize the status ifc
            if (jq->status_ifc != NULL) {
                _initialize_status_ifc(jq);
            }
            // wait for the printer to be idle
            if ((jq->status_ifc != NULL) && (jq->status_ifc->get_status != NULL)) {
                int retry = 0;
                int loop = 1;
                printer_state_dyn_t printer_state;
                do {
                    print_status_t status;
                    jq->status_ifc->get_status(jq->status_ifc, &printer_state);
                    status = printer_state.printer_status & ~PRINTER_IDLE_BIT;

                    // Pass along any certificate received in future callbacks
                    cb_param.certificate = jq->certificate;
                    cb_param.certificate_len = jq->certificate_len;

                    switch (status) {
                        case PRINT_STATUS_IDLE:
                            printer_state.printer_status = PRINT_STATUS_IDLE;
                            jq->blocked_reasons = 0;
                            loop = 0;
                            break;
                        case PRINT_STATUS_UNKNOWN:
                            if (printer_state.printer_reasons[0] == PRINT_STATUS_UNKNOWN) {
                                LOGE("PRINTER STATUS UNKNOWN - Ln 747 libwprint.c");
                                // no status available, break out and hope for the best
                                printer_state.printer_status = PRINT_STATUS_IDLE;
                                loop = 0;
                                break;
                            }
                        case PRINT_STATUS_SVC_REQUEST:
                            if ((printer_state.printer_reasons[0] == PRINT_STATUS_UNABLE_TO_CONNECT)
                                    || (printer_state.printer_reasons[0] == PRINT_STATUS_OFFLINE)) {
                                if (_is_certificate_allowed(jq)) {
                                    LOGD("_job_thread: Received an Unable to Connect message");
                                    jq->blocked_reasons = BLOCKED_REASON_UNABLE_TO_CONNECT;
                                } else {
                                    LOGD("_job_thread: Bad certificate");
                                    jq->blocked_reasons = BLOCKED_REASON_BAD_CERTIFICATE;
                                }
                                loop = 0;
                                break;
                            }
                        default:
                            if (printer_state.printer_status & PRINTER_IDLE_BIT) {
                                LOGD("printer blocked but appears to be in an idle state. "
                                        "Allowing job to proceed");
                                printer_state.printer_status = PRINT_STATUS_IDLE;
                                loop = 0;
                                break;
                            } else if (retry >= MAX_IDLE_WAIT) {
                                jq->blocked_reasons |= BLOCKED_REASONS_PRINTER_BUSY;
                                loop = 0;
                            } else if (!jq->job_params.cancelled) {
                                int blocked_reasons = 0;
                                for (i = 0; i <= PRINT_STATUS_MAX_STATE; i++) {
                                    if (printer_state.printer_reasons[i] ==
                                            PRINT_STATUS_MAX_STATE) {
                                        break;
                                    }
                                    blocked_reasons |= (1 << printer_state.printer_reasons[i]);
                                }
                                if (blocked_reasons == 0) {
                                    blocked_reasons |= BLOCKED_REASONS_PRINTER_BUSY;
                                }

                                if ((jq->job_state != JOB_STATE_BLOCKED) ||
                                        (jq->blocked_reasons != blocked_reasons)) {
                                    jq->job_state = JOB_STATE_BLOCKED;
                                    jq->blocked_reasons = blocked_reasons;
                                    if (jq->cb_fn) {
                                        cb_param.state = JOB_BLOCKED;
                                        cb_param.blocked_reasons = blocked_reasons;
                                        cb_param.job_done_result = OK;

                                        jq->cb_fn(jq->job_handle, (void *) &cb_param);
                                    }
                                }
                                _unlock();
                                sleep(1);
                                _lock();
                                retry++;
                            }
                            break;
                    }
                    if (jq->job_params.cancelled) {
                        loop = 0;
                    }
                } while (loop);

                if (jq->job_params.cancelled) {
                    job_result = CANCELLED;
                } else {
                    job_result = (((printer_state.printer_status & ~PRINTER_IDLE_BIT) ==
                            PRINT_STATUS_IDLE) ? OK : ERROR);
                }
            }

            _job_status_tid = pthread_self();
            if (job_result == OK) {
                if (jq->print_ifc) {
                    job_result = jq->print_ifc->init(jq->print_ifc, jq->printer_addr,
                            jq->port_num, jq->printer_uri, jq->use_secure_uri);
                    if (job_result == ERROR) {
                        jq->blocked_reasons = BLOCKED_REASON_UNABLE_TO_CONNECT;
                    }
                }
            }
            if (job_result == OK) {
                _start_status_thread(jq);
            }

            /*  call the plugin's start_job method, if no other job is running
             use callback to notify the client */

            if ((job_result == OK) && jq->cb_fn) {
                cb_param.state = JOB_RUNNING;
                cb_param.blocked_reasons = 0;
                cb_param.job_done_result = OK;

                jq->cb_fn(job_handle, (void *) &cb_param);
            }

            jq->job_params.page_num = -1;
            if (job_result == OK) {
                if (jq->print_ifc != NULL) {
                    LOGD("_job_thread: Calling validate_job");
                    if (jq->print_ifc->validate_job != NULL) {
                        job_result = jq->print_ifc->validate_job(jq->print_ifc, &jq->job_params);
                    }

                    /* PDF format plugin's start_job and end_job are to be called for each copy,
                     * inside the for-loop for num_copies.
                     */

                    // Do not call start_job unless validate_job returned OK
                    if ((job_result == OK) && (jq->print_ifc->start_job != NULL) &&
                            (strcmp(jq->job_params.print_format, PRINT_FORMAT_PDF) != 0)) {
                        jq->print_ifc->start_job(jq->print_ifc, &jq->job_params);
                    }
                }

                // Do not call start_job unless validate_job returned OK
                if (job_result == OK && jq->plugin->start_job != NULL) {
                    job_result = jq->plugin->start_job(job_handle, (void *) &_wprint_ifc,
                            (void *) jq->print_ifc, &(jq->job_params));
                }
            }

            if (job_result == OK) {
                jq->job_params.page_num = 0;
            }

            // multi-page print job
            if (jq->is_dir && (job_result == OK)) {
                int per_copy_page_num;
                for (i = 0; (i < jq->job_params.num_copies) &&
                        ((job_result == OK) || (job_result == CORRUPT)) &&
                        (!jq->job_params.cancelled); i++) {
                    if ((i > 0) &&
                            jq->job_params.copies_supported &&
                            (strcmp(jq->job_params.print_format, PRINT_FORMAT_PDF) == 0)) {
                        LOGD("_job_thread multi_page: breaking out copies supported");
                        break;
                    }
                    bool pdf_printed = false;
                    if (jq->print_ifc->start_job != NULL &&
                            (strcmp(jq->job_params.print_format, PRINT_FORMAT_PDF) == 0)) {
                        jq->print_ifc->start_job(jq->print_ifc, &jq->job_params);
                    }

                    per_copy_page_num = 0;
                    jq->job_state = JOB_STATE_RUNNING;

                    // while there is a page to print
                    _unlock();

                    while (OK == msgQReceive(jq->pageQ, (char *) &page, sizeof(page),
                            WAIT_FOREVER)) {
                        _lock();

                        // check for any printing problems so far
                        if (jq->print_ifc->check_status) {
                            if (jq->print_ifc->check_status(jq->print_ifc) == ERROR) {
                                job_result = ERROR;
                                break;
                            }
                        }

                        /* take empty filename as cue to break out of the loop
                         * but we have to do last_page processing
                         */

                        // all copies are clubbed together as a single print job
                        if (page.last_page && ((i == jq->job_params.num_copies - 1) ||
                                (jq->job_params.copies_supported &&
                                        strcmp(jq->job_params.print_format,
                                                PRINT_FORMAT_PDF) == 0))) {
                            jq->job_params.last_page = page.last_page;
                        } else {
                            jq->job_params.last_page = false;
                        }

                        if (strlen(page.filename) > 0) {
                            per_copy_page_num++;
                            {
                                jq->job_params.page_num++;
                            }
                            if (page.pdf_page) {
                                jq->job_params.page_num = page.page_num;
                            } else {
                                jq->job_params.page_num = per_copy_page_num;
                            }

                            // setup page margin information
                            jq->job_params.print_top_margin += page.top_margin;
                            jq->job_params.print_left_margin += page.left_margin;
                            jq->job_params.print_right_margin += page.right_margin;
                            jq->job_params.print_bottom_margin += page.bottom_margin;

                            jq->job_params.copy_num = (i + 1);
                            jq->job_params.copy_page_num = page.page_num;
                            jq->job_params.page_backside = (per_copy_page_num & 0x1);
                            jq->job_params.page_corrupted = (page.corrupted ? 1 : 0);
                            jq->job_params.page_printing = true;
                            _unlock();

                            if (!page.corrupted) {
                                LOGD("_job_thread(): page not corrupt, calling plugin's print_page"
                                        " function for page #%d", page.page_num);
                                if (strcmp(jq->job_params.print_format, PRINT_FORMAT_PDF) != 0) {
                                    job_result = jq->plugin->print_page(&(jq->job_params),
                                            jq->mime_type,
                                            page.filename);
                                } else if (!pdf_printed) {
                                    // for PDF plugin, print_page prints entire document,
                                    // so need to be called only once
                                    job_result = jq->plugin->print_page(&(jq->job_params),
                                            jq->mime_type,
                                            page.filename);
                                    pdf_printed = true;
                                }
                            } else {
                                LOGD("_job_thread(): page IS corrupt, printing blank page for "
                                        "page #%d", page.page_num);
                                job_result = CORRUPT;
                                if ((jq->job_params.duplex != DUPLEX_MODE_NONE) &&
                                        (jq->plugin->print_blank_page != NULL)) {
                                    jq->plugin->print_blank_page(job_handle, &(jq->job_params));
                                }
                            }
                            _lock();

                            jq->job_params.print_top_margin -= page.top_margin;
                            jq->job_params.print_left_margin -= page.left_margin;
                            jq->job_params.print_right_margin -= page.right_margin;
                            jq->job_params.print_bottom_margin -= page.bottom_margin;
                            jq->job_params.page_printing = false;

                            // make sure we only count corrupted pages once
                            if (page.corrupted == false) {
                                page.corrupted = ((job_result == CORRUPT) ? true : false);
                                corrupted += (job_result == CORRUPT);
                            }
                        }

                        // make sure we always print an even number of pages in duplex jobs
                        if (page.last_page && (jq->job_params.duplex != DUPLEX_MODE_NONE)
                                && (jq->job_params.page_backside)
                                && (jq->plugin->print_blank_page != NULL)) {
                            _unlock();
                            jq->plugin->print_blank_page(job_handle, &(jq->job_params));
                            _lock();
                        }

                        // if multiple copies are requested, save the contents of the pageQ message
                        if (jq->saveQ && !jq->job_params.cancelled && (job_result != ERROR)) {
                            job_result = msgQSend(jq->saveQ, (char *) &page,
                                    sizeof(page), NO_WAIT, MSG_Q_FIFO);

                            // swap pageQ and saveQ
                            if (page.last_page && !jq->job_params.last_page) {
                                msg_q_id tmpQ = jq->pageQ;
                                jq->pageQ = jq->saveQ;
                                jq->saveQ = tmpQ;

                                // defensive programming
                                while (msgQNumMsgs(tmpQ) > 0) {
                                    msgQReceive(tmpQ, (char *) &page, sizeof(page), NO_WAIT);
                                    LOGE("pageQ inconsistencies, discarding page #%d, file %s",
                                            page.page_num, page.filename);
                                }
                            }
                        }

                        if (page.last_page || jq->job_params.cancelled) {
                            // Leave the sempahore locked
                            break;
                        }

                        // unlock to go back to the top of the while loop
                        _unlock();
                    } // while there is another page

                    if ((strcmp(jq->job_params.print_format, PRINT_FORMAT_PDF) == 0) &&
                            (jq->print_ifc->end_job)) {
                        int end_job_result = jq->print_ifc->end_job(jq->print_ifc);
                        if (job_result == OK) {
                            if (end_job_result == ERROR) {
                                job_result = ERROR;
                            } else if (end_job_result == CANCELLED) {
                                job_result = CANCELLED;
                            }
                        }
                    }
                } // for each copy of the job
            } else if (job_result == OK) {
                // single page job
                for (i = 0; ((i < jq->job_params.num_copies) && (job_result == OK)); i++) {
                    if ((i > 0) && jq->job_params.copies_supported &&
                            (strcmp(jq->job_params.print_format, PRINT_FORMAT_PDF) == 0)) {
                        LOGD("_job_thread single_page: breaking out copies supported");
                        break;
                    }

                    // check for any printing problems so far
                    if ((jq->print_ifc != NULL) && (jq->print_ifc->check_status)) {
                        if (jq->print_ifc->check_status(jq->print_ifc) == ERROR) {
                            job_result = ERROR;
                            break;
                        }
                    }

                    jq->job_state = JOB_STATE_RUNNING;
                    jq->job_params.page_num++;
                    jq->job_params.last_page = (i == (jq->job_params.num_copies - 1));
                    jq->job_params.copy_num = (i + 1);
                    jq->job_params.copy_page_num = 1;
                    jq->job_params.page_corrupted = (job_result == CORRUPT);
                    jq->job_params.page_printing = true;

                    _unlock();
                    job_result = jq->plugin->print_page(&(jq->job_params), jq->mime_type,
                            jq->pathname);

                    if ((jq->job_params.duplex != DUPLEX_MODE_NONE)
                            && (jq->plugin->print_blank_page != NULL)) {
                        jq->plugin->print_blank_page(job_handle,
                                &(jq->job_params));
                    }

                    _lock();
                    jq->job_params.page_printing = false;

                    corrupted += (job_result == CORRUPT);
                } // for each copy
            }

            // if we started the job end it
            if (jq->job_params.page_num >= 0) {
                // if the job was cancelled without sending anything through, print a blank sheet
                if ((jq->job_params.page_num == 0)
                        && (jq->plugin->print_blank_page != NULL)) {
                    jq->plugin->print_blank_page(job_handle, &(jq->job_params));
                }
                if (jq->plugin->end_job != NULL) {
                    jq->plugin->end_job(&(jq->job_params));
                }
                if ((jq->print_ifc != NULL) && (jq->print_ifc->end_job) &&
                        (strcmp(jq->job_params.print_format, PRINT_FORMAT_PDF) != 0)) {
                    int end_job_result = jq->print_ifc->end_job(jq->print_ifc);
                    if (job_result == OK) {
                        if (end_job_result == ERROR) {
                            job_result = ERROR;
                        } else if (end_job_result == CANCELLED) {
                            job_result = CANCELLED;
                        }
                    }
                }
            }

            // if we started to print, wait for idle
            if ((jq->job_params.page_num > 0) && (jq->status_ifc != NULL)) {
                int retry, result;
                _unlock();

                for (retry = 0, result = ERROR; ((result == ERROR) && (retry <= MAX_START_WAIT));
                        retry++) {
                    if (retry != 0) {
                        sleep(1);
                    }
                    result = sem_trywait(&_job_start_wait_sem);
                }

                if (result == OK) {
                    for (retry = 0, result = ERROR; ((result == ERROR) && (retry <= MAX_DONE_WAIT));
                            retry++) {
                        if (retry != 0) {
                            _lock();
                            if (jq->job_params.cancelled && !jq->cancel_ok) {
                                /* The user tried to cancel and it either didn't go through
                                 * or the printer doesn't support cancel through an OID.
                                 * Either way it's pointless to sit here waiting for idle when
                                 * may never come, so we'll bail out early
                                 */
                                retry = (MAX_DONE_WAIT + 1);
                            }
                            _unlock();
                            sleep(1);
                            if (retry == MAX_DONE_WAIT) {
                                _lock();
                                if (!jq->job_params.cancelled &&
                                        (jq->blocked_reasons
                                                & (BLOCKED_REASON_OUT_OF_PAPER
                                                        | BLOCKED_REASON_JAMMED
                                                        | BLOCKED_REASON_DOOR_OPEN))) {
                                    retry = (MAX_DONE_WAIT - 1);
                                }
                                _unlock();
                            }
                        }
                        result = sem_trywait(&_job_end_wait_sem);
                    }
                } else {
                    LOGD("_job_thread(): the job never started");
                }
                _lock();
            }

            // make sure page_num doesn't stay as a negative number
            jq->job_params.page_num = MAX(0, jq->job_params.page_num);
            _stop_status_thread(jq);

            if (corrupted != 0) {
                job_result = CORRUPT;
            }

            LOGI("job_thread(): with job_state value: %d ", jq->job_state);
            if ((jq->job_state == JOB_STATE_COMPLETED) || (jq->job_state == JOB_STATE_ERROR)
                    || (jq->job_state == JOB_STATE_CANCELLED)
                    || (jq->job_state == JOB_STATE_CORRUPTED)
                    || (jq->job_state == JOB_STATE_FREE)) {
                LOGI("_job_thread(): job finished early: do not send callback again");
            } else {
                switch (job_result) {
                    case OK:
                        if (!jq->job_params.cancelled) {
                            jq->job_state = JOB_STATE_COMPLETED;
                            jq->blocked_reasons = 0;
                            break;
                        } else {
                            job_result = CANCELLED;
                        }
                    case CANCELLED:
                        jq->job_state = JOB_STATE_CANCELLED;
                        jq->blocked_reasons = BLOCKED_REASONS_CANCELLED;
                        if (!jq->cancel_ok) {
                            jq->blocked_reasons |= BLOCKED_REASON_PARTIAL_CANCEL;
                        }
                        break;
                    case CORRUPT:
                        LOGE("_job_thread(): %d file(s) in the job were corrupted", corrupted);
                        jq->job_state = JOB_STATE_CORRUPTED;
                        jq->blocked_reasons = 0;
                        break;
                    case ERROR:
                    default:
                        LOGE("_job_thread(): ERROR plugin->start_job(%ld): %s => %s", job_handle,
                                jq->mime_type, jq->job_params.print_format);
                        job_result = ERROR;
                        jq->job_state = JOB_STATE_ERROR;
                        break;
                } // job_result

                // end of job callback
                if (jq->cb_fn) {
                    cb_param.state = JOB_DONE;
                    cb_param.blocked_reasons = jq->blocked_reasons;
                    cb_param.job_done_result = job_result;

                    jq->cb_fn(job_handle, (void *) &cb_param);
                }

                if (jq->print_ifc != NULL) {
                    jq->print_ifc->destroy(jq->print_ifc);
                    jq->print_ifc = NULL;
                }

                if (jq->status_ifc != NULL) {
                    jq->status_ifc->destroy(jq->status_ifc);
                    jq->status_ifc = NULL;
                }
            }
        } else {
            LOGI("_job_thread(): job %ld not in queue .. maybe cancelled", job_handle);
        }

        _unlock();
        LOGI("_job_thread(): job finished: %ld", job_handle);
    }

    sem_post(&_job_end_wait_sem);
    return NULL;
}

/*
 * Starts the wprint background job thread
 */
static int _start_thread(void) {
    sigset_t allsig, oldsig;
    int result;

    _job_tid = pthread_self();

    result = OK;
    sigfillset(&allsig);
#if CHECK_PTHREAD_SIGMASK_STATUS
    result = pthread_sigmask(SIG_SETMASK, &allsig, &oldsig);
#else // else CHECK_PTHREAD_SIGMASK_STATUS
    pthread_sigmask(SIG_SETMASK, &allsig, &oldsig);
#endif // CHECK_PTHREAD_SIGMASK_STATUS
    if (result == OK) {
        result = pthread_create(&_job_tid, 0, _job_thread, NULL);
        if ((result == ERROR) && (_job_tid != pthread_self())) {
#if USE_PTHREAD_CANCEL
            pthread_cancel(_job_tid);
#else // else USE_PTHREAD_CANCEL
            pthread_kill(_job_tid, SIGKILL);
#endif // USE_PTHREAD_CANCEL
            _job_tid = pthread_self();
        }
    }

    if (result == OK) {
        sched_yield();
#if CHECK_PTHREAD_SIGMASK_STATUS
        result = pthread_sigmask(SIG_SETMASK, &oldsig, 0);
#else // else CHECK_PTHREAD_SIGMASK_STATUS
        pthread_sigmask(SIG_SETMASK, &oldsig, 0);
#endif // CHECK_PTHREAD_SIGMASK_STATUS
    }

    return result;
}

/*
 * Waits for the job thread to reach a stopped state
 */
static int _stop_thread(void) {
    if (!pthread_equal(_job_tid, pthread_self())) {
        pthread_join(_job_tid, 0);
        _job_tid = pthread_self();
        return OK;
    } else {
        return ERROR;
    }
}

static const wprint_io_plugin_t _file_io_plugin = {
        .version = WPRINT_PLUGIN_VERSION(_INTERFACE_MINOR_VERSION),
        .port_num = PORT_FILE, .getCapsIFC = NULL, .getStatusIFC = NULL,
        .getPrintIFC = _printer_file_connect,};

static const wprint_io_plugin_t _ipp_io_plugin = {
        .version = WPRINT_PLUGIN_VERSION(_INTERFACE_MINOR_VERSION),
        .port_num = PORT_IPP, .getCapsIFC = ipp_status_get_capabilities_ifc,
        .getStatusIFC = ipp_status_get_monitor_ifc, .getPrintIFC = ipp_get_print_ifc,};

static void _setup_io_plugins() {
    _io_plugins[0].port_num = PORT_FILE;
    _io_plugins[0].io_plugin = &_file_io_plugin;

    _io_plugins[1].port_num = PORT_IPP;
    _io_plugins[1].io_plugin = &_ipp_io_plugin;
}

extern wprint_plugin_t *libwprintplugin_pcl_reg(void);

extern wprint_plugin_t *libwprintplugin_pdf_reg(void);

static void _setup_print_plugins() {
    plugin_reset();
    plugin_add(libwprintplugin_pcl_reg());
    plugin_add(libwprintplugin_pdf_reg());
}

bool wprintIsRunning() {
    return _msgQ != 0;
}

int wprintInit(void) {
    int count = 0;

    _setup_print_plugins();
    _setup_io_plugins();

    _msgQ = msgQCreate(_MAX_MSGS, sizeof(_msg_t));

    if (!_msgQ) {
        LOGE("ERROR: cannot create msgQ");
        return ERROR;
    }

    sem_init(&_job_end_wait_sem, 0, 0);
    sem_init(&_job_start_wait_sem, 0, 0);

    signal(SIGPIPE, SIG_IGN); // avoid broken pipe process shutdowns
    pthread_mutexattr_settype(&_q_lock_attr, PTHREAD_MUTEX_RECURSIVE_NP);
    pthread_mutex_init(&_q_lock, &_q_lock_attr);

    if (_start_thread() != OK) {
        LOGE("could not start job thread");
        return ERROR;
    }
    return count;
}

static const printer_capabilities_t _default_cap = {.color = true, .borderless = true,
        .numSupportedMediaSizes = 0, .numSupportedMediaTrays = 0,
        .numSupportedMediaTypes = 0,};

/*
 * Check if a media size is supported
 */
static bool is_supported(media_size_t media_size) {
    int i;
    for (i = 0; i < SUPPORTED_MEDIA_SIZE_COUNT; i++) {
        if (SupportedMediaSizes[i].media_size == media_size) return true;
    }
    return false;
}

/*
 * Return true if the specified int array of the supplied length contains a value.
 */
static bool int_array_contains(const int *array, int length, int value) {
    for (int i = 0; i < length; i++) {
        if (array[i] == value) return true;
    }
    return false;
}

/*
 * Checks printers reported media sizes and validates that wprint supports them
 */
static void _validate_supported_media_sizes(printer_capabilities_t *printer_cap) {
    if (printer_cap == NULL) return;

    if (printer_cap->numSupportedMediaSizes == 0) {
        unsigned int i = 0;
        printer_cap->supportedMediaSizes[i++] = ISO_A4;
        printer_cap->supportedMediaSizes[i++] = US_LETTER;
        printer_cap->supportedMediaSizes[i++] = INDEX_CARD_4X6;
        printer_cap->supportedMediaSizes[i++] = INDEX_CARD_5X7;
        printer_cap->numSupportedMediaSizes = i;
    } else {
        unsigned int read, write;
        for (read = write = 0; read < printer_cap->numSupportedMediaSizes; read++) {
            if (is_supported(printer_cap->supportedMediaSizes[read])) {
                printer_cap->supportedMediaSizes[write++] =
                        printer_cap->supportedMediaSizes[read];
            }
        }
        printer_cap->numSupportedMediaSizes = write;
    }
}

/*
 * Checks printers numSupportedMediaTrays. If none, then add Auto.
 */
static void _validate_supported_media_trays(printer_capabilities_t *printer_cap) {
    if (printer_cap == NULL) return;

    if (printer_cap->numSupportedMediaTrays == 0) {
        printer_cap->supportedMediaTrays[0] = TRAY_SRC_AUTO_SELECT;
        printer_cap->numSupportedMediaTrays = 1;
    }
}

/*
 * Add a printer's supported input formats to the capabilities struct
 */
static void _collect_supported_input_formats(printer_capabilities_t *printer_caps) {
    unsigned long long input_formats = 0;
    plugin_get_passthru_input_formats(&input_formats);

    // remove things the printer can't support
    if (!printer_caps->canPrintPDF) {
        input_formats &= ~(1 << INPUT_MIME_TYPE_PDF);
    }
    if (!printer_caps->canPrintPCLm) {
        input_formats &= ~(1 << INPUT_MIME_TYPE_PCLM);
    }
    if (!printer_caps->canPrintPWG) {
        input_formats &= ~(1 << INPUT_MIME_TYPE_PWG);
    }
    printer_caps->supportedInputMimeTypes = input_formats;
}

/*
 * Check the print resolutions supported by the printer and verify that wprint supports them.
 * If nothing is found, the desired resolution is selected.
 */
static unsigned int _findCloseResolutionSupported(int desiredResolution, int maxResolution,
        const printer_capabilities_t *printer_cap) {
    int closeResolution = 0;
    int closeDifference = 0;
    unsigned int index = 0;
    for (index = 0; index < printer_cap->numSupportedResolutions; index++) {
        int resolution = printer_cap->supportedResolutions[index];
        if (resolution == desiredResolution) {
            // An exact match wins.. stop looking.
            return resolution;
        } else {
            int difference = abs(desiredResolution - resolution);
            if ((closeResolution == 0) || (difference < closeDifference)) {
                if (resolution <= maxResolution) {
                    // We found a better match now.. record it but keep looking.
                    closeResolution = resolution;
                    closeDifference = difference;
                }
            }
        }
    }

    // If we get here we did not find an exact match.
    if (closeResolution == 0) {
        // We did not find anything.. just pick the desired value.
        closeResolution = desiredResolution;
    }
    return closeResolution;
}

status_t wprintGetCapabilities(const wprint_connect_info_t *connect_info,
        printer_capabilities_t *printer_cap) {
    LOGD("wprintGetCapabilities: Enter");
    status_t result = ERROR;
    int index;
    int port_num = connect_info->port_num;
    const ifc_printer_capabilities_t *caps_ifc = NULL;

    memcpy(printer_cap, &_default_cap, sizeof(printer_capabilities_t));

    caps_ifc = _get_caps_ifc(((port_num == 0) ? PORT_FILE : PORT_IPP));
    LOGD("wprintGetCapabilities: after getting caps ifc: %p", caps_ifc);
    switch (port_num) {
        case PORT_FILE:
            printer_cap->duplex = 1;
            printer_cap->borderless = 1;
            printer_cap->canPrintPCLm = (_default_pcl_type == PCLm);
            printer_cap->canPrintPWG = (_default_pcl_type == PCLPWG);
            printer_cap->stripHeight = STRIPE_HEIGHT;
            result = OK;
            break;
        default:
            break;
    }

    if (caps_ifc != NULL) {
        caps_ifc->init(caps_ifc, connect_info);
        result = caps_ifc->get_capabilities(caps_ifc, printer_cap);
        caps_ifc->destroy(caps_ifc);
    }

    _validate_supported_media_sizes(printer_cap);
    _collect_supported_input_formats(printer_cap);
    _validate_supported_media_trays(printer_cap);

    printer_cap->isSupported = (printer_cap->canPrintPCLm || printer_cap->canPrintPDF ||
            printer_cap->canPrintPWG);

    if (result == OK) {
        LOGD("\tmake: %s", printer_cap->make);
        LOGD("\thas color: %d", printer_cap->color);
        LOGD("\tcan duplex: %d", printer_cap->duplex);
        LOGD("\tcan rotate back page: %d", printer_cap->canRotateDuplexBackPage);
        LOGD("\tcan print borderless: %d", printer_cap->borderless);
        LOGD("\tcan print pdf: %d", printer_cap->canPrintPDF);
        LOGD("\tcan print pclm: %d", printer_cap->canPrintPCLm);
        LOGD("\tcan print pwg: %d", printer_cap->canPrintPWG);
        LOGD("\tsource application name supported: %d", printer_cap->docSourceAppName);
        LOGD("\tsource application version supported: %d", printer_cap->docSourceAppVersion);
        LOGD("\tsource os name supported: %d", printer_cap->docSourceOsName);
        LOGD("\tsource os version supported: %d", printer_cap->docSourceOsVersion);
        LOGD("\tprinter supported: %d", printer_cap->isSupported);
        LOGD("\tstrip height: %d", printer_cap->stripHeight);
        LOGD("\tinkjet: %d", printer_cap->inkjet);
        LOGD("\tresolutions supported:");
        for (index = 0; index < printer_cap->numSupportedResolutions; index++) {
            LOGD("\t (%d dpi)", printer_cap->supportedResolutions[index]);
        }
    }
    LOGD("wprintGetCapabilities: Exit");
    return result;
}

/*
 * Returns a preferred print format supported by the printer
 */
static char *_get_print_format(const char *mime_type, const wprint_job_params_t *job_params,
        const printer_capabilities_t *cap) {
    char *print_format = NULL;

    errno = OK;

    if (((strcmp(mime_type, MIME_TYPE_PDF) == 0) && cap->canPrintPDF)) {
        // For content type=photo and a printer that supports both PCLm and PDF,
        // prefer PCLm over PDF.
        if (job_params && (strcasecmp(job_params->docCategory, "photo") == 0) &&
                cap->canPrintPCLm) {
            print_format = PRINT_FORMAT_PCLM;
            LOGI("_get_print_format(): print_format switched from PDF to PCLm");
        } else {
            print_format = PRINT_FORMAT_PDF;
        }
    } else if (cap->canPrintPCLm || cap->canPrintPDF) {
        // PCLm is a subset of PDF
        print_format = PRINT_FORMAT_PCLM;
#if (USE_PWG_OVER_PCLM != 0)
        if (cap->canPrintPWG) {
            print_format = PRINT_FORMAT_PWG;
        }
#endif // (USE_PWG_OVER_PCLM != 0)
    } else if (cap->canPrintPWG) {
        print_format = PRINT_FORMAT_PWG;
    } else {
        errno = EBADRQC;
    }

    if (print_format != NULL) {
        LOGI("\t_get_print_format(): print_format: %s", print_format);
    }

    return print_format;
}

status_t wprintGetDefaultJobParams(wprint_job_params_t *job_params) {
    status_t result = ERROR;
    static const wprint_job_params_t _default_job_params = {.print_format = _DEFAULT_PRINT_FORMAT,
            .pcl_type = _DEFAULT_PCL_TYPE, .media_size = US_LETTER, .media_type = MEDIA_PLAIN,
            .duplex = DUPLEX_MODE_NONE, .dry_time = DUPLEX_DRY_TIME_NORMAL,
            .color_space = COLOR_SPACE_COLOR, .media_tray = TRAY_SRC_AUTO_SELECT,
            .pixel_units = DEFAULT_RESOLUTION, .render_flags = 0, .num_copies =1,
            .borderless = false, .cancelled = false, .renderInReverseOrder = false,
            .ipp_1_0_supported = false, .ipp_2_0_supported = false, .epcl_ipp_supported = false,
            .strip_height = STRIPE_HEIGHT, .docCategory = {0},
            .copies_supported = false};

    if (job_params == NULL) return result;

    memcpy(job_params, &_default_job_params, sizeof(_default_job_params));

    return OK;
}

status_t wprintGetFinalJobParams(wprint_job_params_t *job_params,
        const printer_capabilities_t *printer_cap) {
    int i;
    status_t result = ERROR;
    float margins[NUM_PAGE_MARGINS];

    if (job_params == NULL) {
        return result;
    }
    result = OK;

    job_params->accepts_pclm = printer_cap->canPrintPCLm;
    job_params->accepts_pdf = printer_cap->canPrintPDF;
    job_params->media_default = printer_cap->mediaDefault;

    if (printer_cap->ePclIppVersion == 1) {
        job_params->epcl_ipp_supported = true;
    }

    if (printer_cap->canCopy) {
        job_params->copies_supported = true;
    }

    if (printer_cap->ippVersionMajor == 2) {
        job_params->ipp_1_0_supported = true;
        job_params->ipp_2_0_supported = true;
    } else if (printer_cap->ippVersionMajor == 1) {
        job_params->ipp_1_0_supported = true;
        job_params->ipp_2_0_supported = false;
    }

    if (!printer_cap->color) {
        job_params->color_space = COLOR_SPACE_MONO;
    }

    if (printer_cap->canPrintPCLm || printer_cap->canPrintPDF) {
        job_params->pcl_type = PCLm;
#if (USE_PWG_OVER_PCLM != 0)
        if ( printer_cap->canPrintPWG) {
            job_params->pcl_type = PCLPWG;
        }
#endif // (USE_PWG_OVER_PCLM != 0)
    } else if (printer_cap->canPrintPWG) {
        job_params->pcl_type = PCLPWG;
    }

    LOGD("wprintGetFinalJobParams: Using PCL Type %s", getPCLTypeString(job_params->pcl_type));

    // set strip height
    job_params->strip_height = printer_cap->stripHeight;

    // make sure the number of copies is valid
    if (job_params->num_copies <= 0) {
        job_params->num_copies = 1;
    }

    // If printing photo and HIGH quality is supported, specify it.
    if (strcasecmp(job_params->docCategory, "photo") == 0 && int_array_contains(
            printer_cap->supportedQuality, printer_cap->numSupportedQuality, IPP_QUALITY_HIGH)) {
        job_params->print_quality = IPP_QUALITY_HIGH;
    }

    // confirm that the media size is supported
    for (i = 0; i < printer_cap->numSupportedMediaSizes; i++) {
        if (job_params->media_size == printer_cap->supportedMediaSizes[i]) {
            break;
        }
    }

    if (i >= printer_cap->numSupportedMediaSizes) {
        job_params->media_size = ISO_A4;
        job_params->media_tray = TRAY_SRC_AUTO_SELECT;
    }

    // check that we support the media tray
    for (i = 0; i < printer_cap->numSupportedMediaTrays; i++) {
        if (job_params->media_tray == printer_cap->supportedMediaTrays[i]) {
            break;
        }
    }

    // media tray not supported, default to automatic
    if (i >= printer_cap->numSupportedMediaTrays) {
        job_params->media_tray = TRAY_SRC_AUTO_SELECT;
    }

    if (printer_cap->isMediaSizeNameSupported == true) {
        job_params->media_size_name = true;
    } else {
        job_params->media_size_name = false;
    }

    // verify borderless setting
    if ((job_params->borderless == true) && !printer_cap->borderless) {
        job_params->borderless = false;
    }

    // borderless and margins don't get along
    if (job_params->borderless &&
            ((job_params->job_top_margin > 0.0f) || (job_params->job_left_margin > 0.0f) ||
                    (job_params->job_right_margin > 0.0f)
                    || (job_params->job_bottom_margin > 0.0f))) {
        job_params->borderless = false;
    }

    // verify duplex setting
    if ((job_params->duplex != DUPLEX_MODE_NONE) && !printer_cap->duplex) {
        job_params->duplex = DUPLEX_MODE_NONE;
    }

    // borderless and duplex don't get along either
    if (job_params->borderless && (job_params->duplex != DUPLEX_MODE_NONE)) {
        job_params->duplex = DUPLEX_MODE_NONE;
    }

    if ((job_params->duplex == DUPLEX_MODE_BOOK)
            && !printer_cap->canRotateDuplexBackPage) {
        job_params->render_flags |= RENDER_FLAG_ROTATE_BACK_PAGE;
    }

    if (job_params->render_flags & RENDER_FLAG_ROTATE_BACK_PAGE) {
        LOGD("wprintGetFinalJobParams: Duplex is on and device needs back page rotated.");
    }

    if ((job_params->duplex == DUPLEX_MODE_NONE) && !printer_cap->faceDownTray) {
        job_params->renderInReverseOrder = true;
    } else {
        job_params->renderInReverseOrder = false;
    }

    if (job_params->render_flags & RENDER_FLAG_AUTO_SCALE) {
        job_params->render_flags |= AUTO_SCALE_RENDER_FLAGS;
    } else if (job_params->render_flags & RENDER_FLAG_AUTO_FIT) {
        job_params->render_flags |= AUTO_FIT_RENDER_FLAGS;
    }

    job_params->pixel_units = _findCloseResolutionSupported(DEFAULT_RESOLUTION,
            MAX_SUPPORTED_RESOLUTION, printer_cap);

    printable_area_get_default_margins(job_params, printer_cap, &margins[TOP_MARGIN],
            &margins[LEFT_MARGIN], &margins[RIGHT_MARGIN], &margins[BOTTOM_MARGIN]);
    printable_area_get(job_params, margins[TOP_MARGIN], margins[LEFT_MARGIN], margins[RIGHT_MARGIN],
            margins[BOTTOM_MARGIN]);

    job_params->accepts_app_name = printer_cap->docSourceAppName;
    job_params->accepts_app_version = printer_cap->docSourceAppVersion;
    job_params->accepts_os_name = printer_cap->docSourceOsName;
    job_params->accepts_os_version = printer_cap->docSourceOsVersion;

    return result;
}

wJob_t wprintStartJob(const char *printer_addr, port_t port_num,
        const wprint_job_params_t *job_params, const printer_capabilities_t *printer_cap,
        const char *mime_type, const char *pathname, wprint_status_cb_t cb_fn,
        const char *debugDir, const char *scheme) {
    wJob_t job_handle = WPRINT_BAD_JOB_HANDLE;
    _msg_t msg;
    struct stat stat_buf;
    bool is_dir = false;
    _job_queue_t *jq;
    wprint_plugin_t *plugin = NULL;
    char *print_format;
    ifc_print_job_t *print_ifc;

    if (mime_type == NULL) {
        errno = EINVAL;
        return job_handle;
    }

    print_format = _get_print_format(mime_type, job_params, printer_cap);
    if (print_format == NULL) return job_handle;

    // check to see if we have an appropriate plugin
    if (OK == stat(pathname, &stat_buf)) {
        if (S_ISDIR(stat_buf.st_mode)) {
            is_dir = true;
        } else if (stat_buf.st_size == 0) {
            errno = EBADF;
            return job_handle;
        }
    } else {
        errno = ENOENT;
        return job_handle;
    }

    // Make sure we have job_params
    if (job_params == NULL) {
        errno = ECOMM;
        return job_handle;
    }

    plugin = plugin_search(mime_type, print_format);
    _lock();

    if (plugin) {
        job_handle = _get_handle();
        if (job_handle == WPRINT_BAD_JOB_HANDLE) {
            errno = EAGAIN;
        }
    } else {
        errno = ENOSYS;
        LOGE("wprintStartJob(): ERROR: no plugin found for %s => %s", mime_type, print_format);
    }

    if (job_handle != WPRINT_BAD_JOB_HANDLE) {
        print_ifc = (ifc_print_job_t *) _get_print_ifc(((port_num == 0) ? PORT_FILE : PORT_IPP));

        // fill out the job queue record
        jq = _get_job_desc(job_handle);
        if (jq == NULL) {
            _recycle_handle(job_handle);
            job_handle = WPRINT_BAD_JOB_HANDLE;
            _unlock();
            return job_handle;
        }

        if (debugDir != NULL) {
            strncpy(jq->debug_path, debugDir, MAX_PATHNAME_LENGTH);
            jq->debug_path[MAX_PATHNAME_LENGTH] = 0;
        }

        strncpy(jq->printer_addr, printer_addr, MAX_PRINTER_ADDR_LENGTH);
        strncpy(jq->mime_type, mime_type, MAX_MIME_LENGTH);
        strncpy(jq->pathname, pathname, MAX_PATHNAME_LENGTH);

        jq->port_num = port_num;
        jq->cb_fn = cb_fn;
        jq->print_ifc = print_ifc;
        jq->cancel_ok = true; // assume cancel is ok
        jq->plugin = plugin;
        memcpy(jq->printer_uri, printer_cap->httpResource,
                MIN(ARRAY_SIZE(printer_cap->httpResource), ARRAY_SIZE(jq->printer_uri)));

        jq->status_ifc = _get_status_ifc(((port_num == 0) ? PORT_FILE : PORT_IPP));

        memcpy((char *) &(jq->job_params), job_params, sizeof(wprint_job_params_t));

        jq->use_secure_uri = (strstr(scheme, IPPS_PREFIX) != NULL);

        size_t useragent_len = strlen(USERAGENT_PREFIX) + strlen(jq->job_params.docCategory) + 1;
        char *useragent = (char *) malloc(useragent_len);
        if (useragent != NULL) {
            snprintf(useragent, useragent_len, USERAGENT_PREFIX "%s", jq->job_params.docCategory);
            jq->job_params.useragent = useragent;
        }

        jq->job_params.page_num = 0;
        jq->job_params.print_format = print_format;
        if (strcmp(print_format, PRINT_FORMAT_PCLM) == 0) {
            if (printer_cap->canPrintPCLm || printer_cap->canPrintPDF) {
                jq->job_params.pcl_type = PCLm;
            } else {
                jq->job_params.pcl_type = PCLNONE;
            }
        }

        if (strcmp(print_format, PRINT_FORMAT_PWG) == 0) {
            if (printer_cap->canPrintPWG) {
                jq->job_params.pcl_type = PCLPWG;
            } else {
                jq->job_params.pcl_type = PCLNONE;
            }
        }

        // if the pathname is a directory, then this is a multi-page job with individual pages
        if (is_dir) {
            jq->is_dir = true;
            jq->num_pages = 0;

            // create a pageQ for queuing page information
            jq->pageQ = msgQCreate(_MAX_PAGES_PER_JOB, sizeof(_page_t));

            // create a secondary page Q for subsequently saving page data for copies #2 to n
            if (jq->job_params.num_copies > 1) {
                jq->saveQ = msgQCreate(_MAX_PAGES_PER_JOB, sizeof(_page_t));
            }
        } else {
            jq->num_pages = 1;
        }

        // post a message with job_handle to the msgQ that is serviced by a thread
        msg.id = MSG_RUN_JOB;
        msg.job_id = job_handle;

        if (print_ifc && plugin && plugin->print_page &&
                (msgQSend(_msgQ, (char *) &msg, sizeof(msg), NO_WAIT, MSG_Q_FIFO) == OK)) {
            errno = OK;
            LOGD("wprintStartJob(): print job %ld queued (%s => %s)", job_handle,
                    mime_type, print_format);
        } else {
            if (print_ifc == NULL) {
                errno = EAFNOSUPPORT;
            } else if ((plugin == NULL) || (plugin->print_page == NULL)) {
                errno = ELIBACC;
            } else {
                errno = EBADMSG;
            }

            LOGE("wprintStartJob(): ERROR plugin->start_job(%ld) : %s => %s", job_handle,
                    mime_type, print_format);
            jq->job_state = JOB_STATE_ERROR;
            _recycle_handle(job_handle);
            job_handle = WPRINT_BAD_JOB_HANDLE;
        }
    }
    _unlock();
    return job_handle;
}

status_t wprintEndJob(wJob_t job_handle) {
    _page_t page;
    _job_queue_t *jq;
    status_t result = ERROR;

    _lock();
    jq = _get_job_desc(job_handle);

    if (jq) {
        // if the job is done and is to be freed, do it
        if ((jq->job_state == JOB_STATE_CANCELLED) || (jq->job_state == JOB_STATE_ERROR) ||
                (jq->job_state == JOB_STATE_CORRUPTED) || (jq->job_state == JOB_STATE_COMPLETED)) {
            result = OK;
            if (jq->pageQ) {
                while ((msgQNumMsgs(jq->pageQ) > 0)
                        && (msgQReceive(jq->pageQ, (char *) &page, sizeof(page),
                                WAIT_FOREVER) == OK)) {
                }
                result |= msgQDelete(jq->pageQ);
                jq->pageQ = NULL;
            }

            if (jq->saveQ) {
                while ((msgQNumMsgs(jq->saveQ) > 0)
                        && (msgQReceive(jq->saveQ, (char *) &page, sizeof(page),
                                WAIT_FOREVER) == OK)) {
                }
                result |= msgQDelete(jq->saveQ);
                jq->saveQ = NULL;
            }
            _recycle_handle(job_handle);
        } else {
            LOGE("job %ld cannot be ended from state %d", job_handle, jq->job_state);
        }
    } else {
        LOGE("ERROR: wprintEndJob(%ld), job not found", job_handle);
    }

    _unlock();
    return result;
}

status_t wprintPage(wJob_t job_handle, int page_num, const char *filename, bool last_page,
        bool pdf_page, unsigned int top_margin, unsigned int left_margin, unsigned int right_margin,
        unsigned int bottom_margin) {
    _job_queue_t *jq;
    _page_t page;
    status_t result = ERROR;
    struct stat stat_buf;

    _lock();
    jq = _get_job_desc(job_handle);

    // use empty string to indicate EOJ for an empty job
    if (!filename) {
        filename = "";
        last_page = true;
    } else if (OK == stat(filename, &stat_buf)) {
        if (!S_ISREG(stat_buf.st_mode) || (stat_buf.st_size == 0)) {
            _unlock();
            return result;
        }
    } else {
        _unlock();
        return result;
    }

    // must be setup as a multi-page job, page_num must be valid, and filename must fit
    if (jq && jq->is_dir && !(jq->last_page_seen) && (((strlen(filename) < MAX_PATHNAME_LENGTH)) ||
            (jq && (strcmp(filename, "") == 0) && last_page))) {
        memset(&page, 0, sizeof(page));
        page.page_num = page_num;
        page.corrupted = false;
        page.pdf_page = pdf_page;
        page.last_page = last_page;
        page.top_margin = top_margin;
        page.left_margin = left_margin;
        page.right_margin = right_margin;
        page.bottom_margin = bottom_margin;

        if ((strlen(filename) == 0) || strchr(filename, '/')) {
            // assume empty or complete pathname and use it as it is
            strncpy(page.filename, filename, MAX_PATHNAME_LENGTH);
        } else {
            // generate a complete pathname
            snprintf(page.filename, MAX_PATHNAME_LENGTH, "%s/%s", jq->pathname, filename);
        }

        if (last_page) {
            jq->last_page_seen = true;
        }

        result = msgQSend(jq->pageQ, (char *) &page, sizeof(page), NO_WAIT, MSG_Q_FIFO);
    }

    if (result == OK) {
        LOGD("wprintPage(%ld, %d, %s, %d)", job_handle, page_num, filename, last_page);
        if (!(last_page && (strcmp(filename, "") == 0))) {
            jq->num_pages++;
        }
    } else {
        LOGE("wprintPage(%ld, %d, %s, %d)", job_handle, page_num, filename, last_page);
    }

    _unlock();
    return result;
}

status_t wprintCancelJob(wJob_t job_handle) {
    _job_queue_t *jq;
    status_t result;

    _lock();

    jq = _get_job_desc(job_handle);

    if (jq) {
        LOGI("received cancel request");
        // send a dummy page in case we're waiting on the msgQ page receive
        if ((jq->job_state == JOB_STATE_RUNNING) || (jq->job_state == JOB_STATE_BLOCKED)) {
            bool enableTimeout = true;
            jq->cancel_ok = true;
            jq->job_params.cancelled = true;
            wprintPage(job_handle, jq->num_pages + 1, NULL, true, false, 0, 0, 0, 0);
            if (jq->status_ifc) {
                // are we blocked waiting for the job to start
                if ((jq->job_state != JOB_STATE_BLOCKED) || (jq->job_params.page_num != 0)) {
                    errno = OK;
                    jq->cancel_ok = ((jq->status_ifc->cancel)(jq->status_ifc,
                            jq->job_params.job_originating_user_name) == 0);
                    if ((jq->cancel_ok == true) && (errno != OK)) {
                        enableTimeout = false;
                    }
                }
            }
            if (!jq->cancel_ok) {
                LOGE("CANCEL did not go through or is not supported for this device");
                enableTimeout = true;
            }
            if (enableTimeout && (jq->print_ifc != NULL) &&
                    (jq->print_ifc->enable_timeout != NULL)) {
                jq->print_ifc->enable_timeout(jq->print_ifc, 1);
            }

            errno = (jq->cancel_ok ? OK : ENOTSUP);
            jq->job_state = JOB_STATE_CANCEL_REQUEST;
            result = OK;
        } else if ((jq->job_state == JOB_STATE_CANCEL_REQUEST) ||
                (jq->job_state == JOB_STATE_CANCELLED)) {
            result = OK;
            errno = (jq->cancel_ok ? OK : ENOTSUP);
        } else if (jq->job_state == JOB_STATE_QUEUED) {
            jq->job_params.cancelled = true;
            jq->job_state = JOB_STATE_CANCELLED;

            if (jq->cb_fn) {
                wprint_job_callback_params_t cb_param;
                cb_param.state = JOB_DONE;
                cb_param.blocked_reasons = BLOCKED_REASONS_CANCELLED;
                cb_param.job_done_result = CANCELLED;
                cb_param.certificate = jq->certificate;
                cb_param.certificate_len = jq->certificate_len;

                jq->cb_fn(job_handle, (void *) &cb_param);
            }

            errno = OK;
            result = OK;
        } else {
            LOGE("job in other state");
            result = ERROR;
            errno = EBADRQC;
        }
    } else {
        LOGE("could not find job");
        result = ERROR;
        errno = EBADR;
    }

    _unlock();

    return result;
}

status_t wprintExit(void) {
    _msg_t msg;

    if (_msgQ) {
        //  toss the remaining messages in the msgQ
        while ((msgQNumMsgs(_msgQ) > 0) &&
                (OK == msgQReceive(_msgQ, (char *) &msg, sizeof(msg), NO_WAIT))) {}

        // send a quit message
        msg.id = MSG_QUIT;
        msgQSend(_msgQ, (char *) &msg, sizeof(msg), NO_WAIT, MSG_Q_FIFO);

        // stop the job thread
        _stop_thread();

        // empty out the semaphore
        while (sem_trywait(&_job_end_wait_sem) == OK);
        while (sem_trywait(&_job_start_wait_sem) == OK);

        // receive any messages just in case
        while ((msgQNumMsgs(_msgQ) > 0)
                && (OK == msgQReceive(_msgQ, (char *) &msg, sizeof(msg), NO_WAIT))) {}

        // delete the msgQ
        msgQDelete(_msgQ);
        _msgQ = NULL;

        sem_destroy(&_job_end_wait_sem);
        sem_destroy(&_job_start_wait_sem);
        pthread_mutex_destroy(&_q_lock);
    }

    return OK;
}

void wprintSetSourceInfo(const char *appName, const char *appVersion, const char *osName) {
    if (appName) {
        strncpy(g_appName, appName, (sizeof(g_appName) - 1));
    }

    if (appVersion) {
        strncpy(g_appVersion, appVersion, (sizeof(g_appVersion) - 1));
    }

    if (osName) {
        strncpy(g_osName, osName, (sizeof(g_osName) - 1));
    }

    LOGI("App Name: '%s', Version: '%s', OS: '%s'", g_appName, g_appVersion, g_osName);
}