/* This file is part of the KDE project
   Copyright (C) 2003 Ignacio Castaño <castano@ludicon.com>

   This program is free software; you can redistribute it and/or
   modify it under the terms of the Lesser 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 code is based on Thacher Ulrich PSD loading code released
   on public domain. See: http://tulrich.com/geekstuff/
*/

/* this code supports:
 * reading:
 *     rle and raw psd files
 * writing:
 *     not supported
 */

#include "psd.h"

#include <QtGui/QImage>
#include <QtCore/QDataStream>

#include <kdebug.h>

typedef quint32 uint;
typedef quint16 ushort;
typedef quint8 uchar;

namespace {	// Private.

	enum ColorMode {
		CM_BITMAP = 0,
		CM_GRAYSCALE = 1,
		CM_INDEXED = 2,
		CM_RGB = 3,
		CM_CMYK = 4,
		CM_MULTICHANNEL = 7,
		CM_DUOTONE = 8,
		CM_LABCOLOR = 9
	};

	struct PSDHeader {
		uint signature;
		ushort version;
		uchar reserved[6];
		ushort channel_count;
		uint height;
		uint width;
		ushort depth;
		ushort color_mode;
	};

	static QDataStream & operator>> ( QDataStream & s, PSDHeader & header )
	{
		s >> header.signature;
		s >> header.version;
		for( int i = 0; i < 6; i++ ) {
			s >> header.reserved[i];
		}
		s >> header.channel_count;
		s >> header.height;
		s >> header.width;
		s >> header.depth;
		s >> header.color_mode;
		return s;
	}
        static bool seekBy(QDataStream& s, unsigned int bytes)
        {
                char buf[4096];
                while (bytes) {
                        unsigned int num= qMin(bytes,( unsigned int )sizeof(buf));
                        unsigned int l = num;
                        s.readRawData(buf, l);
                        if(l != num)
                          return false;
                        bytes -= num;
                }
                return true;
        }

	// Check that the header is a valid PSD.
	static bool IsValid( const PSDHeader & header )
	{
		if( header.signature != 0x38425053 ) {	// '8BPS'
			return false;
		}
		return true;
	}

	// Check that the header is supported.
	static bool IsSupported( const PSDHeader & header )
	{
		if( header.version != 1 ) {
			return false;
		}
		if( header.channel_count > 16 ) {
			return false;
		}
		if( header.depth != 8 ) {
			return false;
		}
		if( header.color_mode != CM_RGB ) {
			return false;
		}
		return true;
	}

	// Load the PSD image.
	static bool LoadPSD( QDataStream & s, const PSDHeader & header, QImage & img )
	{
		// Create dst image.
		img = QImage( header.width, header.height, QImage::Format_RGB32 );

		uint tmp;

		// Skip mode data.
		s >> tmp;
		s.device()->seek( s.device()->pos() + tmp );

		// Skip image resources.
		s >> tmp;
		s.device()->seek( s.device()->pos() + tmp );

		// Skip the reserved data.
		s >> tmp;
		s.device()->seek( s.device()->pos() + tmp );

		// Find out if the data is compressed.
		// Known values:
		//   0: no compression
		//   1: RLE compressed
		ushort compression;
		s >> compression;

		if( compression > 1 ) {
			// Unknown compression type.
			return false;
		}

		uint channel_num = header.channel_count;

		// Clear the image.
		if( channel_num < 4 ) {
			img.fill(qRgba(0, 0, 0, 0xFF));
		}
		else {
			// Enable alpha.
			img = img.convertToFormat(QImage::Format_ARGB32);

			// Ignore the other channels.
			channel_num = 4;
		}

		const uint pixel_count = header.height * header.width;

		static const uint components[4] = {2, 1, 0, 3}; // @@ Is this endian dependant?

		if( compression ) {

			// Skip row lengths.
                        if(!seekBy(s, header.height*header.channel_count*sizeof(ushort)))
                                return false;

			// Read RLE data.
			for(uint channel = 0; channel < channel_num; channel++) {

				uchar * ptr = img.bits() + components[channel];

				uint count = 0;
				while( count < pixel_count ) {
					uchar c;
                                        if(s.atEnd())
                                                return false;
					s >> c;
					uint len = c;

					if( len < 128 ) {
						// Copy next len+1 bytes literally.
						len++;
						count += len;
                                                if ( count > pixel_count )
                                                        return false;

						while( len != 0 ) {
							s >> *ptr;
							ptr += 4;
							len--;
						}
					}
					else if( len > 128 ) {
						// Next -len+1 bytes in the dest are replicated from next source byte.
						// (Interpret len as a negative 8-bit int.)
						len ^= 0xFF;
						len += 2;
						count += len;
                                                if(s.atEnd() || count > pixel_count)
                                                        return false;
						uchar val;
						s >> val;
						while( len != 0 ) {
							*ptr = val;
							ptr += 4;
							len--;
						}
					}
					else if( len == 128 ) {
						// No-op.
					}
				}
			}
		}
		else {
			// We're at the raw image data.  It's each channel in order (Red, Green, Blue, Alpha, ...)
			// where each channel consists of an 8-bit value for each pixel in the image.

			// Read the data by channel.
			for(uint channel = 0; channel < channel_num; channel++) {

				uchar * ptr = img.bits() + components[channel];

				// Read the data.
				uint count = pixel_count;
				while( count != 0 ) {
					s >> *ptr;
					ptr += 4;
					count--;
				}
			}
		}

		return true;
	}

} // Private


PSDHandler::PSDHandler()
{
}

bool PSDHandler::canRead() const
{
    if (canRead(device())) {
        setFormat("psd");
        return true;
    }
    return false;
}

bool PSDHandler::read(QImage *image)
{
    QDataStream s( device() );
    s.setByteOrder( QDataStream::BigEndian );

    PSDHeader header;
    s >> header;

    // Check image file format.
    if( s.atEnd() || !IsValid( header ) ) {
        kDebug(399) << "This PSD file is not valid.";
        return false;
    }

    // Check if it's a supported format.
    if( !IsSupported( header ) ) {
        kDebug(399) << "This PSD file is not supported.";
        return false;
    }

    QImage img;
    if( !LoadPSD(s, header, img) ) {
        kDebug(399) << "Error loading PSD file.";
        return false;
    }

    *image = img;
    return true;
}

bool PSDHandler::write(const QImage &)
{
    // TODO Stub!
    return false;
}

QByteArray PSDHandler::name() const
{
    return "psd";
}

bool PSDHandler::canRead(QIODevice *device)
{
       if (!device) {
        qWarning("PSDHandler::canRead() called with no device");
        return false;
    }

    qint64 oldPos = device->pos();

    char head[4];
    qint64 readBytes = device->read(head, sizeof(head));
    if (readBytes != sizeof(head)) {
        if (device->isSequential()) {
            while (readBytes > 0)
                device->ungetChar(head[readBytes-- - 1]);
        } else {
            device->seek(oldPos);
        }
        return false;
    }

    if (device->isSequential()) {
        while (readBytes > 0)
            device->ungetChar(head[readBytes-- - 1]);
    } else {
        device->seek(oldPos);
    }

    return qstrncmp(head, "8BPS", 4) == 0;
}


class PSDPlugin : public QImageIOPlugin
{
public:
    QStringList keys() const;
    Capabilities capabilities(QIODevice *device, const QByteArray &format) const;
    QImageIOHandler *create(QIODevice *device, const QByteArray &format = QByteArray()) const;
};

QStringList PSDPlugin::keys() const
{
    return QStringList() << "psd" << "PSD";
}

QImageIOPlugin::Capabilities PSDPlugin::capabilities(QIODevice *device, const QByteArray &format) const
{
    if (format == "psd" || format == "PSD")
        return Capabilities(CanRead);
    if (!format.isEmpty())
        return 0;
    if (!device->isOpen())
        return 0;

    Capabilities cap;
    if (device->isReadable() && PSDHandler::canRead(device))
        cap |= CanRead;
    return cap;
}

QImageIOHandler *PSDPlugin::create(QIODevice *device, const QByteArray &format) const
{
    QImageIOHandler *handler = new PSDHandler;
    handler->setDevice(device);
    handler->setFormat(format);
    return handler;
}

Q_EXPORT_STATIC_PLUGIN(PSDPlugin)
Q_EXPORT_PLUGIN2(psd, PSDPlugin)
