/* load ppm from a file
 *
 * Stephen Chan ... original code
 *
 * 21/11/00 JC
 *	- hacked for VIPS
 *	- reads ppm/pgm/pbm
 *	- mmaps binary pgm/ppm
 *	- reads all ascii formats (slowly!)
 * 22/11/00 JC
 *	- oops, ascii read was broken
 *	- does 16/32 bit ascii now as well
 * 24/5/01
 *	- im_ppm2vips_header() added
 * 28/11/03 JC
 *	- better no-overshoot on tile loop
 * 22/5/04
 *	- does 16/32 bit binary too
 *	- tiny fix for missing file close on read error
 * 19/8/05
 * 	- use im_raw2vips() for binary read
 * 9/9/05
 * 	- tiny cleanups
 * 3/11/07
 * 	- use im_wbuffer() for bg writes
 * 1/5/10
 * 	- add PFM (portable float map) support
 * 19/12/11
 * 	- rework as a set of fns ready to be called from a class
 * 8/11/14
 * 	- add 1 bit write
 * 29/7/19 Kyle-Kyle
 * 	- fix a loop with malformed ppm
 * 13/11/19
 * 	- redone with source/target
 * 	- sequential load, plus mmap for filename sources
 * 	- faster plus lower memory use
 * 02/02/2020
 * 	- ban max_vaue < 0 
 */

/*

    This file is part of VIPS.
    
    VIPS is free software; you can redistribute it and/or modify
    it under the terms of the GNU Lesser 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 Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
    02110-1301  USA

 */

/*

    These files are distributed with VIPS - http://www.vips.ecs.soton.ac.uk

 */

/*
#define DEBUG
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif /*HAVE_CONFIG_H*/
#include <vips/intl.h>

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

#include <vips/vips.h>
#include <vips/internal.h>

#include "pforeign.h"

#ifdef HAVE_PPM

typedef struct _VipsForeignLoadPpm {
	VipsForeignLoad parent_object;

	/* The source we load from, and the buffered wrapper for it.
	 */
	VipsSource *source;
	VipsSbuf *sbuf;

	/* Properties of this ppm, from the header.
	 */
	int width;
	int height;
	int bands;
	VipsBandFormat format;
	VipsInterpretation interpretation;
	float scale;
	int max_value;
	int index;		/* ppm type .. index in magic_names[] */
	int bits;		/* 1, 8, 16 or 32 */
	gboolean ascii;		/* TRUE for ascii encoding */
	gboolean msb_first;	/* TRUE if most sig byte is first */

	gboolean have_read_header;

} VipsForeignLoadPpm;

typedef VipsForeignLoadClass VipsForeignLoadPpmClass;

G_DEFINE_ABSTRACT_TYPE( VipsForeignLoadPpm, vips_foreign_load_ppm, 
	VIPS_TYPE_FOREIGN_LOAD );

/* ppm types.
 */
static char *magic_names[] = {
	"P1",	/* pbm ... 1 band 1 bit, ascii */
	"P2",	/* pgm ... 1 band many bit, ascii */
	"P3",	/* ppm ... 3 band many bit, ascii */
	"P4",	/* pbm ... 1 band 1 bit, binary */
	"P5",	/* pgm ... 1 band 8 bit, binary */
	"P6",	/* ppm ... 3 band 8 bit, binary */
	"PF",	/* pfm ... 3 band 32 bit, binary */
	"Pf"	/* pfm ... 1 band 32 bit, binary */
};

/* Shared with ppmsave.
 */
const char *vips__ppm_suffs[] = { ".ppm", ".pgm", ".pbm", ".pfm", NULL };

static gboolean
vips_foreign_load_ppm_is_a_source( VipsSource *source )
{
	const unsigned char *data;

	if( (data = vips_source_sniff( source, 2 )) ) { 
		int i;

		for( i = 0; i < VIPS_NUMBER( magic_names ); i++ )
			if( vips_isprefix( magic_names[i], (char *) data ) )
				return( TRUE );
	}

	return( FALSE );
}

static int
get_int( VipsSbuf *sbuf, int *i )
{
	const char *txt;

	if( vips_sbuf_skip_whitespace( sbuf ) ||
		!(txt = vips_sbuf_get_non_whitespace( sbuf )) )
		return( -1 );

	*i = atoi( txt ); 

	return( 0 );
}

static int
get_float( VipsSbuf *sbuf, float *f )
{
	const char *txt;

	if( vips_sbuf_skip_whitespace( sbuf ) ||
		!(txt = vips_sbuf_get_non_whitespace( sbuf )) )
		return( -1 );

	/* We don't want the locale str -> float conversion.
	 */
	*f = g_ascii_strtod( txt, NULL );

	return( 0 );
}

static void
vips_foreign_load_ppm_dispose( GObject *gobject )
{
	VipsForeignLoadPpm *ppm = (VipsForeignLoadPpm *) gobject;

	VIPS_UNREF( ppm->sbuf );
	VIPS_UNREF( ppm->source );

	G_OBJECT_CLASS( vips_foreign_load_ppm_parent_class )->
		dispose( gobject );
}

/* Scan the header into our class.
 */
static int
vips_foreign_load_ppm_parse_header( VipsForeignLoadPpm *ppm )
{
	VipsObjectClass *class = VIPS_OBJECT_GET_CLASS( ppm );

	int i;
	char buf[2];

	/* Characteristics, indexed by ppm type.
	 */
	static int lookup_bits[] = {
		1, 8, 8, 1, 8, 8, 32, 32
	};
	static int lookup_bands[] = {
		1, 1, 3, 1, 1, 3, 3, 1
	};
	static int lookup_ascii[] = {
		1, 1, 1, 0, 0, 0, 0, 0
	};

	if( vips_source_rewind( ppm->source ) )
		return( -1 );

	/* Read in the magic number.
	 */
	buf[0] = VIPS_SBUF_GETC( ppm->sbuf );
	buf[1] = VIPS_SBUF_GETC( ppm->sbuf );

	for( i = 0; i < VIPS_NUMBER( magic_names ); i++ )
		if( vips_isprefix( magic_names[i], buf ) ) 
			break;
	if( i == VIPS_NUMBER( magic_names ) ) {
		vips_error( class->nickname, "%s", _( "bad magic number" ) );
		return( -1 );
	}
	ppm->index = i;
	ppm->bits = lookup_bits[i];
	ppm->bands = lookup_bands[i];
	ppm->ascii = lookup_ascii[i];

	/* Default ... can be changed below for PFM images.
	 */
	ppm->msb_first = 0;

	/* Read in size.
	 */
	if( get_int( ppm->sbuf, &ppm->width ) ||
		get_int( ppm->sbuf, &ppm->height ) )
		return( -1 );

	/* Read in max value / scale for >1 bit images.
	 */
	if( ppm->bits > 1 ) {
		if( ppm->index == 6 || 
			ppm->index == 7 ) {
			if( get_float( ppm->sbuf, &ppm->scale ) )
				return( -1 );

			/* Scale > 0 means big-endian.
			 */
			ppm->msb_first = ppm->scale > 0;
		}
		else {
			if( get_int( ppm->sbuf, &ppm->max_value ) )
				return( -1 );

			/* max_value must be > 0 and <= 65535, according to
			 * the spec, but we allow up to 32 bits per pixel.
			 */
			if( ppm->max_value < 0 )
				ppm->max_value = 0;

			if( ppm->max_value > 255 )
				ppm->bits = 16;
			if( ppm->max_value > 65535 )
				ppm->bits = 32;
		}
	}

	/* For binary images, there is always exactly 1 more whitespace
	 * character before the data starts.
	 */
	if( !ppm->ascii && 
		!isspace( VIPS_SBUF_GETC( ppm->sbuf ) ) ) {
		vips_error( class->nickname, "%s", 
			_( "no whitespace before start of binary data" ) );
		return( -1 );
	}

	/* Choose a VIPS bandfmt.
	 */
	switch( ppm->bits ) {
	case 1:
	case 8:
		ppm->format = VIPS_FORMAT_UCHAR;
		break;

	case 16:
		ppm->format = VIPS_FORMAT_USHORT;
		break;

	case 32:
		if( ppm->index == 6 || 
			ppm->index == 7 )
			ppm->format = VIPS_FORMAT_FLOAT;
		else
			ppm->format = VIPS_FORMAT_UINT;
		break;

	default:
		g_assert_not_reached();

		/* Stop compiler warnings.
		 */
		ppm->format = VIPS_FORMAT_UCHAR;
	}

	if( ppm->bands == 1 ) {
		if( ppm->format == VIPS_FORMAT_USHORT )
			ppm->interpretation = VIPS_INTERPRETATION_GREY16;
		else
			ppm->interpretation = VIPS_INTERPRETATION_B_W;
	}
	else {
		if( ppm->format == VIPS_FORMAT_USHORT )
			ppm->interpretation = VIPS_INTERPRETATION_RGB16;
		else 
			ppm->interpretation = VIPS_INTERPRETATION_sRGB;
	}

	ppm->have_read_header = TRUE;

	return( 0 );
}

static VipsForeignFlags
vips_foreign_load_ppm_get_flags( VipsForeignLoad *load )
{
	VipsForeignLoadPpm *ppm = (VipsForeignLoadPpm *) load;

	VipsForeignFlags flags;

	flags = 0;

	/* If this source supports fast mmap and this PPM is >=8 bit binary,
	 * then we can mmap the file and support partial load. Otherwise,
	 * it's sequential.
	 */
	if( !ppm->have_read_header &&
		vips_foreign_load_ppm_parse_header( ppm ) )
		return( 0 );
	if( vips_source_is_mappable( ppm->source ) &&
		!ppm->ascii && 
		ppm->bits >= 8 )
		flags |= VIPS_FOREIGN_PARTIAL;
	else
		flags |= VIPS_FOREIGN_SEQUENTIAL;

	return( flags );
}

static void
vips_foreign_load_ppm_set_image( VipsForeignLoadPpm *ppm, VipsImage *image )
{
	vips_image_init_fields( image,
		ppm->width, ppm->height, ppm->bands, ppm->format, 
		VIPS_CODING_NONE, ppm->interpretation, 1.0, 1.0 );

        vips_image_pipelinev( image, VIPS_DEMAND_STYLE_THINSTRIP, NULL );

	if( ppm->index == 6 || 
		ppm->index == 7 ) 
		vips_image_set_double( image, 
			"pfm-scale", VIPS_FABS( ppm->scale ) );
	else
		vips_image_set_double( image, 
			"ppm-max-value", VIPS_ABS( ppm->max_value ) );

	VIPS_SETSTR( image->filename, 
		vips_connection_filename( VIPS_CONNECTION( ppm->sbuf->source ) ) );
}

static int
vips_foreign_load_ppm_header( VipsForeignLoad *load )
{
	VipsForeignLoadPpm *ppm = (VipsForeignLoadPpm *) load;

	if( !ppm->have_read_header &&
		vips_foreign_load_ppm_parse_header( ppm ) )
		return( 0 );

	vips_foreign_load_ppm_set_image( ppm, load->out );

	vips_source_minimise( ppm->source );

	return( 0 );
}

/* Read a ppm/pgm file using mmap().
 */
static int
vips_foreign_load_ppm_map( VipsForeignLoadPpm *ppm, VipsImage *image )
{
	VipsImage **t = (VipsImage **)
                vips_object_local_array( VIPS_OBJECT( ppm ), 3 );

	gint64 header_offset;
	size_t length;
	const void *data;

	vips_sbuf_unbuffer( ppm->sbuf );
	header_offset = vips_source_seek( ppm->source, 0, SEEK_CUR );
	data = vips_source_map( ppm->source, &length );
	if( header_offset < 0 || 
		!data )
		return( -1 );
	data += header_offset;
       	length -= header_offset;

	if( !(t[0] = vips_image_new_from_memory( data, length,
		ppm->width, ppm->height, ppm->bands, ppm->format )) )
		return( -1 );

	if( vips__byteswap_bool( t[0], &t[1],
                        vips_amiMSBfirst() != ppm->msb_first ) ||
		vips_image_write( t[1], image ) ) 
		return( -1 );

	return( 0 );
}

static int
vips_foreign_load_ppm_generate_binary( VipsRegion *or, 
	void *seq, void *a, void *b, gboolean *stop )
{
        VipsRect *r = &or->valid;
	VipsForeignLoadPpm *ppm = (VipsForeignLoadPpm *) a;
	VipsObjectClass *class = VIPS_OBJECT_GET_CLASS( ppm );
	VipsImage *image = or->im;
	size_t sizeof_line = VIPS_IMAGE_SIZEOF_LINE( image );

	int y;

	for( y = 0; y < r->height; y++ ) {
		VipsPel *q = VIPS_REGION_ADDR( or, 0, r->top + y );

		size_t bytes_read;

		bytes_read = vips_source_read( ppm->source, q, sizeof_line );
		if( bytes_read != sizeof_line ) {
			vips_error( class->nickname, 
				"%s", _( "file truncated" ) );
			return( -1 );
		}
	}

	return( 0 );
}

static int
vips_foreign_load_ppm_generate_1bit_ascii( VipsRegion *or, 
	void *seq, void *a, void *b, gboolean *stop )
{
        VipsRect *r = &or->valid;
	VipsForeignLoadPpm *ppm = (VipsForeignLoadPpm *) a;
	VipsImage *image = or->im;

	int x, y;

	for( y = 0; y < r->height; y++ ) {
		VipsPel *q = VIPS_REGION_ADDR( or, 0, r->top + y );

		for( x = 0; x < image->Xsize; x++ ) {
			int val;

			if( get_int( ppm->sbuf, &val ) )
				return( -1 );

			if( val )
				q[x] = 0;
			else
				q[x] = 255;
		}
	}

	return( 0 );
}

static int
vips_foreign_load_ppm_generate_1bit_binary( VipsRegion *or, 
	void *seq, void *a, void *b, gboolean *stop )
{
        VipsRect *r = &or->valid;
	VipsForeignLoadPpm *ppm = (VipsForeignLoadPpm *) a;
	VipsImage *image = or->im;

	int x, y;
	int bits;

	bits = VIPS_SBUF_GETC( ppm->sbuf );
	for( y = 0; y < r->height; y++ ) {
		VipsPel *q = VIPS_REGION_ADDR( or, 0, r->top + y );

		for( x = 0; x < image->Xsize; x++ ) {
			q[x] = (bits & 128) ? 0 : 255;
			bits = VIPS_LSHIFT_INT( bits, 1 );
			if( (x & 7) == 7 )
				bits = VIPS_SBUF_GETC( ppm->sbuf );
		}

		/* Skip any unaligned bits at end of line.
		 */
		if( (x & 7) != 0 )
			bits = VIPS_SBUF_GETC( ppm->sbuf );
	}

	return( 0 );
}

static int
vips_foreign_load_ppm_generate_ascii_int( VipsRegion *or, 
	void *seq, void *a, void *b, gboolean *stop )
{
        VipsRect *r = &or->valid;
	VipsForeignLoadPpm *ppm = (VipsForeignLoadPpm *) a;
	VipsImage *image = or->im;
	int n_elements = image->Xsize * image->Bands;

	int i, y;

	for( y = 0; y < r->height; y++ ) {
		VipsPel *q = VIPS_REGION_ADDR( or, r->left, r->top + y );

		for( i = 0; i < n_elements; i++ ) {
			int val;

			if( get_int( ppm->sbuf, &val ) )
				return( -1 );
			
			switch( image->BandFmt ) {
			case VIPS_FORMAT_UCHAR:
				q[i] = VIPS_CLIP( 0, val, 255 );
				break;

			case VIPS_FORMAT_USHORT:
				((unsigned short *) q)[i] = 
					VIPS_CLIP( 0, val, 65535 );
				break;

			case VIPS_FORMAT_UINT:
				((unsigned int *) q)[i] = val;
				break;

			default:
				g_assert_not_reached();
			}
		}
	}

	return( 0 );
}

typedef int (*VipsPpmLoaderFn)( VipsForeignLoadPpm *ppm, VipsImage *image );

static int
vips_foreign_load_ppm_load( VipsForeignLoad *load )
{
	VipsForeignLoadPpm *ppm = (VipsForeignLoadPpm *) load;
	VipsImage **t = (VipsImage **) 
		vips_object_local_array( (VipsObject *) load, 2 );

	if( !ppm->have_read_header &&
		vips_foreign_load_ppm_parse_header( ppm ) )
		return( 0 );

	/* If the source is mappable and this is a binary file, we can map it.
	 */
	if( vips_source_is_mappable( ppm->source ) &&
		!ppm->ascii && 
		ppm->bits >= 8 ) {
		if( vips_foreign_load_ppm_map( ppm, load->real ) )
			return( -1 );
	}
	else {
		VipsGenerateFn generate;

		/* What sort of read are we doing?
		 */
		if( !ppm->ascii && ppm->bits >= 8 ) {
			generate = vips_foreign_load_ppm_generate_binary;

			/* The binary loader does not use the buffered IO 
			 * object.
			 */
			vips_sbuf_unbuffer( ppm->sbuf ); 
		}
		else if( !ppm->ascii && ppm->bits == 1 )
			generate = vips_foreign_load_ppm_generate_1bit_binary;
		else if( ppm->ascii && ppm->bits == 1 )
			generate = vips_foreign_load_ppm_generate_1bit_ascii;
		else 
			generate = vips_foreign_load_ppm_generate_ascii_int;

		t[0] = vips_image_new(); 
		vips_foreign_load_ppm_set_image( ppm, t[0] );
		if( vips_image_generate( t[0], 
				NULL, generate, NULL, ppm, NULL ) ||
			vips_sequential( t[0], &t[1], NULL ) ||
			vips_image_write( t[1], load->real ) ) 
			return( -1 );
	}

	if( vips_source_decode( ppm->source ) )
		return( -1 );

	return( 0 );
}

static void
vips_foreign_load_ppm_class_init( VipsForeignLoadPpmClass *class )
{
	GObjectClass *gobject_class = G_OBJECT_CLASS( class );
	VipsObjectClass *object_class = (VipsObjectClass *) class;
	VipsForeignClass *foreign_class = (VipsForeignClass *) class;
	VipsForeignLoadClass *load_class = (VipsForeignLoadClass *) class;

	gobject_class->dispose = vips_foreign_load_ppm_dispose;
	gobject_class->set_property = vips_object_set_property;
	gobject_class->get_property = vips_object_get_property;

	object_class->nickname = "ppmload_base";
	object_class->description = _( "load ppm base class" );

	foreign_class->suffs = vips__ppm_suffs;

	/* We are fast at is_a(), so high priority.
	 */
	foreign_class->priority = 200;

	load_class->get_flags = vips_foreign_load_ppm_get_flags;
	load_class->header = vips_foreign_load_ppm_header;
	load_class->load = vips_foreign_load_ppm_load;

}

static void
vips_foreign_load_ppm_init( VipsForeignLoadPpm *ppm )
{
	ppm->scale = 1.0;
}

typedef struct _VipsForeignLoadPpmFile {
	VipsForeignLoadPpm parent_object;

	char *filename;

} VipsForeignLoadPpmFile;

typedef VipsForeignLoadPpmClass VipsForeignLoadPpmFileClass;

G_DEFINE_TYPE( VipsForeignLoadPpmFile, vips_foreign_load_ppm_file, 
	vips_foreign_load_ppm_get_type() );

static gboolean
vips_foreign_load_ppm_file_is_a( const char *filename )
{
	VipsSource *source;
	gboolean result;

	if( !(source = vips_source_new_from_file( filename )) )
		return( FALSE );
	result = vips_foreign_load_ppm_is_a_source( source );
	VIPS_UNREF( source );

	return( result );
}

static int
vips_foreign_load_ppm_file_build( VipsObject *object )
{
	VipsForeignLoadPpmFile *file = (VipsForeignLoadPpmFile *) object;
	VipsForeignLoadPpm *ppm = (VipsForeignLoadPpm *) object;

	if( file->filename ) {
		if( !(ppm->source = 
			vips_source_new_from_file( file->filename )) )
			return( -1 );
		ppm->sbuf = vips_sbuf_new_from_source( ppm->source );
	}

	if( VIPS_OBJECT_CLASS( vips_foreign_load_ppm_file_parent_class )->
		build( object ) )
		return( -1 );

	return( 0 );
}

static void
vips_foreign_load_ppm_file_class_init( VipsForeignLoadPpmClass *class )
{
	GObjectClass *gobject_class = G_OBJECT_CLASS( class );
	VipsObjectClass *object_class = (VipsObjectClass *) class;
	VipsForeignLoadClass *load_class = (VipsForeignLoadClass *) class;

	gobject_class->set_property = vips_object_set_property;
	gobject_class->get_property = vips_object_get_property;

	object_class->nickname = "ppmload";
	object_class->description = _( "load ppm from file" );
	object_class->build = vips_foreign_load_ppm_file_build;

	load_class->is_a = vips_foreign_load_ppm_file_is_a;

	VIPS_ARG_STRING( class, "filename", 1, 
		_( "Filename" ),
		_( "Filename to load from" ),
		VIPS_ARGUMENT_REQUIRED_INPUT, 
		G_STRUCT_OFFSET( VipsForeignLoadPpmFile, filename ),
		NULL );
}

static void
vips_foreign_load_ppm_file_init( VipsForeignLoadPpmFile *file )
{
}

#endif /*HAVE_PPM*/

/**
 * vips_ppmload:
 * @filename: file to load
 * @out: (out): output image
 * @...: %NULL-terminated list of optional named arguments
 *
 * Read a PPM/PBM/PGM/PFM file into a VIPS image. 
 *
 * It can read 1, 8, 16 and 32 bit images, colour or monochrome,
 * stored in binary or in ASCII. One bit images become 8 bit VIPS images, 
 * with 0 and 255 for 0 and 1.
 *
 * See also: vips_image_new_from_file().
 *
 * Returns: 0 on success, -1 on error.
 */
int
vips_ppmload( const char *filename, VipsImage **out, ... )
{
	va_list ap;
	int result;

	va_start( ap, out );
	result = vips_call_split( "ppmload", ap, filename, out ); 
	va_end( ap );

	return( result );
}

