/*
 *  Copyright (c) 2010 Lukáš Tvrdý <lukast.dev@gmail.com>
 *  Copyright (c) 2007 Eric Lamarque <eric.lamarque@free.fr>
 *
 *  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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

// Initial code from Marco Lamberto abr2gbr (http://the.sunnyspot.org/gimp/)
// Ported code from http://registry.gimp.org/node/126
 
#include <QColor>
#include <QtEndian>
#include <QByteArray>
#include <QDebug>
#include <QString>
#include <QFile>
#include <netinet/in.h>
#include <QImage>

struct AbrInfo{
    //big endian
    short version;
    short subversion;
    // count of the images (brushes) in the abr file
    short count;
};

/// save the QImages as png files to directory image_tests
static void saveToQImage(char * buffer,qint32 width, qint32 height, const QString fileName){
    // create 8-bit indexed image
    QImage img(width, height, QImage::Format_Indexed8);
    
    // every image needs color index table
    QVector<QRgb> table;
    for (int i = 0; i < 255; ++i) table.append(qRgb(i, i, i));
    img.setColorTable(table);

    for (int y = 0; y < height;y++){
        memcpy(img.scanLine(y), buffer + width * y, width);
    }

    QImage out(width, height, QImage::Format_ARGB32);
    QColor black(Qt::black);
    out.fill(black.rgb());
    out.setAlphaChannel(img);
    out.save("image_tests/" + fileName + ".png");
} 

static qint32 rle_decode(QDataStream & abr, char *buffer, qint32 height)
{
    qint32 n;
    char ptmp;
    char ch;
    int i, j, c;
    short *cscanline_len;
    char *data = buffer;

    // read compressed size foreach scanline
    cscanline_len = new short[ height ];
    for (i = 0; i < height; i++){
        // short
        abr >> cscanline_len[i];
    }
    
    // unpack each scanline data 
    for (i = 0; i < height; i++) {
        for (j = 0; j < cscanline_len[i];) {
            // char
            abr.device()->getChar(&ptmp);
            n = ptmp;
            
            j++;
            if (n >= 128)     // force sign 
                n -= 256;
            if (n < 0) {      // copy the following char -n + 1 times 
                if (n == -128)  // it's a nop 
                continue;
                n = -n + 1;
                // char
                abr.device()->getChar(&ch);
                
                j++;
                for (c = 0; c < n; c++, data++){
                    *data = ch;
                }
            } else 
            {          
                // read the following n + 1 chars (no compr) 
                for (c = 0; c < n + 1; c++, j++, data++){
                    // char
                    abr.device()->getChar(data);
                }
            }
        }
    }
    delete [] cscanline_len;
    return 0;
}


static QString abr_v1_brush_name (const QString filename, qint32 id)
{
    QString result = filename;
    int pos = filename.lastIndexOf(".");
    result.remove(pos,4);
    QTextStream(&result) << "_" << id;
    return result;
}

static bool abr_supported_content(AbrInfo *abr_hdr)
{
  switch(abr_hdr->version)
    {
    case 1:
    case 2:
      return true;
      break;
    case 6:
      if (abr_hdr->subversion == 1 || abr_hdr->subversion == 2)
        return true;
      break;
    }
  return false;
}

static bool abr_reach_8BIM_section(QDataStream & abr, const QString name)
{
    char tag[4];
    char tagname[5];
    qint32 section_size = 0;
    int r;
  
    // find 8BIMname section
    while(!abr.atEnd()) {
        r = abr.readRawData(tag,4);
        
        if (r != 4) {
            qDebug() << "Error: Cannot read 8BIM tag ";
            return false;
        }
        
        if (strncmp(tag, "8BIM", 4)) {
            qDebug() << "Error: Start tag not 8BIM but " << (int)tag[0] << (int)tag[1] << (int)tag[2] << (int)tag[3] << " at position " << abr.device()->pos();
            return false;
        }
        
        r = abr.readRawData(tagname,4);
        
        if (r != 4) {
            qDebug() << "Error: Cannot read 8BIM tag name";
            return false;
        }
        tagname[4] = '\0';

        QString s1 = QString::fromLatin1(tagname,4);
        
        if (!s1.compare(name)){
            return true;
        }
        
        // long
        abr >> section_size;
        abr.device()->seek( abr.device()->pos() + section_size);
    }
    return true;
}



static qint32 find_sample_count_v6(QDataStream & abr, AbrInfo *abr_info)
{
    qint64 origin;
    qint32 sample_section_size;
    qint32 sample_section_end;
    qint32 samples=0;
    qint32 data_start;
    
    qint32 brush_size;
    qint32 brush_end;

    if (!abr_supported_content(abr_info))
        return 0;

    origin = abr.device()->pos();

    if (!abr_reach_8BIM_section (abr, "samp")) {
        // reset to origin 
        abr.device()->seek(origin);
        return 0;
    }

    // long
    abr >> sample_section_size;
    sample_section_end = sample_section_size + abr.device()->pos();
    
  
  
  data_start = abr.device()->pos();

  while ( ( !abr.atEnd() ) && (abr.device()->pos() < sample_section_end) ) {
    // read long
    abr >> brush_size;
    brush_end = brush_size;
    // complement to 4 
    while (brush_end % 4 != 0) brush_end++;
    abr.device()->seek(abr.device()->pos() + brush_end);
    samples++;
  }

  // set stream to samples data 
  abr.device()->seek(data_start);
  
  //qDebug() <<"samples : "<< samples;
  return samples;
}



static bool abr_read_content(QDataStream & abr, AbrInfo *abr_hdr)
{

    abr >> abr_hdr->version;
    abr_hdr->subversion = 0;
    abr_hdr->count = 0;

    switch (abr_hdr->version)
        {
        case 1:
        case 2:
            abr >> abr_hdr->count;
        break;
        case 6:
            abr >> abr_hdr->subversion;
            abr_hdr->count = find_sample_count_v6(abr, abr_hdr);
        break;
        default:
            // unknown versions 
        break;
        }
    // next bytes in abr are samples data 
    
    return true;
}


static QString abr_read_ucs2_text (QDataStream & abr)
{
    quint64 name_size;
    quint64 buf_size;
    uint   i;
   /* two-bytes characters encoded (UCS-2)
    *  format:
    *   long : size - number of characters in string
    *   data : zero terminated UCS-2 string
    */

    // long
    abr >> name_size;
    if (name_size == 0)
    {
        return QString::Null();
    }
    
    //buf_size = name_size * 2;
    buf_size = name_size;
    
    //name_ucs2 = (char*) malloc (buf_size * sizeof (char));
    //name_ucs2 = new char[buf_size];
    
    ushort * name_ucs2 = new ushort[buf_size];
    for (i = 0; i < buf_size ; i++)
        {
            //* char*/
            //abr >> name_ucs2[i];
            
            // I will use ushort as that is input to fromUtf16
            abr >>  name_ucs2[i];
        }
    QString name_utf8 = QString::fromUtf16(name_ucs2, buf_size);
    delete [] name_ucs2;
    
    return name_utf8;
}


static quint32 abr_brush_load_v6 (QDataStream & abr, AbrInfo *abr_hdr, const QString filename, qint32 image_ID, qint32 id)
{
    Q_UNUSED(image_ID);
    qint32 brush_size;
    qint32 brush_end;
    qint32 complement_to_4;
    qint32 next_brush;

    qint32 top, left, bottom, right;
    short depth;
    char compression;

    qint32 width, height;
    qint32 size;

    qint32 layer_ID = -1;

    char *buffer;

    abr >> brush_size;
    brush_end = brush_size;
    // complement to 4 
    while (brush_end % 4 != 0){ 
        brush_end++;
    }
    
    complement_to_4 = brush_end - brush_size;
    next_brush = abr.device()->pos() + brush_end;

    // discard key
    abr.device()->seek( abr.device()->pos() + 37 );
    if (abr_hdr->subversion == 1)
        // discard short coordinates and unknown short 
        abr.device()->seek( abr.device()->pos() + 10 );
    else
        // discard unknown bytes 
        abr.device()->seek( abr.device()->pos() + 264 );

    // long
    abr >> top;
    abr >> left;
    abr >> bottom;
    abr >> right;
    // short
    abr >> depth;
    // char
    abr.device()->getChar(&compression);

    width = right - left;
    height = bottom - top;
    size = width * (depth >> 3) * height;

    // remove .abr and add some id, so something like test.abr -> test_12345
    QString name = abr_v1_brush_name (filename, id);
    
    buffer = (char*)malloc(size);

    // data decoding 
    if (!compression) {
        // not compressed - read raw bytes as brush data 
        //fread (buffer, size, 1, abr);
        abr.readRawData(buffer,size);
    } else {
        rle_decode (abr, buffer, height);
    }

    saveToQImage(buffer, width, height , name);
    
    free (buffer);
    abr.device()->seek(next_brush);

    layer_ID = id;
    return layer_ID;
}


static qint32 abr_brush_load_v12 (QDataStream & abr, AbrInfo *abr_hdr,const QString filename, qint32 image_ID, qint32 id)
{
    Q_UNUSED(image_ID);
    short brush_type;
    qint32 brush_size;
    qint32 next_brush;

    qint32 top, left, bottom, right;
    qint16 depth;
    char compression;
    QString name;

    qint32 width, height;
    qint32 size;

    qint32            layer_ID = -1;
    char           *buffer;
  
    // short
    abr >> brush_type;
    // long
    abr >> brush_size;
    next_brush = abr.device()->pos() + brush_size;

    switch (brush_type) {
        case 1: 
            // computed brush 
            // FIXME: support it! 
            qDebug() << "WARNING: computed brush unsupported, skipping.";
            abr.device()->seek(abr.device()->pos() + next_brush);
            // TODO: test also this one abr.skipRawData(next_brush);
            break;
        case 2: 
            // sampled brush 
            // discard 4 misc bytes and 2 spacing bytes 
            abr.device()->seek(abr.device()->pos() + 6);
            
            if (abr_hdr->version == 2)
            name = abr_read_ucs2_text (abr);
            if (name.isNull()){
                name = abr_v1_brush_name (filename, id);
            }

            // discard 1 byte for antialiasing and 4 x short for short bounds 
            abr.device()->seek(abr.device()->pos() + 9);

            // long
            abr >> top;
            abr >> left;
            abr >> bottom;
            abr >> right;
            // short
            abr >> depth;
            // char
            abr.device()->getChar(&compression);

            width = right - left;
            height = bottom - top;
            size = width * (depth >> 3) * height;

            /* FIXME: support wide brushes */
            if (height > 16384)
            {
                qDebug() << "WARNING: wide brushes not supported";
                abr.device()->seek(next_brush);
                break;
            }

            buffer = (char*)malloc(size);

            if (!compression)
            // not compressed - read raw bytes as brush data 
            //fread (buffer, size, 1, abr);
            abr.readRawData(buffer,size);
            else{
                rle_decode (abr, buffer, height);
            }

            saveToQImage(buffer,width,height,name);
            
            free (buffer);
            break;

        default:
            qDebug() << "WARNING: unknown brush type, skipping.";
            abr.device()->seek(next_brush);
    }

  return layer_ID;
}


static qint32 abr_brush_load (QDataStream & abr, AbrInfo *abr_hdr,const QString filename, qint32 image_ID, qint32 id)
{
  qint32 layer_ID = -1;
  switch (abr_hdr->version)
    {
    case 1:
    case 2:
      layer_ID = abr_brush_load_v12 (abr, abr_hdr, filename, image_ID, id);
      break;
    case 6:
      layer_ID = abr_brush_load_v6 (abr, abr_hdr, filename, image_ID, id);
      break;
    }

  return layer_ID;
}


static qint32 abr_load (const QString filename)
{
    QFile file(filename);
    AbrInfo abr_hdr;
    qint32 image_ID;
    int i;
    qint32 layer_ID;

  // check if the file is open correctly
    if (!file.open(QIODevice::ReadOnly))
    {
        qDebug() << "Can't open file " << filename;
        return -1;
    }
    QDataStream abr(&file); 

    if (!abr_read_content (abr, &abr_hdr))
    {
        qDebug() << "Error: cannot parse ABR file: " << filename;
        return -1;
    }

    if (!abr_supported_content (&abr_hdr))
    {
      qDebug() << "ERROR: unable to decode abr format version " << abr_hdr.version << "(subver " << abr_hdr.subversion << ")";
      return -1;
    }

    if (abr_hdr.count == 0)
    {
      qDebug() << "ERROR: no sample brush found in " << filename;
      return -1;
    }

    image_ID = 123456;
    
    for (i = 0; i < abr_hdr.count; i++)
    {
        layer_ID = abr_brush_load(abr, &abr_hdr, filename, image_ID, i + 1);
        if (layer_ID == -1){
            qDebug() << "Warning: problem loading brush #" << i << " in " << filename;
        }
        qDebug() << i + 1 << " / " << abr_hdr.count;
    }
    file.close();
  
    return image_ID;
}


int main(int argc, const char * argv[] ){
    QString fileName;
    if (argc != 2) {
        fileName = "test.abr";
    }else{
        fileName = QString::fromLatin1(argv[1]);
    }
    abr_load(fileName);
    return 0;
}
