/*   GPDriver source image.c
 *   $Id: image.c 1.13 2004/11/26 17:03:42 root Exp root $
 *
 *   GPDriver - gimp-print based printer spooler for RISC OS
 *   Copyright (C) 2003 Martin Wuerthner
 *
 *   This program is free software; you can redistribute it and/or modify it
 *   under the terms of the GNU General Public License as published by the Free
 *   Software Foundation; either version 2 of the License, or (at your option)
 *   any later version.
 *
 *   This program is distributed in the hope that it will be useful, but
 *   WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 *   or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 *   for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program; if not, write to the Free Software
 *   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 *
 *   Martin Wuerthner <ro-printing@mw-software.com>
 *   Mannheimer Str. 18, 67655 Kaiserslautern, Germany
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "gimp-print/gimp-print.h"
#include "oslib/osgbpb.h"
#include "oslib/osargs.h"
#include "image.h"
#include "gpd_gui.h"

#define DEBUG 0
#include "debug.h"

#if (GIMPPRINT_MINOR_VERSION < 3)
  #define STP_IMAGE_STATUS_ABORT STP_IMAGE_ABORT
  #define STP_IMAGE_STATUS_OK    STP_IMAGE_OK
#endif

/* a gpd_image is a structure that describes a page to be printed - it is an in-memory
   interface to the ROUGPS print job file */
/* it can be seen as an adaptor between the ROUGPS job and the stp_image interface used
   by gimp-print */
struct gpd_image_s {
  /* The first fields are loaded directly from the job file header */
  /* so their layout is the same as in the job file */
  int nrows;           /* number of rows on page (height) */
  int npix;            /* number of pixels per row (width) */
  int firstr;          /* first non-empty row (first encoded row), equals nrows if none */
  int lastr;           /* last non-empty row (last encoded row), inclusive */
  int firstp;          /* leftmost pixel found */
  int lastp;           /* rightmost pixel found */
  int topm;            /* number of pixel rows to skip at top */
  int leftm;           /* number of pixels to skip at left margin */
  int jflags;          /* job flags */
  int copies;          /* number of copies requested */
  int pwidth;          /* paper width (millipoints) */
  int pheight;         /* paper height (mp) */
  int lowx;            /* printable area x0 */
  int lowy;            /*                y0 */
  int highx;           /*                x1 */
  int highy;           /*                y1 */
  int xres;
  int yres;
  int snamoff;         /* offset to short printer name (as returned by PDriver_Info */
  int pnamoff;         /* offset to long printer name from PDF as used by gimp-print */
  int rnamoff;         /* offset to resolution name from PDF as used by gimp-print */
  int rowoff;          /* offset from start of file to first encoded row */
  /* end of file header fields */

  /* job file descriptor */
  /* the first non-header field, this property is used below to work out the size of the header! */
  os_fw job_file_h;    /* the file handle of the ROUGPS job file */
  int size;            /* size of the file */

  char* sname;         /* short printer name as read from job file */
  char* pname;         /* long printer name as read from job file */
  char* rname;         /* resolution name as read from job file */

  /* image descriptor */
  int width;           /* size of our bitmap data in XRES/YRES resolution*/
  int height;
  int virtual_width;   /* virtual size for gimp-print with square pixel size */
  int virtual_height;
  int virtual_res;     /* the uniform resolution of the virtual square pixel bitmap */
  int vden;            /* the vertical multiplication factor denominator */
  int vnum;            /* the vertical multiplication factor numerator */
  int bpp;             /* for gimp-print */

  stp_image_t* stp_image;     /* the stp_image pointer to be passed to gimp-print */

  /* gui variables */
  void (*progress_func)(int,int);

  /* caching variables */
  char* buffer;               /* input buffer */
  int current_off;            /* position of file pointer in job file */
  int current_row;            /* row number that is currently cached at offset boff in buffer
                                 (gimp-print numbering, i.e., row 0 is firstr) */
  int boff;                   /* offset of current row in buffer */
  int cached;                 /* number of buffered bytes left */
};

/* job flags */
#define JFLAGS_COLOUR (1<<0)

/* current file version we understand */
#define CURRENT_FVER (100)

typedef struct gpd_row_s {
  int len;
  int firstp;
  int lastp;
  char cid;
  char pad1, pad2, pad3;
} gpd_row_t;

#define CID_UNCOMPRESSED 0
#define CID_SIMPLE_RLE 1

#define BUFFSIZE (256 * 1024)

os_error could_not_allocate = { 0, "Could not allocate string buffer" };

os_error* read_word(os_fw file_h, int* wordp);
os_error* read_block(os_fw file_h, char* buff, int num);
os_error* read_string(os_fw file_h, int offset, char** buffp);
int read_next_row(gpd_image_t* img);
int decompress_row(gpd_image_t* img, unsigned char *data);

/* callback functions for gimp-print */
void init_image(struct stp_image *image);
void reset_image(struct stp_image *image);
void progress_init_image(struct stp_image *image);
void note_progress_image(struct stp_image *image, double current, double total);
void progress_conclude_image(struct stp_image *image);
const char *get_image_appname(struct stp_image *image);
int get_image_bpp(struct stp_image *image);
int get_image_width(struct stp_image *image);
int get_image_height(struct stp_image *image);
stp_image_status_t get_image_row(struct stp_image *image, unsigned char *data,
#if (GIMPPRINT_MINOR_VERSION >= 3)
  size_t byte_limit,
#endif
int row);

extern gpd_image_t* gpdi_create_image(os_fw job_file_h, void (*progress_func)(int,int))
{
  struct gpd_image_s* img;
  stp_image_t* stp_image;
  int id, fver;

  /* check the job file identification word */
  if (gui_complain(read_word(job_file_h, &id))) return NULL;
  if (id != 'P' + ('D' << 8) + ('G' << 16) + ('P' << 24)) {
    gui_error2("Unknown job file identification"); return NULL;
  }
  if (gui_complain(read_word(job_file_h, &fver))) return NULL;
  if (fver > CURRENT_FVER) {
    gui_error2("Job file was produced for a more recent version of GPDriver");
    return NULL;
  }
  /* OK, we can read the job file */
  img = malloc(sizeof(struct gpd_image_s));
  if (!img) {
    gui_error2("Not enough memory to allocate image structure"); return NULL;
  }
  img->job_file_h = job_file_h;
  if (gui_complain(read_block(job_file_h, (char*)(&img->nrows), ((char*)&(img->job_file_h)) - ((char*)img)))) return NULL;

  REPORT_D("NROWS = %d",img->nrows)
  REPORT_D("NPIX = %d",img->npix)
  REPORT_D("FIRSTR = %d",img->firstr)
  REPORT_D("LASTR = %d",img->lastr)
  REPORT_D("FIRSTP = %d",img->firstp)
  REPORT_D("LASTP = %d",img->lastp)

#if 0
  printf("NROWS = %d\nNPIX = %d\nFIRSTR=%d\nLASTR=%d\n"
         "FIRSTP = %d\nLASTP = %d\n"
         "TOPM = %d\nLEFTM = %d\n"
         "PWIDTH = %d\nPHEIGHT = %d\n"
         "XRES = %d\nYRES = %d\nROWOFF = %d\n",
         img->nrows, img->npix, img->firstr, img->lastr,
         img->firstp, img->lastp, img->topm, img->leftm, img->pwidth, img->pheight,
         img->xres, img->yres,
         img->rowoff);
#endif

  if (gui_complain(read_string(job_file_h, img->snamoff, &img->sname))) return NULL;
  /* printf("short printer name: %s\n", img->sname); */
  if (gui_complain(read_string(job_file_h, img->pnamoff, &img->pname))) return NULL;
  /* printf("long printer name: %s\n", img->pname); */
  if (gui_complain(read_string(job_file_h, img->rnamoff, &img->rname))) return NULL;
  /* printf("resolution name: %s\n", img->rname); */

  img->progress_func = progress_func;

  if (img->firstr < img->nrows && img->firstp < img->npix) {
    img->width = img->lastp - img->firstp + 1;
    img->height = img->lastr - img->firstr + 1;
  }
  else {
    /* empty page - lastr, lastp undefined - we fake a 1x1 bitmap to allow us to
       use the driver to actually print an empty page */
    /* of course, get_image_row below must be prepared to still return the row
       when requested, so it tests for img->firstr > img->nrows || img->firstp > img->npix
       and returns empty rows only in this case */
    img->width = 1; img->height = 1;
  }
  REPORT_D("width = %d",img->width)
  REPORT_D("height = %d",img->height)
  /* printf("width = %d\nheight = %d\n", img->width, img->height); */
  img->bpp = 3;   /* true-colour RGB */

  if (img->xres == img->yres) {
    img->virtual_width = img->width;
    img->virtual_height = img->height;
    img->virtual_res = img->xres;
    img->vden = 1; img->vnum = 1;
  }
  else if (img->xres > img->yres) {
    /* higher horizontal resolution, so fake an image of the same vertical resolution */
    /* this code is pretty stupid, a gcd computation would support all ratios */
    if (img->xres == 2 * img->yres) {
      img->virtual_width = img->width;
      img->virtual_height = 2 * img->height;
      img->virtual_res = img->xres;
      img->vden = 2; img->vnum = 1;
    }
    else if (img->xres == 3 * img->yres) {
      img->virtual_width = img->width;
      img->virtual_height = 3 * img->height;
      img->virtual_res = img->xres;
      img->vden = 3; img->vnum = 1;
    }
    else if (2 * img->xres == 3 * img->yres) {
      img->virtual_width = img->width;
      img->virtual_height = (3 * img->height) / 2;
      img->virtual_res = img->xres;
      img->vden = 3; img->vnum = 2;
    }
    else if (img->xres == 4 * img->yres) {
      img->virtual_width = img->width;
      img->virtual_height = 4 * img->height;
      img->virtual_res = img->xres;
      img->vden = 4; img->vnum = 1;
    }
    else {
      gui_fatal_error2("GPDriver does not handle source bitmaps with XRES > YRES except for XRES:YRES = 1:2, 1:3, 1:4 and 3:2");
    }
  }
  else if (img->yres > img->xres) {
    /* we do not do this for the time being */
    /* it would well be possible by faking the same horizontal resolution
       and then expanding the pixel rows to that higher resolution, but
       this will really cost processing time while faking a higher vertical
       resolution is for free - gimp-print will simply not enquire about the
       additional pixel rows we invented */
    gui_fatal_error2("GPDriver does not handle source bitmaps with XRES < YRES");
  }
  /* printf("Gimp-print virtual image width = %d, height = %d\n", img->virtual_width, img->virtual_height); */

  /* so far we have not created an stp_image structure */
  stp_image = malloc(sizeof(stp_image_t));
  stp_image->init = &init_image;
  stp_image->reset = &reset_image;
  stp_image->transpose = NULL;
  stp_image->hflip = NULL;
  stp_image->vflip = NULL;
  stp_image->crop = NULL;
  stp_image->rotate_ccw = NULL;
  stp_image->rotate_cw = NULL;
  stp_image->rotate_180 = NULL;
  stp_image->bpp = &get_image_bpp;
  stp_image->width = &get_image_width;
  stp_image->height = &get_image_height;
  stp_image->get_row = &get_image_row;
  stp_image->get_appname = &get_image_appname;
  stp_image->progress_init = &progress_init_image;
  stp_image->note_progress = &note_progress_image;
  stp_image->progress_conclude = &progress_conclude_image;
  stp_image->rep = img;

  img->stp_image = stp_image;

  /* position the file pointer at the first row */
  img->buffer = malloc(BUFFSIZE);
  if (!img->buffer) {
    gui_error2("Not enough memory for buffer"); return NULL;
  }
  if (gui_complain(xosargs_read_extw(job_file_h, &img->size))) return NULL;
  if (!gpdi_reset_image(img)) return NULL;
  return img;
}

extern void gpdi_dispose(gpd_image_t* img)
{
  free(img->buffer);
  free(img->stp_image);
  free(img);
}

/* get the short printer name used to create the image */
extern const char* gpdi_get_short_name(gpd_image_t* img)
{
  return img->sname;
}

/* get the long printer name used to create the image */
extern const char* gpdi_get_long_name(gpd_image_t* img)
{
  return img->pname;
}

/* get the resolution name used to create the image */
extern const char* gpdi_get_resolution_name(gpd_image_t* img)
{
  return img->rname;
}

/* get the x coordinate of the left edge of the printable area in millipoints */
extern int gpdi_get_page_x0_mp(gpd_image_t* img) { return img->lowx; }
/* get the y coordinate of the bottom edge of the printable area in millipoints */
extern int gpdi_get_page_y0_mp(gpd_image_t* img) { return img->lowy; }
/* get the x coordinate of the right edge of the printable area in millipoints */
extern int gpdi_get_page_x1_mp(gpd_image_t* img) { return img->highx; }
/* get the y coordinate of the top edge of the printable area in millipoints */
extern int gpdi_get_page_y1_mp(gpd_image_t* img) { return img->highy; }

/* get the media width in millipoints */
extern int gpdi_get_pwidth_mp(gpd_image_t* img) { return img->pwidth; }
/* get the media heigth in millipoints */
extern int gpdi_get_pheight_mp(gpd_image_t* img) { return img->pheight; }

/* get the width of the bitmap in pixels */
extern int gpdi_get_width(gpd_image_t* img) { return img->width; }
/* get the height of the bitmap in pixels */
extern int gpdi_get_height(gpd_image_t* img) { return img->height; }

/* get the x resolution of the bitmap */
extern int gpdi_get_xres(gpd_image_t* img) { return img->xres; }
/* get the y resolution of the bitmap */
extern int gpdi_get_yres(gpd_image_t* img) { return img->yres; }

/* get the first non-empty row on the page */
extern int gpdi_get_firstp(gpd_image_t* img) { return img->firstp; }
/* get the first non-empty column on the page */
extern int gpdi_get_firstr(gpd_image_t* img) { return img->firstr; }

/* get the number of pixel rows to skip at top of page */
extern int gpdi_get_topm(gpd_image_t* img) { return img->topm; }
/* get the number of pixels to skip at the left margin */
extern int gpdi_get_leftm(gpd_image_t* img) { return img->leftm; }

/* get the number of copies requested */
extern int gpdi_get_copies(gpd_image_t* img) { return img->copies; }

/* get the virtual resolution of the bitmap (uniform) */
extern int gpdi_get_virtual_res(gpd_image_t* img) { return img->virtual_res; }

/* get the underlying stp_image pointer */
extern stp_image_t* gpdi_get_stp_image(gpd_image_t* img)
{
  return img->stp_image;
}

void init_image(struct stp_image *image)
{
  /* do nothing */
  /* printf("init_image\n"); */
}

void reset_image(struct stp_image *image)
{
  /* do nothing */
  /* printf("reset_image\n"); */
}

int get_image_bpp(struct stp_image *image)
{
  return ((struct gpd_image_s*)(image->rep))->bpp;
}

int get_image_width(struct stp_image *image)
{
  return ((struct gpd_image_s*)(image->rep))->virtual_width;
}

int get_image_height(struct stp_image *image)
{
  return ((struct gpd_image_s*)(image->rep))->virtual_height;
}

/*---------------*/
/* get image row */
/*---------------*/
/* MAIN CALLBACK FUNCTION for gimp-print - called to get an image row */
stp_image_status_t
get_image_row(struct stp_image *image, unsigned char *data,
#if (GIMPPRINT_MINOR_VERSION >= 3)
 size_t byte_limit,
#endif
int row)
{
  gpd_image_t* img = (gpd_image_t*)image->rep;
  REPORT_D("get_row (virtual) %d", row)
  /* printf("get_row (virtual) %d\n",row); */
  if (img->vnum != 1 || img->vden != 1) {
    /* we have faked a higher vertical resolution, so compute the real row */
    int vrow = row;
    row = row * img->vnum / img->vden;
    /* if this does not exactly hit a real row we need to round up,
       otherwise 2:3 ratio will not work */
    if (row * img->vden != vrow * img->vnum) row++;
    REPORT_D("get_row (real) %d", row)
    /* printf("get_row (real) %d\n",row); */
  }
  /* call the user interface progress function */
  img->progress_func(row, img->height);

  if (gui_aborted()) return STP_IMAGE_STATUS_ABORT;

  if (img->firstr >= img->nrows || img->firstp >= img->npix) {
    /* we actually have an empty page, so return an empty row */
    memset(data, 255, 3 * img->virtual_width);
    return STP_IMAGE_STATUS_OK;
  }

  /* read until the requested row is in the cache */
  while(img->current_row < row) {
    if (!read_next_row(img)) return STP_IMAGE_STATUS_ABORT;
  }
  if (!decompress_row(img, data)) return STP_IMAGE_STATUS_ABORT;
  return STP_IMAGE_STATUS_OK;
}

/* decompress the current row into the buffer pointed to by data */
int decompress_row(gpd_image_t* img, unsigned char *data)
{
  gpd_row_t* row = (gpd_row_t*)(img->buffer + img->boff);
  /* gimp-print expects a row of size img->virtual_width */
  REPORT_D("decompress, row->len = %d", row->len)
  if (row->len == 4) {
    /* empty row */
    memset(data, 255, 3 * img->virtual_width);
  }
  else {
    /* a real row to decompress */
    unsigned char* buffer = ((unsigned char*)row) + sizeof(gpd_row_t);
    if (img->firstp < row->firstp) {
      /* there are empty pixels at the left of the row */
      REPORT_D("%d leading empty pixels\n",row->firstp - img->firstp)
      memset(data, 255, 3 * (row->firstp - img->firstp));
      data += 3 * (row->firstp - img->firstp);
    }
    if (row->firstp <= row->lastp) {
      /* there are some real pixels (should always be the case) */
      if (row->cid == CID_UNCOMPRESSED) {
        /* copy the data across */
        int dsize = 3 * (row->lastp - row->firstp + 1);
        REPORT_D("%d uncompressed data pixels\n",dsize / 3);
        memcpy(data, buffer, dsize);
        data += dsize; buffer += dsize;
      }
      else if (row->cid == CID_SIMPLE_RLE) {
        /* expand the RLE data */
        unsigned char xbyte = row->pad1;
        int pixels = 0;
        const int to_do = row->lastp - row->firstp + 1;
        REPORT_D("RLE to_do = %d", to_do)
        while(pixels < to_do) {
          unsigned char b = *buffer++;
          if (b == xbyte) {
            int num = *buffer++;
            unsigned char p1 = *buffer++;
            unsigned char p2 = *buffer++;
            unsigned char p3 = *buffer++;
            ++num;       /* the counter as stored represented the number of additional bytes */
            while(num--) {
              *data++ = p1; *data++ = p2; *data++ = p3;
              pixels++;
            }
          }
          else {
            *data++ = b; *data++ = *buffer++; *data++ = *buffer++;
            pixels++;
          }
        }
      }
      else {
        gui_error2("Unknown compression type");
        return 0;
      }
    }
    if (img->lastp > row->lastp) {
      /* there are empty pixels at the right of the row */
      REPORT_D("%d trailing pixels\n",img->lastp - row->lastp);
      memset(data, 255, 3 * (img->lastp - row->lastp));
    }
  }
  return 1;
}

const char *get_image_appname(struct stp_image *image)
{
  return "GPDriver";
}

void progress_init_image(struct stp_image *image)
{
  /* do nothing */
  /* printf("progress_init\n"); */
}

void note_progress_image(struct stp_image *image, double current, double total)
{
#if 0
  if (total != 0.0) printf("Completed %.2f%%\n",100 * current / total);
#endif
}

void progress_conclude_image(struct stp_image *image)
{
  /* do nothing */
  /* printf("progress_conclude\n");*/
}

os_error* read_word(os_fw file_h, int* wordp)
{
  return read_block(file_h, (char*) wordp, 4);
}

os_error* read_block(os_fw file_h, char* buff, int num)
{
  os_error* err = NULL;
  byte* p = (byte*)buff;
  int oldnum;
  while(num) {
    oldnum = num;
    if ((err=xosgbpb_readw(file_h,p,num,&num)) != NULL) return err;
    p += oldnum - num;
  }
  return err;
}

/* read a string from a given file position and put a pointer to it
   at a given address */
os_error* read_string(os_fw file_h, int offset, char** buffp)
{
  os_error* err;
  int len = 0;
  char* buff;
  char b;
  if ((err = xosargs_set_ptrw(file_h, offset)) != NULL) return err;
  do {
    bits psr;
    if ((err = xos_bget(file_h, &b, &psr)) != NULL) return err;
    len++;
  } while(b);
  buff = malloc(len);        /* len includes the terminating '\0' */
  if (!buff) return &could_not_allocate;
  *buffp = buff;
  if ((err = xosargs_set_ptrw(file_h, offset)) != NULL) return err;
  return read_block(file_h, buff, len);
}

/* make sure that the next row is fully present in the buffer */
int read_next_row(gpd_image_t* img)
{
  int space_in_buffer = BUFFSIZE - img->cached;
  int left_in_file, ll;

  REPORT_D("read_next_row img->cached = %d", img->cached)

    if (img->cached) {
      /* a full row is in the cache */
      int row_len = *((int*)(img->buffer + img->boff));
      REPORT_D("prev row_len = %d", row_len)
      img->boff += row_len;
      img->cached -= row_len;

      if (img->cached) {
        /* we need this check because we could have just consumed the complete rest of the buffer,
           which would mean that the following length field is not in the cache! */
        row_len = *((int*)(img->buffer + img->boff));
        REPORT_D("current row_len = %d", row_len)
        if (row_len <= img->cached) {
          /* the full row is in memory */
          REPORT("PRESENT")
          img->current_row++;
          return 1;
        }
        else {
          REPORT("MOVE")
          /* we need to cache some more data, so move remainder to beginning */
          memmove(img->buffer, img->buffer + img->boff, img->cached);
        }
      }
    }

    /* fill buffer */
    left_in_file = img->size - img->current_off;
    ll = left_in_file<space_in_buffer?left_in_file:space_in_buffer;
    REPORT_D("Reading %d bytes", ll)
    if (gui_complain(read_block(img->job_file_h, img->buffer + img->cached, ll))) return 0;
    img->current_off += ll;
    img->cached += ll;
    img->boff = 0;
    if (*((int*)(img->buffer)) > BUFFSIZE) {
      gui_error2("Error: Row is larger than buffer size!"); return 0;
    }
    img->current_row++;
    return 1;
}

/* reset the image, so it can be printed again */
int gpdi_reset_image(gpd_image_t* img)
{
  if (gui_complain(xosargs_set_ptrw(img->job_file_h, img->rowoff))) return FALSE;
  img->current_off = img->rowoff;
  img->current_row = -1;
  img->cached = 0;
  img->boff = 0;
  return TRUE;
}
