/* printer.c: Printer support
   Copyright (c) 2001-2004 Ian Collier, Russell Marks, Philip Kendall

   $Id: printer.c,v 1.19 2004/04/19 21:47:15 pak21 Exp $

   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

   Author contact information:

   E-mail: pak21-fuse@srcf.ucam.org
   Postal address: 15 Crescent Road, Wokingham, Berks, RG40 2DB, England

*/

/* The ZX Printer support is based on Ian Collier's emulation in xz80.
 * Well, `based' is an understatement, it's almost exactly the same. :-)
 */
   

#include <stdio.h>
#include <string.h>
#include <ctype.h>

#include <config.h>

#include "fuse.h"
#include "machine.h"
#include "memory.h"
#include "printer.h"
#include "settings.h"
#include "ui/ui.h"

static int printer_graphics_enabled=0;
static int printer_text_enabled=0;
static FILE *printer_graphics_file=NULL;
static FILE *printer_text_file=NULL;

/* for the ZX Printer */
static int zxpframes,zxpspeed,zxpnewspeed;
static libspectrum_dword zxpcycles;
static int zxpheight,zxppixel,zxpstylus;
static unsigned char zxpline[256];
static unsigned int frames=0;
static unsigned int zxplineofchar=0;

/* last 8 pixel lines output, as bitmap */
static unsigned char zxplast8[32*8];


/* for parallel */
static unsigned char parallel_data=0;

/* see printer_parallel_strobe_write() comment; must be less than one
 * frame's worth.
 */
#define PARALLEL_STROBE_MAX_CYCLES	10000



static void printer_zxp_init(void)
{
zxpstylus=zxpspeed=zxpheight=zxpnewspeed=zxplineofchar=0;
zxppixel=-1;
}


static void printer_text_init(void)
{
/* nothing yet, just a placeholder */
}


static int printer_zxp_open_file(void)
{
static const char *pbmstart="P4\n256 ";
FILE *tmpf;
int overwrite=1;

if(!printer_graphics_enabled || !settings_current.printer_graphics_filename)
  return(0);

/* first, see if there's an existing file we can add to. */
if((tmpf=fopen(settings_current.printer_graphics_filename,"rb"))!=NULL)
  {
  char buf[7+10+1];		/* 7 being length of pbmstart */

  /* check it has a header in our slightly odd format. */
  if(fread(buf,1,sizeof(buf),tmpf)==sizeof(buf) &&
     memcmp(buf,pbmstart,strlen(pbmstart))==0 &&
     buf[sizeof(buf)-1]=='\n')
    {
    char *ptr=buf+strlen(pbmstart);
    int f,want_space=1;
    
    /* looks good so far, but now we need to check that
     * the height field looks ok. It should be all-spaces
     * until we get a digit, then all-digits.
     */
    for(f=0;f<10;f++,ptr++)
      {
      /* make sure it's a space or digit */
      if(!strchr(" 0123456789",*ptr))
        break;
      
      if(want_space)
        {
        if(*ptr!=' ')
          want_space=0;
        }
      else
        if(!isdigit( (int)*ptr ))
          break;
      }

    /* if it got through that, reuse the file */
    if(f==10)
      {
      overwrite=0;
      zxpheight=atoi(buf+strlen(pbmstart));
      }
    }

  fclose(tmpf);
  }

if((printer_graphics_file=fopen(settings_current.printer_graphics_filename,
                                overwrite?"wb":"r+b"))==NULL)
  {
  ui_error(UI_ERROR_ERROR,"Couldn't open '%s', graphics printout disabled",
	   settings_current.printer_graphics_filename);
  printer_graphics_enabled=0;
  return(0);
  }

if(overwrite)
  {
  /* we reserve 10 chars for height */
  fprintf(printer_graphics_file,pbmstart);
  fprintf(printer_graphics_file,"%10d\n",0);
  }
else
  {
  /* if appending, seek to the correct place */
  if(fseek(printer_graphics_file,
           strlen(pbmstart)+10+1+(256/8)*zxpheight,
           SEEK_SET)!=0)
    {
    ui_error(UI_ERROR_ERROR,
	     "Couldn't seek on file, graphics printout disabled");
    fclose(printer_graphics_file);
    printer_graphics_file=NULL;
    printer_graphics_enabled=0;
    }
  }

return(1);
}


static int printer_text_open_file(void)
{
if(!printer_text_enabled || !settings_current.printer_text_filename)
  return(0);

/* append to any existing file... */
if((printer_text_file=fopen(settings_current.printer_text_filename,"a"))==NULL)
  {
  ui_error(UI_ERROR_ERROR,"Couldn't open '%s', text printout disabled",
	   settings_current.printer_text_filename);
  printer_text_enabled=0;
  return(0);
  }

return(1);
}


/* this is the output routine for the `proper' printers
 * (those connected via serial/parallel ports), and for
 * the text-parsing ZX Printer mode.
 */
static void printer_text_output_char(int c)
{
if(!printer_text_enabled)
  return;

if(!printer_text_file && !printer_text_open_file())
  return;

fputc(c,printer_text_file);
}


static void printer_zxp_update_header(void)
{
long pos;

if(!printer_graphics_enabled || !zxpheight) return;

if(!printer_graphics_file && !printer_zxp_open_file())
  return;

pos=ftell(printer_graphics_file);

/* seek back to write the image height */
if(fseek(printer_graphics_file,strlen("P4\n256 "),SEEK_SET)!=0)
  ui_error(UI_ERROR_ERROR,
	   "Couldn't seek to write graphics printout image height");
else
  {
  /* I originally had spaces after the image height, but that actually
   * breaks the format as defined in pbm(5) (not to mention breaking
   * when read by zgv :-)). So they're now before the height.
   */
  fprintf(printer_graphics_file,"%10d",zxpheight);
  }

if(fseek(printer_graphics_file,pos,SEEK_SET)!=0)
  {
  ui_error(UI_ERROR_ERROR,
	   "Couldn't re-seek on file, graphics printout disabled");
  fclose(printer_graphics_file);
  printer_graphics_file=NULL;
  printer_graphics_enabled=0;
  }
}


static void printer_zxp_end(void)
{
/* stop the printer */
printer_zxp_write(0xfb,4);

/* if not enabled or not opened, can't have written anything */
if(!printer_graphics_enabled || !printer_graphics_file || zxpheight==0)
  return;

/* write header */
printer_zxp_update_header();

fclose(printer_graphics_file);
printer_graphics_file=NULL;
printer_graphics_enabled=0;
}


static void printer_text_end(void)
{
if(!printer_text_enabled)
  return;

if(printer_text_file)
  {
  fclose(printer_text_file);
  printer_text_file=NULL;
  }
}


/* output the last line printed as text. */
static void printer_zxp_output_as_text(void)
{
static unsigned char charset[256*8];
static unsigned char outbuf[32];
unsigned char *ptr;
int x,y,f,c,chars;

#define SYSV_CHARS	0x5c36

chars=readbyte_internal(SYSV_CHARS);
chars+=256*readbyte_internal(SYSV_CHARS+1);

memset(charset,0,sizeof(charset));
ptr=charset+32*8;
for(f=32*8;f<128*8;f++)
  *ptr++=readbyte_internal(chars+f);

for(x=0;x<32;x++)
  {
  c=-1;
  
  /* try each char */
  for(f=32;f<128 && c==-1;f++)
    {
    ptr=zxplast8+x;
    c=f;
    for(y=0;y<8;y++,ptr+=32)
      if(*ptr!=charset[f*8+y])
        {
        c=-1;
        break;
        }
    }

  /* can't do UDGs, too unreliable */

  if(c==-1) c=32;
  
  outbuf[x]=c;
  }

for(f=31;f>=0 && outbuf[f]==32;f--)
  outbuf[f]=0;

for(f=0;f<32 && outbuf[f];f++)
  printer_text_output_char(outbuf[f]);
printer_text_output_char('\n');
}


/* output a pixel line */
static void printer_zxp_output_line(void)
{
unsigned char *ptr;
int i,j,d;

if(!printer_graphics_enabled) return;

if(!printer_graphics_file && !printer_zxp_open_file())
  return;

zxpheight++;
zxplineofchar++;

/* scroll up record of last char-line */
memmove(zxplast8,zxplast8+32,sizeof(zxplast8)-32);

ptr=zxplast8+sizeof(zxplast8)-32;
for(i=0;i<32;i++)
  {
  for(d=j=0;j<8;j++)
    {
    d<<=1;
    d|=(zxpline[i*8+j]?1:0);
    }
  
  *ptr++=d;
  
  fputc(d,printer_graphics_file);
  }

if(zxplineofchar>=8)
  {
  printer_zxp_output_as_text();
  zxplineofchar=0;
  }
}


/* currently just incrs a frame counter we need for ZX Printer.
 * can't fail, hence no return value.
 */
void printer_frame(void)
{
frames++;
}



/* ZX Printer support, transliterated from IMC's xz80 by a bear of
 * very little brain. :-) Or at least, I don't grok it that well.
 * It works wonderfully though.
 */
libspectrum_byte printer_zxp_read(libspectrum_word port GCC_UNUSED,
				  int *attached)
{
if(!settings_current.printer)
  return(0xff);
if(!printer_graphics_enabled)
  return(0xff);

*attached=1;

if(!zxpspeed)
  return 0x3e;
else
  {
  int frame=frames-zxpframes;
  int cycles=tstates-zxpcycles;
  int pix=zxppixel;
  int sp=zxpnewspeed;
  int x,ans;
  int cpp=440/zxpspeed;
      
  if(frame>400)
    frame=400;
  cycles+=frame*machine_current->timings.tstates_per_frame;
  x=cycles/cpp-64;        /* x-coordinate reached */
      
  while(x>320)
    {           /* if we are on another line, */
    pix=-1;              /* find out where we are */
    x-=384;
    if(sp)
      {
      x=(x+64)*cpp;
      cpp=440/sp;
      x=x/cpp-64;
      sp=0;
      }
    }
  if((x>-10 && x<0) | zxpstylus)
    ans=0xbe;
  else
    ans=0x3e;
  if(x>pix)
    ans|=1;
  return ans;
  }
}


void printer_zxp_write(libspectrum_word port GCC_UNUSED,libspectrum_byte b)
{
if(!settings_current.printer)
  return;
if(!zxpspeed)
  {
  if(!(b&4))
    {
    zxpspeed=(b&2)?1:2;
    zxpframes=frames;
    zxpcycles=tstates;
    zxpstylus=b&128;
    zxppixel=-1;
    zxplineofchar=0;
    }
  }
else
  {
  int frame=frames-zxpframes;
  int cycles=tstates-zxpcycles;
  int i,x;
  int cpp=440/zxpspeed;
      
  if(frame>400)
    frame=400; /* limit height of blank paper */
  cycles+=frame*machine_current->timings.tstates_per_frame;
  x=cycles/cpp-64;        /* x-coordinate reached */
  for(i=zxppixel;i<x && i<256;i++)
    if(i>=0)		/* should be, but just in case */
      zxpline[i]=zxpstylus;
  if(x>=256 && zxppixel<256)
    printer_zxp_output_line();
      
  while(x>=320)
    {          /* move to next line */
    zxpcycles+=cpp*384;
    if(zxpcycles>=machine_current->timings.tstates_per_frame)
      zxpcycles-=machine_current->timings.tstates_per_frame,zxpframes++;
    x-=384;
    if(zxpnewspeed)
      {
      zxpspeed=zxpnewspeed;
      zxpnewspeed=0;
      x=(x+64)*cpp;
      cpp=440/zxpspeed;
      x=x/cpp-64;
      }
    for(i=0;i<x && i<256;i++)
      zxpline[i]=zxpstylus;
    if(x>=256)
      printer_zxp_output_line();
    }
  if(x<0)
    x=-1;
  if(b&4)
    {
    if(x>=0 && x<256)
      {
      for(i=x;i<256;i++)
        zxpline[i]=zxpstylus;
      printer_zxp_output_line();
      }
    zxpspeed=zxpstylus=0;

    /* this marks the end of a char line or COPY */
    zxplineofchar=0;
    
    /* this is pretty frequent (on a per-char-line basis!),
     * but it's the only time we can really do it automagically.
     */
    printer_zxp_update_header();
    }
  else
    {
    zxppixel=x;
    zxpstylus=b&128;
    if(x<0)
      zxpspeed=(b&2)?1:2;
    else
      {
      zxpnewspeed=(b&2)?1:2;
      if(zxpnewspeed==zxpspeed)
        zxpnewspeed=0;
      }
    }
  } 
}


void printer_zxp_reset(void)
{
/* stop printer - XXX not sure if the real one does this on reset */
printer_zxp_write(0xfb,4);

zxplineofchar=0;
}


/* called when writing to the AY port which deals with RS232 output.
 * Assuming that the port gets written for every bit sent (which seems
 * likely; certainly the ROMs do it) means we can use a
 * bps-independent approach.
 */
void printer_serial_write(libspectrum_byte b)
{
static int reading=0,bits_to_get=0,ser_byte=0;
int high=(b&8);
if(!settings_current.printer)
  return;
if(!reading)
  {
  if(!high)
    {
    bits_to_get=9;
    reading=1;
    }
  }
else /* reading */
  {
  if(bits_to_get)
    {
    ser_byte>>=1;
    ser_byte|=(high?0x100:0);
    bits_to_get--;
    if(!bits_to_get)
      {
      if(ser_byte&0x100)	/* check stop bit is valid */
        printer_text_output_char(ser_byte&0xff);
      reading=0;
      }
    }
  }
}


/* The +2A/+3 ROM writes the strobe the wrong way, which doesn't work on
 * some printers. But some programs (notably Ian Collier's 48K Disk BASIC)
 * write it the *right* way. This means we can't treat either edge *alone*
 * as a signal to print, otherwise we'll print a junk byte when 48KDB
 * starts up. Realistically, we have to look for two close together.
 *
 * The Centronics spec seems to require that the strobe `goes low' for
 * a minimum of 1us to give the printer time to read the data. But that's
 * not even a NOP's worth on the speccy, so we need to allow for longer;
 * 10000 cycles seems reasonable, being thousands of times longer. :-)
 * (And the longest delay I've seen was just over 5000 cycles.)
 */
void printer_parallel_strobe_write(int on)
{
static int old_on=0;
static int second_edge=0;
static unsigned int last_frames=0;
static libspectrum_dword last_tstates=0;
static unsigned char last_data=0;
libspectrum_dword diff;

if(!settings_current.printer)
  return;
if((old_on && !on) || (!old_on && on))
  {
  /* got an edge */

  if(!second_edge)
    {
    /* the ROM also seems to assume the printer is reading at the first
     * edge, breaking the spec (sigh). So we need to save current
     * data or we lose CRs in LPRINT/LLIST.
     */
    last_data=parallel_data;
    second_edge=1;
    }
  else
    {
    second_edge=0;
    diff=tstates;
    if(frames!=last_frames)
      diff+=machine_current->timings.tstates_per_frame;
    diff-=last_tstates;

    if(diff<=PARALLEL_STROBE_MAX_CYCLES)
      printer_text_output_char(last_data);
    else
      {
      /* too long ago, treat it as first edge */
      last_data=parallel_data;
      second_edge=1;
      }
    }

  last_frames=frames;
  last_tstates=tstates;
  }

old_on=on;
}


libspectrum_byte printer_parallel_read(libspectrum_word port GCC_UNUSED,
				       int *attached)
{
if(!settings_current.printer)
  return(0xff);

*attached = 1;

/* bit 0 = busy. other bits high? */

return(0xfe);	/* never busy */
}


void printer_parallel_write(libspectrum_word port GCC_UNUSED,
			    libspectrum_byte b)
{
if(!settings_current.printer)
  return;
parallel_data=b;
}


int printer_init(void)
{
printer_graphics_enabled=printer_text_enabled=1;
printer_graphics_file=printer_text_file=NULL;

printer_zxp_init();
printer_text_init();
return(0);
}


void printer_end(void)
{
printer_text_end();
printer_zxp_end();
}
