/** 
 * Copyright (C) 2000-2002 the KGhostView authors. See file AUTHORS.
 * 	
 * 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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

/*

Ghostview interface to ghostscript

When the GHOSTVIEW environment variable is set, ghostscript draws on
an existing drawable rather than creating its own window.  Ghostscript
can be directed to draw on either a window or a pixmap.

Drawing on a Window

The GHOSTVIEW environment variable contains the window id of the target
window.  The window id is an integer.  Ghostscript will use the attributes
of the window to obtain the width, height, colormap, screen, and visual of
the window. The remainder of the information is gotten from the GHOSTVIEW
property on that window.


Drawing on a Pixmap

The GHOSTVIEW environment variable contains a window id and a pixmap id.
They are integers separated by white space.  Ghostscript will use the
attributes of the window to obtain the colormap, screen, and visual to use.
The width and height will be obtained from the pixmap. The remainder of the
information, is gotten from the GHOSTVIEW property on the window.  In this
case, the property is deleted when read.

The GHOSTVIEW environment variable

parameters:	window-id [pixmap-id]

scanf format:	"%d %d"

explanation of parameters:

	window-id: tells ghostscript where to
		    - read the GHOSTVIEW property
		    - send events
		    If pixmap-id is not present,
		    ghostscript will draw on this window.

	pixmap-id: If present, tells ghostscript that a pixmap will be used
		    as the final destination for drawing.  The window will
		    not be touched for drawing purposes.

The GHOSTVIEW property

type:	STRING

parameters:

    bpixmap orient llx lly urx ury xdpi ydpi [left bottom top right]

scanf format: "%d %d %d %d %d %d %f %f %d %d %d %d"

explanation of parameters:

	bpixmap: pixmap id of the backing pixmap for the window.  If no
		pixmap is to be used, this parameter should be zero.  This
		parameter must be zero when drawing on a pixmap.

	orient:	orientation of the page.  The number represents clockwise
		rotation of the paper in degrees.  Permitted values are
		0, 90, 180, 270.

	llx, lly, urx, ury: Bounding box of the drawable.  The bounding box
		is specified in PostScript points in default user coordinates.
		Wilco: Note the word *drawable*. This means that this bounding 
		       box is generally not the same as the BoundingBox 
		       specified in the DSC. In case DocumentMedia is 
		       specified, it is equal to the Media's bounding box.

	xdpi, ydpi: Resolution of window.  (This can be derived from the
		other parameters, but not without roundoff error.  These
		values are included to avoid this error.)

	left, bottom, top, right: (optional)
		Margins around the window.  The margins extend the imageable
		area beyond the boundaries of the window.  This is primarily
		used for popup zoom windows.  I have encountered several
		instances of PostScript programs that position themselves
		with respect to the imageable area.  The margins are specified
		in PostScript points.  If omitted, the margins are assumed to
		be 0.

Events from ghostscript

If the final destination is a pixmap, the client will get a property notify
event when ghostscript reads the GHOSTVIEW property causing it to be deleted.

Ghostscript sends events to the window where it read the GHOSTVIEW property.
These events are of type ClientMessage.  The message_type is set to
either PAGE or DONE.  The first long data value gives the window to be used
to send replies to ghostscript.  The second long data value gives the primary
drawable.  If rendering to a pixmap, it is the primary drawable.  If rendering
to a window, the backing pixmap is the primary drawable.  If no backing pixmap
is employed, then the window is the primary drawable.  This field is necessary
to distinguish multiple ghostscripts rendering to separate pixmaps where the
GHOSTVIEW property was placed on the same window.

The PAGE message indicates that a "page" has completed.  Ghostscript will
wait until it receives a ClientMessage whose message_type is NEXT before
continuing.

The DONE message indicates that ghostscript has finished processing.

*/

#include <stdlib.h>

#include <qdrawutil.h>
#include <qpaintdevicemetrics.h>
#include <qstringlist.h>

#include <kapplication.h>
#include <kdebug.h>
#include <klocale.h>
#include <kmessagebox.h>
#include <kprocess.h>

#include "kgvconfigdialog.h"
#include "kpswidget.h"
#include "messages.h"

#include "kpswidget.moc"

#include <X11/Xlib.h>
#include <X11/Xatom.h>

#define BUFFER_SIZE (8192)

int handler( Display* d, XErrorEvent* e )
{
    char msg[80], req[80], number[80];

    XGetErrorText( d, e->error_code, msg, sizeof( msg ) );
    sprintf( number, "%d", e->request_code );
    XGetErrorDatabaseText( d, "XRequest", number, "<unknown>", 
                           req, sizeof( req ) );
    return 0;
}

KPSWidget::KPSWidget( QWidget* parent, const char* name )
 : QWidget( parent, name )
{
    _gsWindow = None;
    _process = 0;

    XSetErrorHandler( handler );

    // Get the screen resolution.
    QPaintDeviceMetrics qpdm( this );
    _logicalDpiX = qpdm.logicalDpiX();
    _logicalDpiY = qpdm.logicalDpiY();

    _orientation = CDSC_PORTRAIT;
    _dpiX = _logicalDpiX; 
    _dpiY = _logicalDpiY;
    _palette = COLOR;

    _widgetDirty = true;

    messages  = new MessagesDialog( 0, "messages" );
    intConfig = new KGVConfigDialog( topLevelWidget(), "intConfig" );

    _inputBuffer = 0;
    _currentRecord = 0;
    _bytesLeft = 0;
    // _inputQueue.setAutoDelete( true );

    _interpreterBusy	 = false;
    _stdinReady		 = false;
    _interpreterReady	 = false;
    _interpreterDisabled = false;

#define GSATOM(name) (Atom) XInternAtom( x11Display(), name, false );
    _Atoms.GHOSTVIEW        = GSATOM( "GHOSTVIEW" );
    _Atoms.GHOSTVIEW_COLORS = GSATOM( "GHOSTVIEW_COLORS" );
    _Atoms.NEXT             = GSATOM( "NEXT" );
    _Atoms.PAGE             = GSATOM( "PAGE" );
    _Atoms.DONE             = GSATOM( "DONE" );

    readSettings();
    setupWidget();
}

KPSWidget::~KPSWidget()
{
    stopInterpreter();
    
    delete intConfig;
    delete messages;
}

void KPSWidget::enableInterpreter()
{
    _interpreterDisabled = false;
    startInterpreter();
}

void KPSWidget::disableInterpreter()
{
    _interpreterDisabled = true;
    stopInterpreter();
}

bool KPSWidget::isInterpreterReady() const
{
    return isInterpreterRunning() && _interpreterReady;
}

bool KPSWidget::isInterpreterBusy() const
{
    return _interpreterBusy;
}

bool KPSWidget::isInterpreterRunning() const
{
    return ( _process && _process->isRunning() );
}

bool KPSWidget::nextPage()
{
    if( !isInterpreterRunning() )
	return false;

    if( _gsWindow == None ) {
	kdDebug(4500) << "kghostview: communication window unknown!" << endl;
	return false;
    }

    if( _interpreterReady ) 
    {
	_interpreterReady = false;
	
	_interpreterBusy = true;
	setCursor( waitCursor );

	XEvent e;
	e.xclient.type = ClientMessage;
	e.xclient.display = x11Display();
	e.xclient.window = _gsWindow;
	e.xclient.message_type = _Atoms.NEXT;
	e.xclient.format = 32;

	XSendEvent( x11Display(), _gsWindow, false, 0, &e );
	XFlush( x11Display() );

	return true;
    }
    else
	return false;
}

bool KPSWidget::sendPS( FILE* fp, unsigned int begin, unsigned int end )
{
    kdDebug(4500) << "KPSWidget: sendPS" << endl;
    
    if( !isInterpreterRunning() )
	return false;

    // Create a new record to add to the queue.
    Record* ps_new = new Record;
    ps_new->fp = fp;
    ps_new->begin = begin;
    ps_new->len = end - begin;

    _inputQueue.enqueue( ps_new );

    // Start processing the queue.
    if( _stdinReady )
	gs_input();

    return true;
}

void KPSWidget::setGhostscriptPath( const QString& path )
{
    if( _ghostscriptPath != path )
    {
	_ghostscriptPath = path;
	stopInterpreter();
	_ghostscriptDirty = true;
    }
}

void KPSWidget::setGhostscriptArguments( const QStringList& arguments )
{
    if( _ghostscriptArguments != arguments )
    {
	_ghostscriptArguments = arguments;
	stopInterpreter();
	_ghostscriptDirty = true;
    }
}

void KPSWidget::setFileName( const QString& fileName )
{
    if( _fileName != fileName )
    {
	_fileName = fileName;
	stopInterpreter();
	_ghostscriptDirty = true;
    }
}

void KPSWidget::setOrientation( CDSC_ORIENTATION_ENUM orientation )
{   
    if( _orientation != orientation ) 
    {
	_orientation = orientation;
	stopInterpreter();
	_widgetDirty = true;
    }
}

void KPSWidget::setBoundingBox( KDSCBBOX boundingBox )
{
    if( _boundingBox != boundingBox ) 
    {
	_boundingBox = boundingBox;
	stopInterpreter();
	_widgetDirty = true;
    }
}

void KPSWidget::setResolution( int dpiX, int dpiY )
{
    if( _dpiX != dpiX || _dpiY != dpiY ) 
    {
	_dpiX = dpiX; _dpiY = dpiY;
	stopInterpreter();
	_widgetDirty = true;
    }
}

void KPSWidget::setMagnification( double magnification )
{
    setResolution( static_cast<int>( logicalDpiX() * magnification ), 
                   static_cast<int>( logicalDpiY() * magnification ) );
}

void KPSWidget::setPalette( Palette palette )
{    
    if( _palette != palette ) 
    {
	_palette = palette;
	stopInterpreter();
	_widgetDirty = true;
    }
}

int KPSWidget::orientation2angle( CDSC_ORIENTATION_ENUM orientation )
{
    Q_ASSERT( orientation != CDSC_ORIENT_UNKNOWN );

    int angle = 0;

    switch( orientation ) 
    {
    case CDSC_ORIENT_UNKNOWN:		    break; // Catched by Q_ASSERT
    case CDSC_PORTRAIT:	    angle = 0;	    break;
    case CDSC_LANDSCAPE:    angle = 90;	    break;
    case CDSC_UPSIDEDOWN:   angle = 180;    break;
    case CDSC_SEASCAPE:	    angle = 270;    break;
    }

    return angle;
}

QCString KPSWidget::palette2String( Palette palette )
{
    QCString str;
    
    switch( palette )
    {
    case COLOR:      str = "Color";      break;
    case GRAYSCALE:  str = "Grayscale";  break;
    case MONOCHROME: str = "Monochrome"; break;
    }

    return str;
}

bool KPSWidget::configure()
{
    intConfig->setup();
    if (intConfig->exec())
    {
	readSettings();
	setupWidget();
	return true;
    }

    return false;
}

void KPSWidget::setupWidget()
{
    if( !_widgetDirty )
	return;
    
    kdDebug(4500) << "KPSWidget::setupWidget()" << endl;
    
    Q_ASSERT( orientation() != CDSC_ORIENT_UNKNOWN );

    int newWidth = 0, newHeight = 0;
    switch( orientation() ) 
    {
    case CDSC_ORIENT_UNKNOWN:
	break;
    case CDSC_PORTRAIT:
	newWidth  = (int) ( boundingBox().width()  / 72.0 * _dpiX + 0.5 );
	newHeight = (int) ( boundingBox().height() / 72.0 * _dpiY + 0.5 );
	break;
    case CDSC_LANDSCAPE:
	newWidth  = (int) ( boundingBox().height() / 72.0 * _dpiX + 0.5 );
	newHeight = (int) ( boundingBox().width()  / 72.0 * _dpiY + 0.5 );
	break;
    case CDSC_UPSIDEDOWN:
	newWidth  = (int) ( boundingBox().width()  / 72.0 * _dpiX + 0.5 );
	newHeight = (int) ( boundingBox().height() / 72.0 * _dpiY + 0.5 );
	break;
    case CDSC_SEASCAPE:
	newWidth  = (int) ( boundingBox().height() / 72.0 * _dpiX + 0.5 );
	newHeight = (int) ( boundingBox().width()  / 72.0 * _dpiY + 0.5 );
	break;
    }
    bool sizeChanged = ( newWidth != width() || newHeight != height() );

    // stopInterpreter();
    
    if( sizeChanged ) 
    {
	_backgroundPixmap.fill( white );
	setErasePixmap( _backgroundPixmap );

	setFixedSize( newWidth, newHeight );
	kapp->processEvents();

	_backgroundPixmap.resize( size() );
	_backgroundPixmap.fill( white );
	setErasePixmap( _backgroundPixmap );
    }

    char buf[512];
    
    sprintf( buf, "%ld %d %d %d %d %d %g %g",
             _backgroundPixmap.handle(),
             orientation2angle( orientation() ),
             boundingBox().llx(), boundingBox().lly(), 
	     boundingBox().urx(), boundingBox().ury(),
             _dpiX, _dpiY );
    XChangeProperty( x11Display(), winId(),
                     _Atoms.GHOSTVIEW,
                     XA_STRING, 8, PropModeReplace,
                     (unsigned char*)buf, strlen( buf ) );

    sprintf( buf, "%s %d %d",
             palette2String( _palette ).data(),
             (int)BlackPixel( x11Display(), DefaultScreen( x11Display() ) ),
             (int)WhitePixel( x11Display(), DefaultScreen( x11Display() ) ) );
    XChangeProperty( x11Display(), winId(),
                     _Atoms.GHOSTVIEW_COLORS,
                     XA_STRING, 8, PropModeReplace,
                     (unsigned char*)buf, strlen( buf ) );

    // Make sure the properties are updated immediately.
    XSync( x11Display(), false );

    repaint();
    
    _widgetDirty = false;
}

void KPSWidget::startInterpreter()
{
    // stopInterpreter();

    // erase();

    if( _interpreterDisabled )
	return;

    setupWidget();
    
    _process = new KProcess;
    _process->setEnvironment( "GHOSTVIEW", QString::number( winId() ) );
    
    *_process << _ghostscriptPath.local8Bit();
    
    QStringList::Iterator it = _ghostscriptArguments.begin();
    for( ; it != _ghostscriptArguments.end(); ++it )
	*_process << (*it);
    
    if( _fileName.isEmpty() )
	*_process << "-";
    else
	*_process << _fileName << "-c" << "quit";

    connect( _process, SIGNAL( processExited( KProcess* ) ),
             this, SLOT( slotProcessExited( KProcess* ) ) );
    connect( _process, SIGNAL( receivedStdout( KProcess*, char*, int ) ),
             this, SLOT( gs_output( KProcess*, char*, int ) ) );
    connect( _process, SIGNAL( receivedStderr( KProcess*, char*, int ) ),
             this, SLOT( gs_output( KProcess*, char*, int ) ) );
    connect( _process, SIGNAL( wroteStdin( KProcess*) ),
             this, SLOT( gs_input() ) );

    kapp->flushX();

    // Finally fire up the interpreter.
    kdDebug(4500) << "KPSWidget: starting interpreter" << endl;
    if( _process->start( KProcess::NotifyOnExit, 
              _fileName.isEmpty() ? KProcess::All : KProcess::AllOutput ) ) 
    {
	_interpreterBusy = true;
	setCursor( waitCursor );
    
	_stdinReady = true;
	_interpreterReady = false;
	_ghostscriptDirty = false;
    }
    else 
	KMessageBox::error( this, 
	        i18n( "Could not start Ghostscript. This is most likely "
		      "caused by an incorrectly specified interpreter." ) );
}

void KPSWidget::stopInterpreter()
{
    // if( !_interpreterBusy ) return;

    if( isInterpreterRunning() )
	_process->kill( SIGHUP );

    _process = 0;

    if( _currentRecord != 0 )
    {
	delete _currentRecord;
	_currentRecord = 0;
	_bytesLeft = 0;
    }
    _inputQueue.setAutoDelete( true );
    _inputQueue.clear();
    _inputQueue.setAutoDelete( false );
    
    _interpreterBusy = false;
    unsetCursor();
}

void KPSWidget::interpreterFailed()
{
    stopInterpreter();
}

void KPSWidget::slotProcessExited( KProcess* process )
{
    kdDebug(4500) << "KPSWidget: process exited" << endl;
    
    delete process;
}

/**
 * Output - receive I/O from ghostscript's stdout and stderr.
 * Pass this to the application via the output_callback.
 */
void KPSWidget::gs_output( KProcess*, char* buffer, int len )
{
    kdDebug(4500) << "KPSWidget: received output" << endl;

    QString line = QString::fromLocal8Bit( buffer, len );

    if( !line.isEmpty() && _showMessages )
    {
	messages->show();
	messages->cancel->setFocus();
	messages->messageBox->append( line );
   }
}

void KPSWidget::gs_input()
{
    kdDebug(4500) << "KPSWidget: gs_input" << endl;
    
    _stdinReady = true;

    // Get a Record with size greater than 0 from the queue 
    while( _bytesLeft == 0 && !_inputQueue.isEmpty() )
    {
	if( _currentRecord != 0 ) 
	    delete _currentRecord;
	_currentRecord = _inputQueue.dequeue();
	_bytesLeft = _currentRecord->len;
	fseek( _currentRecord->fp, _currentRecord->begin, SEEK_SET );
    }
	    
    if( _bytesLeft > 0 )
    {
	if( _inputBuffer == 0 )
	    _inputBuffer = (char *) malloc(BUFFER_SIZE);

	int bytesRead = fread( _inputBuffer, sizeof (char), 
	                       QMIN( BUFFER_SIZE, _bytesLeft ), 
			       _currentRecord->fp );
	if( bytesRead > 0 ) 
	{
	    _bytesLeft -= bytesRead;
	    if( _process->writeStdin( _inputBuffer, bytesRead ) )
		_stdinReady = false;
	    else
		interpreterFailed();
	}
	else
	    interpreterFailed();
    }
    else 
	_interpreterReady = true;
}

void KPSWidget::readSettings()
{
    setGhostscriptPath( intConfig->interpreterPath() );
    
    QStringList arguments;
    
    if( intConfig->antiAlias() )
	arguments = QStringList::split( " ", intConfig->antialiasArgs() );
    else
	arguments = QStringList::split( " ", intConfig->nonAntialiasArgs() );
    
    if( !intConfig->platformFonts() )
	arguments << "-dNOPLATFONTS";
    
    arguments << "-dNOPAUSE" << "-dQUIET" << "-dSAFER";

    setGhostscriptArguments( arguments );

    _showMessages = intConfig->showMessages();

    switch( intConfig->paletteType() )
    {
    case KGVConfigDialog::COLOR_PALETTE: setPalette( COLOR );      break;
    case KGVConfigDialog::GRAY_PALETTE:  setPalette( GRAYSCALE );  break;
    case KGVConfigDialog::MONO_PALETTE:  setPalette( MONOCHROME ); break;
    }
}

bool KPSWidget::x11Event( XEvent* e )
{
    if( e->type == ClientMessage )
    {
	_gsWindow = e->xclient.data.l[0];
	
	if( e->xclient.message_type == _Atoms.PAGE )
	{
	    kdDebug(4500) << "KPSWidget: received PAGE" << endl;
	    _interpreterBusy = false;
	    unsetCursor();
	    emit pageFinished();
	    return true;
	}
	else if( e->xclient.message_type == _Atoms.DONE )
	{
	    kdDebug(4500) << "KPSWidget: received DONE" << endl;
	    disableInterpreter();
	    return true;
	}
    }
    return QWidget::x11Event( e );
}
