/* Copyright Krzysztof Kowalczyk 2006-2007
   Copyright Hib Eris <hib@hiberis.nl> 2008
   License: GPLv2 */
/*
  A tool to stress-test poppler rendering and measure rendering times for
  very simplistic performance measuring.

  TODO:
   * make it work with cairo output as well
   * print more info about document like e.g. enumarate images,
     streams, compression, encryption, password-protection. Each should have
     a command-line arguments to turn it on/off
   * never over-write file given as -out argument (optionally, provide -force
     option to force writing the -out file). It's way too easy too lose results
     of a previous run.
*/

#ifdef _MSC_VER
// this sucks but I don't know any other way
#pragma comment(linker,"/manifestdependency:\"type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='x86' publicKeyToken='6595b64144ccf1df' language='*'\"")
#endif

#ifdef _WIN32
#include <windows.h>
#endif

// Define COPY_FILE if you want the file to be copied to a local disk first
// before it's tested. This is desired if a file is on a slow drive.
// Currently copying only works on Windows.
// Not enabled by default.
//#define COPY_FILE 1

#include <assert.h>
#include <config.h>
#include <stdio.h>
#include <stdarg.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <time.h>

#ifdef HAVE_DIRENT_H
#include <dirent.h>
#endif

#include "Error.h"
#include "ErrorCodes.h"
#include "goo/GooString.h"
#include "goo/GooList.h"
#include "goo/GooTimer.h"
#include "GlobalParams.h"
#include "splash/SplashBitmap.h"
#include "Object.h" /* must be included before SplashOutputDev.h because of sloppiness in SplashOutputDev.h */
#include "SplashOutputDev.h"
#include "TextOutputDev.h"
#include "PDFDoc.h"
#include "Link.h"

#ifdef _MSC_VER
#define strdup _strdup
#define strcasecmp _stricmp
#endif

#define dimof(X)    (sizeof(X)/sizeof((X)[0]))

#define INVALID_PAGE_NO     -1

/* Those must be implemented in order to provide preview during execution.
   They can be no-ops. An implementation for windows is in
   perf-test-preview-win.cc
*/
extern void PreviewBitmapInit(void);
extern void PreviewBitmapDestroy(void);
extern void PreviewBitmapSplash(SplashBitmap *bmpSplash);

class PdfEnginePoppler {
public:
    PdfEnginePoppler();
    ~PdfEnginePoppler();

    const char *fileName(void) const { return _fileName; };

    void setFileName(const char *fileName) {
        assert(!_fileName);
        _fileName = (char*)strdup(fileName);
    }

    int pageCount(void) const { return _pageCount; }

    bool load(const char *fileName);
    SplashBitmap *renderBitmap(int pageNo, double zoomReal, int rotation);

    SplashOutputDev *   outputDevice();
private:
    char *              _fileName;
    int                 _pageCount;

    PDFDoc *            _pdfDoc;
    SplashOutputDev *   _outputDev;
};

typedef struct StrList {
    struct StrList *next;
    char *          str;
} StrList;

/* List of all command-line arguments that are not switches.
   We assume those are:
     - names of PDF files
     - names of a file with a list of PDF files
     - names of directories with PDF files
*/
static StrList *gArgsListRoot = NULL;

/* Names of all command-line switches we recognize */
#define TIMINGS_ARG         "-timings"
#define RESOLUTION_ARG      "-resolution"
#define RECURSIVE_ARG       "-recursive"
#define OUT_ARG             "-out"
#define PREVIEW_ARG         "-preview"
#define SLOW_PREVIEW_ARG    "-slowpreview"
#define LOAD_ONLY_ARG       "-loadonly"
#define PAGE_ARG            "-page"
#define TEXT_ARG            "-text"

/* Should we record timings? True if -timings command-line argument was given. */
static bool gfTimings = false;

/* If true, we use render each page at resolution 'gResolutionX'/'gResolutionY'.
   If false, we render each page at its native resolution.
   True if -resolution NxM command-line argument was given. */
static bool gfForceResolution = false;
static int  gResolutionX = 0;
static int  gResolutionY = 0;
/* If NULL, we output the log info to stdout. If not NULL, should be a name
   of the file to which we output log info.
   Controled by -out command-line argument. */
static char *   gOutFileName = NULL;
/* FILE * correspondig to gOutFileName or stdout if gOutFileName is NULL or
   was invalid name */
static FILE *   gOutFile = NULL;
/* FILE * correspondig to gOutFileName or stderr if gOutFileName is NULL or
   was invalid name */
static FILE *   gErrFile = NULL;

/* If True and a directory is given as a command-line argument, we'll process
   pdf files in sub-directories as well.
   Controlled by -recursive command-line argument */
static bool gfRecursive = false;

/* If true, preview rendered image. To make sure that they're being rendered correctly. */
static bool gfPreview = false;

/* 1 second (1000 milliseconds) */
#define SLOW_PREVIEW_TIME 1000

/* If true, preview rendered image in a slow mode i.e. delay displaying for
   SLOW_PREVIEW_TIME. This is so that a human has enough time to see if the
   PDF renders ok. In release mode on fast processor pages take only ~100-200 ms
   to render and they go away too quickly to be inspected by a human. */
static bool gfSlowPreview = false;

/* If true, we only dump the text, not render */
static bool gfTextOnly = false;

#define PAGE_NO_NOT_GIVEN -1

/* If equals PAGE_NO_NOT_GIVEN, we're in default mode where we render all pages.
   If different, will only render this page */
static int  gPageNo = PAGE_NO_NOT_GIVEN;
/* If true, will only load the file, not render any pages. Mostly for
   profiling load time */
static bool gfLoadOnly = false;

#define PDF_FILE_DPI 72

#define MAX_FILENAME_SIZE 1024

/* DOS is 0xd 0xa */
#define DOS_NEWLINE "\x0d\x0a"
/* Mac is single 0xd */
#define MAC_NEWLINE "\x0d"
/* Unix is single 0xa (10) */
#define UNIX_NEWLINE "\x0a"
#define UNIX_NEWLINE_C 0xa

#ifdef _WIN32
  #define DIR_SEP_CHAR '\\'
  #define DIR_SEP_STR  "\\"
#else
  #define DIR_SEP_CHAR '/'
  #define DIR_SEP_STR  "/"
#endif

void memzero(void *data, size_t len)
{
    memset(data, 0, len);
}

void *zmalloc(size_t len)
{
    void *data = malloc(len);
    if (data)
        memzero(data, len);
    return data;
}

/* Concatenate 4 strings. Any string can be NULL.
   Caller needs to free() memory. */
char *str_cat4(const char *str1, const char *str2, const char *str3, const char *str4)
{
    char *str;
    char *tmp;
    size_t str1_len = 0;
    size_t str2_len = 0;
    size_t str3_len = 0;
    size_t str4_len = 0;

    if (str1)
        str1_len = strlen(str1);
    if (str2)
        str2_len = strlen(str2);
    if (str3)
        str3_len = strlen(str3);
    if (str4)
        str4_len = strlen(str4);

    str = (char*)zmalloc(str1_len + str2_len + str3_len + str4_len + 1);
    if (!str)
        return NULL;

    tmp = str;
    if (str1) {
        memcpy(tmp, str1, str1_len);
        tmp += str1_len;
    }
    if (str2) {
        memcpy(tmp, str2, str2_len);
        tmp += str2_len;
    }
    if (str3) {
        memcpy(tmp, str3, str3_len);
        tmp += str3_len;
    }
    if (str4) {
        memcpy(tmp, str4, str1_len);
    }
    return str;
}

char *str_dup(const char *str)
{
    return str_cat4(str, NULL, NULL, NULL);
}

bool str_eq(const char *str1, const char *str2)
{
    if (!str1 && !str2)
        return true;
    if (!str1 || !str2)
        return false;
    if (0 == strcmp(str1, str2))
        return true;
    return false;
}

bool str_ieq(const char *str1, const char *str2)
{
    if (!str1 && !str2)
        return true;
    if (!str1 || !str2)
        return false;
    if (0 == strcasecmp(str1, str2))
        return true;
    return false;
}

bool str_endswith(const char *txt, const char *end)
{
    size_t end_len;
    size_t txt_len;

    if (!txt || !end)
        return false;

    txt_len = strlen(txt);
    end_len = strlen(end);
    if (end_len > txt_len)
        return false;
    if (str_eq(txt+txt_len-end_len, end))
        return true;
    return false;
}

/* TODO: probably should move to some other file and change name to
   sleep_milliseconds */
void sleep_milliseconds(int milliseconds)
{
#ifdef _WIN32
    Sleep((DWORD)milliseconds);
#else
    struct timespec tv;
    int             secs, nanosecs;
    secs = milliseconds / 1000;
    nanosecs = (milliseconds - (secs * 1000)) * 1000;
    tv.tv_sec = (time_t) secs;
    tv.tv_nsec = (long) nanosecs;
    while (1)
    {
        int rval = nanosleep(&tv, &tv);
        if (rval == 0)
            /* Completed the entire sleep time; all done. */
            return;
        else if (errno == EINTR)
            /* Interrupted by a signal. Try again. */
            continue;
        else
            /* Some other error; bail out. */
            return;
    }
    return;
#endif
}

#ifndef _MSC_VER
void strcpy_s(char* dst, size_t dst_size, const char* src)
{
    size_t src_size = strlen(src) + 1;
    if (src_size <= dst_size)
        memcpy(dst, src, src_size);
    else {
        if (dst_size > 0) {
            memcpy(dst, src, dst_size);
            dst[dst_size-1] = 0;
        }
    }
}

void strcat_s(char *dst, size_t dst_size, const char* src)
{
    size_t dst_len = strlen(dst);
    if (dst_len >= dst_size) {
        if (dst_size > 0)
            dst[dst_size-1] = 0;
        return;
    }
    strcpy_s(dst+dst_len, dst_size - dst_len, src);
}
#endif

static SplashColorMode gSplashColorMode = splashModeBGR8;

static SplashColor splashColRed;
static SplashColor splashColGreen;
static SplashColor splashColBlue;
static SplashColor splashColWhite;
static SplashColor splashColBlack;

#define SPLASH_COL_RED_PTR (SplashColorPtr)&(splashColRed[0])
#define SPLASH_COL_GREEN_PTR (SplashColorPtr)&(splashColGreen[0])
#define SPLASH_COL_BLUE_PTR (SplashColorPtr)&(splashColBlue[0])
#define SPLASH_COL_WHITE_PTR (SplashColorPtr)&(splashColWhite[0])
#define SPLASH_COL_BLACK_PTR (SplashColorPtr)&(splashColBlack[0])

static SplashColorPtr  gBgColor = SPLASH_COL_WHITE_PTR;

static void splashColorSet(SplashColorPtr col, Guchar red, Guchar green, Guchar blue, Guchar alpha)
{
    switch (gSplashColorMode)
    {
        case splashModeBGR8:
            col[0] = blue;
            col[1] = green;
            col[2] = red;
            break;
        case splashModeRGB8:
            col[0] = red;
            col[1] = green;
            col[2] = blue;
            break;
        default:
            assert(0);
            break;
    }
}

void SplashColorsInit(void)
{
    splashColorSet(SPLASH_COL_RED_PTR, 0xff, 0, 0, 0);
    splashColorSet(SPLASH_COL_GREEN_PTR, 0, 0xff, 0, 0);
    splashColorSet(SPLASH_COL_BLUE_PTR, 0, 0, 0xff, 0);
    splashColorSet(SPLASH_COL_BLACK_PTR, 0, 0, 0, 0);
    splashColorSet(SPLASH_COL_WHITE_PTR, 0xff, 0xff, 0xff, 0);
}

PdfEnginePoppler::PdfEnginePoppler() : 
   _fileName(0)
   , _pageCount(INVALID_PAGE_NO) 
   , _pdfDoc(NULL)
   , _outputDev(NULL)
{
}

PdfEnginePoppler::~PdfEnginePoppler()
{
    free(_fileName);
    delete _outputDev;
    delete _pdfDoc;
}

bool PdfEnginePoppler::load(const char *fileName)
{
    setFileName(fileName);
    /* note: don't delete fileNameStr since PDFDoc takes ownership and deletes them itself */
    GooString *fileNameStr = new GooString(fileName);
    if (!fileNameStr) return false;

    _pdfDoc = new PDFDoc(fileNameStr, NULL, NULL, (void*)NULL);
    if (!_pdfDoc->isOk()) {
        return false;
    }
    _pageCount = _pdfDoc->getNumPages();
    return true;
}

SplashOutputDev * PdfEnginePoppler::outputDevice() {
    if (!_outputDev) {
        GBool bitmapTopDown = gTrue;
        _outputDev = new SplashOutputDev(gSplashColorMode, 4, gFalse, gBgColor, bitmapTopDown);
        if (_outputDev)
            _outputDev->startDoc(_pdfDoc->getXRef());
    }
    return _outputDev;
}

SplashBitmap *PdfEnginePoppler::renderBitmap(int pageNo, double zoomReal, int rotation)
{
    assert(outputDevice());
    if (!outputDevice()) return NULL;

    double hDPI = (double)PDF_FILE_DPI * zoomReal * 0.01;
    double vDPI = (double)PDF_FILE_DPI * zoomReal * 0.01;
    GBool  useMediaBox = gFalse;
    GBool  crop        = gTrue;
    GBool  doLinks     = gTrue;
    _pdfDoc->displayPage(_outputDev, pageNo, hDPI, vDPI, rotation, useMediaBox, 
        crop, doLinks, NULL, NULL);

    SplashBitmap* bmp = _outputDev->takeBitmap();
    return bmp;
}

struct FindFileState {
    char path[MAX_FILENAME_SIZE];
    char dirpath[MAX_FILENAME_SIZE]; /* current dir path */
    char pattern[MAX_FILENAME_SIZE]; /* search pattern */
    const char *bufptr;
#ifdef _WIN32
    WIN32_FIND_DATA fileinfo;
    HANDLE dir;
#else
    DIR *dir;
#endif
};

#ifdef _WIN32
#include <windows.h>
#include <sys/timeb.h>
#include <direct.h>

__inline char *getcwd(char *buffer, int maxlen)
{
    return _getcwd(buffer, maxlen);
}

int fnmatch(const char *pattern, const char *string, int flags)
{
    int prefix_len;
    const char *star_pos = strchr(pattern, '*');
    if (!star_pos)
        return strcmp(pattern, string) != 0;

    prefix_len = (int)(star_pos-pattern);
    if (0 == prefix_len)
        return 0;

    if (0 == _strnicmp(pattern, string, prefix_len))
        return 0;

    return 1;
}

#else
#include <fnmatch.h>
#endif

#ifdef _WIN32
/* on windows to query dirs we need foo\* to get files in this directory.
    foo\ always fails and foo will return just info about foo directory,
    not files in this directory */
static void win_correct_path_for_FindFirstFile(char *path, int path_max_len)
{
    int path_len = strlen(path);
    if (path_len >= path_max_len-4)
        return;
    if (DIR_SEP_CHAR != path[path_len])
        path[path_len++] = DIR_SEP_CHAR;
    path[path_len++] = '*';
    path[path_len] = 0;
}
#endif

FindFileState *find_file_open(const char *path, const char *pattern)
{
    FindFileState *s;

    s = (FindFileState*)malloc(sizeof(FindFileState));
    if (!s)
        return NULL;
    strcpy_s(s->path, sizeof(s->path), path);
    strcpy_s(s->dirpath, sizeof(s->path), path);
#ifdef _WIN32
    win_correct_path_for_FindFirstFile(s->path, sizeof(s->path));
#endif
    strcpy_s(s->pattern, sizeof(s->pattern), pattern);
    s->bufptr = s->path;
#ifdef _WIN32
    s->dir = INVALID_HANDLE_VALUE;
#else
    s->dir = NULL;
#endif
    return s;
}

#if 0 /* re-enable if we #define USE_OWN_GET_AUTH_DATA */
void *StandardSecurityHandler::getAuthData()
{
    return NULL;
}
#endif

char *makepath(char *buf, int buf_size, const char *path,
               const char *filename)
{
    strcpy_s(buf, buf_size, path);
    int len = strlen(path);
    if (len > 0 && path[len - 1] != DIR_SEP_CHAR && len + 1 < buf_size) {
        buf[len++] = DIR_SEP_CHAR;
        buf[len] = '\0';
    }
    strcat_s(buf, buf_size, filename);
    return buf;
}

#ifdef _WIN32
static int skip_matching_file(const char *filename)
{
    if (0 == strcmp(".", filename))
        return 1;
    if (0 == strcmp("..", filename))
        return 1;
    return 0;
}
#endif

int find_file_next(FindFileState *s, char *filename, int filename_size_max)
{
#ifdef _WIN32
    int    fFound;
    if (INVALID_HANDLE_VALUE == s->dir) {
        s->dir = FindFirstFile(s->path, &(s->fileinfo));
        if (INVALID_HANDLE_VALUE == s->dir)
            return -1;
        goto CheckFile;
    }

    while (1) {
        fFound = FindNextFile(s->dir, &(s->fileinfo));
        if (!fFound)
            return -1;
CheckFile:
        if (skip_matching_file(s->fileinfo.cFileName))
            continue;
        if (0 == fnmatch(s->pattern, s->fileinfo.cFileName, 0) ) {
            makepath(filename, filename_size_max, s->dirpath, s->fileinfo.cFileName);
            return 0;
        }
    }
#else
    struct dirent *dirent;
    const char *p;
    char *q;

    if (s->dir == NULL)
        goto redo;

    for (;;) {
        dirent = readdir(s->dir);
        if (dirent == NULL) {
        redo:
            if (s->dir) {
                closedir(s->dir);
                s->dir = NULL;
            }
            p = s->bufptr;
            if (*p == '\0')
                return -1;
            /* CG: get_str(&p, s->dirpath, sizeof(s->dirpath), ":") */
            q = s->dirpath;
            while (*p != ':' && *p != '\0') {
                if ((q - s->dirpath) < (int)sizeof(s->dirpath) - 1)
                    *q++ = *p;
                p++;
            }
            *q = '\0';
            if (*p == ':')
                p++;
            s->bufptr = p;
            s->dir = opendir(s->dirpath);
            if (!s->dir)
                goto redo;
        } else {
            if (fnmatch(s->pattern, dirent->d_name, 0) == 0) {
                makepath(filename, filename_size_max,
                         s->dirpath, dirent->d_name);
                return 0;
            }
        }
    }
#endif
}

void find_file_close(FindFileState *s)
{
#ifdef _WIN32
    if (INVALID_HANDLE_VALUE != s->dir)
       FindClose(s->dir);
#else
    if (s->dir)
        closedir(s->dir);
#endif
    free(s);
}

int StrList_Len(StrList **root)
{
    int         len = 0;
    StrList *   cur;
    assert(root);
    if (!root)
        return 0;
    cur = *root;
    while (cur) {
        ++len;
        cur = cur->next;
    }
    return len;
}

int StrList_InsertAndOwn(StrList **root, char *txt)
{
    StrList *   el;
    assert(root && txt);
    if (!root || !txt)
        return false;

    el = (StrList*)malloc(sizeof(StrList));
    if (!el)
        return false;
    el->str = txt;
    el->next = *root;
    *root = el;
    return true;
}

int StrList_Insert(StrList **root, char *txt)
{
    char *txtDup;

    assert(root && txt);
    if (!root || !txt)
        return false;
    txtDup = str_dup(txt);
    if (!txtDup)
        return false;

    if (!StrList_InsertAndOwn(root, txtDup)) {
        free((void*)txtDup);
        return false;
    }
    return true;
}

StrList* StrList_RemoveHead(StrList **root)
{
    StrList *tmp;
    assert(root);
    if (!root)
        return NULL;

    if (!*root)
        return NULL;
    tmp = *root;
    *root = tmp->next;
    tmp->next = NULL;
    return tmp;
}

void StrList_FreeElement(StrList *el)
{
    if (!el)
        return;
    free((void*)el->str);
    free((void*)el);
}

void StrList_Destroy(StrList **root)
{
    StrList *   cur;
    StrList *   next;

    if (!root)
        return;
    cur = *root;
    while (cur) {
        next = cur->next;
        StrList_FreeElement(cur);
        cur = next;
    }
    *root = NULL;
}

#ifndef _WIN32
void OutputDebugString(const char *txt)
{
    /* do nothing */
}
#define _snprintf snprintf
#define _vsnprintf vsnprintf
#endif

void my_error(int pos, char *msg, va_list args) {
#if 0
    char        buf[4096], *p = buf;

    // NB: this can be called before the globalParams object is created
    if (globalParams && globalParams->getErrQuiet()) {
        return;
    }

    if (pos >= 0) {
        p += _snprintf(p, sizeof(buf)-1, "Error (%d): ", pos);
        *p   = '\0';
        OutputDebugString(p);
    } else {
        OutputDebugString("Error: ");
    }

    p = buf;
    p += _vsnprintf(p, sizeof(buf) - 1, msg, args);
    while ( p > buf  &&  isspace(p[-1]) )
            *--p = '\0';
    *p++ = '\r';
    *p++ = '\n';
    *p   = '\0';
    OutputDebugString(buf);

    if (pos >= 0) {
        p += _snprintf(p, sizeof(buf)-1, "Error (%d): ", pos);
        *p   = '\0';
        OutputDebugString(buf);
        if (gErrFile)
            fprintf(gErrFile, buf);
    } else {
        OutputDebugString("Error: ");
        if (gErrFile)
            fprintf(gErrFile, "Error: ");
    }
#endif
#if 0
    p = buf;
    va_start(args, msg);
    p += _vsnprintf(p, sizeof(buf) - 3, msg, args);
    while ( p > buf  &&  isspace(p[-1]) )
            *--p = '\0';
    *p++ = '\r';
    *p++ = '\n';
    *p   = '\0';
    OutputDebugString(buf);
    if (gErrFile)
        fprintf(gErrFile, buf);
    va_end(args);
#endif
}

void LogInfo(char *fmt, ...)
{
    va_list args;
    char        buf[4096], *p = buf;

    p = buf;
    va_start(args, fmt);
    p += _vsnprintf(p, sizeof(buf) - 1, fmt, args);
    *p   = '\0';
    fprintf(gOutFile, "%s", buf);
    va_end(args);
    fflush(gOutFile);
}

static void PrintUsageAndExit(int argc, char **argv)
{
    printf("Usage: pdftest [-preview|-slowpreview] [-loadonly] [-timings] [-text] [-resolution NxM] [-recursive] [-page N] [-out out.txt] pdf-files-to-process\n");
    for (int i=0; i < argc; i++) {
        printf("i=%d, '%s'\n", i, argv[i]);
    }
    exit(0);
}

static bool ShowPreview(void)
{
    if (gfPreview || gfSlowPreview)
        return true;
    return false;
}

static void RenderPdfAsText(const char *fileName)
{
    GooString *         fileNameStr = NULL;
    PDFDoc *            pdfDoc = NULL;
    GooString *         txt = NULL;
    int                 pageCount;
    double              timeInMs;

    assert(fileName);
    if (!fileName)
        return;

    LogInfo("started: %s\n", fileName);

    TextOutputDev * textOut = new TextOutputDev(NULL, gTrue, gFalse, gFalse);
    if (!textOut->isOk()) {
        delete textOut;
        return;
    }

    GooTimer msTimer;
    /* note: don't delete fileNameStr since PDFDoc takes ownership and deletes them itself */
    fileNameStr = new GooString(fileName);
    if (!fileNameStr)
        goto Exit;

    pdfDoc = new PDFDoc(fileNameStr, NULL, NULL, NULL);
    if (!pdfDoc->isOk()) {
        error(-1, "RenderPdfFile(): failed to open PDF file %s\n", fileName);
        goto Exit;
    }

    msTimer.stop();
    timeInMs = msTimer.getElapsed();
    LogInfo("load: %.2f ms\n", timeInMs);

    pageCount = pdfDoc->getNumPages();
    LogInfo("page count: %d\n", pageCount);

    for (int curPage = 1; curPage <= pageCount; curPage++) {
        if ((gPageNo != PAGE_NO_NOT_GIVEN) && (gPageNo != curPage))
            continue;

        msTimer.start();
        int rotate = 0;
        GBool useMediaBox = gFalse;
        GBool crop = gTrue;
        GBool doLinks = gFalse;
        pdfDoc->displayPage(textOut, curPage, 72, 72, rotate, useMediaBox, crop, doLinks);
        txt = textOut->getText(0.0, 0.0, 10000.0, 10000.0);
        msTimer.stop();
        timeInMs = msTimer.getElapsed();
        if (gfTimings)
            LogInfo("page %d: %.2f ms\n", curPage, timeInMs);
        printf("%s\n", txt->getCString());
        delete txt;
        txt = NULL;
    }

Exit:
    LogInfo("finished: %s\n", fileName);
    delete textOut;
    delete pdfDoc;
}

#ifdef _MSC_VER
#define POPPLER_TMP_NAME "c:\\poppler_tmp.pdf"
#else
#define POPPLER_TMP_NAME "/tmp/poppler_tmp.pdf"
#endif

static void RenderPdf(const char *fileName)
{
    const char *        fileNameSplash = NULL;
    PdfEnginePoppler *  engineSplash = NULL;
    int                 pageCount;
    double              timeInMs;

#ifdef COPY_FILE
    // TODO: fails if file already exists and has read-only attribute
    CopyFile(fileName, POPPLER_TMP_NAME, false);
    fileNameSplash = POPPLER_TMP_NAME;
#else
    fileNameSplash = fileName;
#endif
    LogInfo("started: %s\n", fileName);

    engineSplash = new PdfEnginePoppler();

    GooTimer msTimer;
    if (!engineSplash->load(fileNameSplash)) {
        LogInfo("failed to load splash\n");
        goto Error;
    }
    msTimer.stop();
    timeInMs = msTimer.getElapsed();
    LogInfo("load splash: %.2f ms\n", timeInMs);
    pageCount = engineSplash->pageCount();

    LogInfo("page count: %d\n", pageCount);
    if (gfLoadOnly)
        goto Error;

    for (int curPage = 1; curPage <= pageCount; curPage++) {
        if ((gPageNo != PAGE_NO_NOT_GIVEN) && (gPageNo != curPage))
            continue;

        SplashBitmap *bmpSplash = NULL;

        GooTimer msTimer;
        bmpSplash = engineSplash->renderBitmap(curPage, 100.0, 0);
        msTimer.stop();
        double timeInMs = msTimer.getElapsed();
        if (gfTimings) {
            if (!bmpSplash)
                LogInfo("page splash %d: failed to render\n", curPage);
            else
                LogInfo("page splash %d (%dx%d): %.2f ms\n", curPage, bmpSplash->getWidth(), bmpSplash->getHeight(), timeInMs);
        }

        if (ShowPreview()) {
            PreviewBitmapSplash(bmpSplash);
            if (gfSlowPreview)
                sleep_milliseconds(SLOW_PREVIEW_TIME);
        }
        delete bmpSplash;
    }
Error:
    delete engineSplash;
    LogInfo("finished: %s\n", fileName);
}

static void RenderFile(const char *fileName)
{
    if (gfTextOnly) {
        RenderPdfAsText(fileName);
        return;
    }

    RenderPdf(fileName);
}

static bool ParseInteger(const char *start, const char *end, int *intOut)
{
    char            numBuf[16];
    int             digitsCount;
    const char *    tmp;

    assert(start && end && intOut);
    assert(end >= start);
    if (!start || !end || !intOut || (start > end))
        return false;

    digitsCount = 0;
    tmp = start;
    while (tmp <= end) {
        if (isspace(*tmp)) {
            /* do nothing, we allow whitespace */
        } else if (!isdigit(*tmp))
            return false;
        numBuf[digitsCount] = *tmp;
        ++digitsCount;
        if (digitsCount == dimof(numBuf)-3) /* -3 to be safe */
            return false;
        ++tmp;
    }
    if (0 == digitsCount)
        return false;
    numBuf[digitsCount] = 0;
    *intOut = atoi(numBuf);
    return true;
}

/* Given 'resolutionString' in format NxM (e.g. "100x200"), parse the string and put N
   into 'resolutionXOut' and M into 'resolutionYOut'.
   Return false if there was an error (e.g. string is not in the right format */
static bool ParseResolutionString(const char *resolutionString, int *resolutionXOut, int *resolutionYOut)
{
    const char *    posOfX;

    assert(resolutionString);
    assert(resolutionXOut);
    assert(resolutionYOut);
    if (!resolutionString || !resolutionXOut || !resolutionYOut)
        return false;
    *resolutionXOut = 0;
    *resolutionYOut = 0;
    posOfX = strchr(resolutionString, 'X');
    if (!posOfX)
        posOfX = strchr(resolutionString, 'x');
    if (!posOfX)
        return false;
    if (posOfX == resolutionString)
        return false;
    if (!ParseInteger(resolutionString, posOfX-1, resolutionXOut))
        return false;
    if (!ParseInteger(posOfX+1, resolutionString+strlen(resolutionString)-1, resolutionYOut))
        return false;
    return true;
}

static void ParseCommandLine(int argc, char **argv)
{
    char *      arg;

    if (argc < 2)
        PrintUsageAndExit(argc, argv);

    for (int i=1; i < argc; i++) {
        arg = argv[i];
        assert(arg);
        if ('-' == arg[0]) {
            if (str_ieq(arg, TIMINGS_ARG)) {
                gfTimings = true;
            } else if (str_ieq(arg, RESOLUTION_ARG)) {
                ++i;
                if (i == argc)
                    PrintUsageAndExit(argc, argv); /* expect a file name after that */
                if (!ParseResolutionString(argv[i], &gResolutionX, &gResolutionY))
                    PrintUsageAndExit(argc, argv);
                gfForceResolution = true;
            } else if (str_ieq(arg, RECURSIVE_ARG)) {
                gfRecursive = true;
            } else if (str_ieq(arg, OUT_ARG)) {
                /* expect a file name after that */
                ++i;
                if (i == argc)
                    PrintUsageAndExit(argc, argv);
                gOutFileName = str_dup(argv[i]);
            } else if (str_ieq(arg, PREVIEW_ARG)) {
                gfPreview = true;
            } else if (str_ieq(arg, TEXT_ARG)) {
                gfTextOnly = true;
            } else if (str_ieq(arg, SLOW_PREVIEW_ARG)) {
                gfSlowPreview = true;
            } else if (str_ieq(arg, LOAD_ONLY_ARG)) {
                gfLoadOnly = true;
            } else if (str_ieq(arg, PAGE_ARG)) {
                /* expect an integer after that */
                ++i;
                if (i == argc)
                    PrintUsageAndExit(argc, argv);
                gPageNo = atoi(argv[i]);
                if (gPageNo < 1)
                    PrintUsageAndExit(argc, argv);
            } else {
                /* unknown option */
                PrintUsageAndExit(argc, argv);
            }
        } else {
            /* we assume that this is not an option hence it must be
               a name of PDF/directory/file with PDF names */
            StrList_Insert(&gArgsListRoot, arg);
        }
    }
}

#if 0
void RenderFileList(char *pdfFileList)
{
    char *data = NULL;
    char *dataNormalized = NULL;
    char *pdfFileName;
    uint64_t fileSize;

    assert(pdfFileList);
    if (!pdfFileList)
        return;
    data = file_read_all(pdfFileList, &fileSize);
    if (!data) {
        error(-1, "couldn't load file '%s'", pdfFileList);
        return;
    }
    dataNormalized = str_normalize_newline(data, UNIX_NEWLINE);
    if (!dataNormalized) {
        error(-1, "couldn't normalize data of file '%s'", pdfFileList);
        goto Exit;
    }
    for (;;) {
        pdfFileName = str_split_iter(&dataNormalized, UNIX_NEWLINE_C);
        if (!pdfFileName)
            break;
        str_strip_ws_both(pdfFileName);
        if (str_empty(pdfFileName)) {
            free((void*)pdfFileName);
            continue;
        }
        RenderFile(pdfFileName);
        free((void*)pdfFileName);
    }
Exit:
    free((void*)dataNormalized);
    free((void*)data);
}
#endif

#ifdef _WIN32
#include <sys/types.h>
#include <sys/stat.h>

bool IsDirectoryName(char *path)
{
    struct _stat    buf;
    int             result;

    result = _stat(path, &buf );
    if (0 != result)
        return false;

    if (buf.st_mode & _S_IFDIR)
        return true;

    return false;
}

bool IsFileName(char *path)
{
    struct _stat    buf;
    int             result;

    result = _stat(path, &buf );
    if (0 != result)
        return false;

    if (buf.st_mode & _S_IFREG)
        return true;

    return false;
}
#else
bool IsDirectoryName(char *path)
{
    /* TODO: implement me */
    return false;
}

bool IsFileName(char *path)
{
    /* TODO: implement me */
    return true;
}
#endif

bool IsPdfFileName(char *path)
{
    if (str_endswith(path, ".pdf"))
        return true;
    return false;
}

static void RenderDirectory(char *path)
{
    FindFileState * ffs;
    char            filename[MAX_FILENAME_SIZE];
    StrList *       dirList = NULL;
    StrList *       el;

    StrList_Insert(&dirList, path);

    while (0 != StrList_Len(&dirList)) {
        el = StrList_RemoveHead(&dirList);
        ffs = find_file_open(el->str, "*");
        while (!find_file_next(ffs, filename, sizeof(filename))) {
            if (IsDirectoryName(filename)) {
                if (gfRecursive) {
                    StrList_Insert(&dirList, filename);
                }
            } else if (IsFileName(filename)) {
                if (IsPdfFileName(filename)) {
                    RenderFile(filename);
                }
            }
        }
        find_file_close(ffs);
        StrList_FreeElement(el);
    }
    StrList_Destroy(&dirList);
}

/* Render 'cmdLineArg', which can be:
   - directory name
   - name of PDF file
   - name of text file with names of PDF files
*/
static void RenderCmdLineArg(char *cmdLineArg)
{
    assert(cmdLineArg);
    if (!cmdLineArg)
        return;
    if (IsDirectoryName(cmdLineArg)) {
        RenderDirectory(cmdLineArg);
    } else if (IsFileName(cmdLineArg)) {
        if (IsPdfFileName(cmdLineArg))
            RenderFile(cmdLineArg);
#if 0
        else
            RenderFileList(cmdLineArg);
#endif
    } else {
        error(-1, "unexpected argument '%s'", cmdLineArg);
    }
}

int main(int argc, char **argv)
{
    setErrorFunction(my_error);
    ParseCommandLine(argc, argv);
    if (0 == StrList_Len(&gArgsListRoot))
        PrintUsageAndExit(argc, argv);
    assert(gArgsListRoot);

    SplashColorsInit();
    globalParams = new GlobalParams();
    if (!globalParams)
        return 1;
    globalParams->setErrQuiet(gFalse);
    globalParams->setBaseDir("");

    FILE * outFile = NULL;
    if (gOutFileName) {
        outFile = fopen(gOutFileName, "wb");
        if (!outFile) {
            printf("failed to open -out file %s\n", gOutFileName);
            return 1;
        }
        gOutFile = outFile;
    }
    else
        gOutFile = stdout;

    if (gOutFileName)
        gErrFile = outFile;
    else
        gErrFile = stderr;

    PreviewBitmapInit();

    StrList * curr = gArgsListRoot;
    while (curr) {
        RenderCmdLineArg(curr->str);
        curr = curr->next;
    }
    if (outFile)
        fclose(outFile);
    PreviewBitmapDestroy();
    StrList_Destroy(&gArgsListRoot);
    delete globalParams;
    free(gOutFileName);
    return 0;
}

