/* This file is part of the KDE project
 * Copyright (C) 2002 Shane Wright <me@shanewright.co.uk>
 *
 * 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 version 2.
 *
 * 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; see the file COPYING.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 *
 */

#include "kfile_avi.h"
#include <QSize>
#include <k3process.h>
#include <klocale.h>
#include <kgenericfactory.h>
#include <kstringvalidator.h>
#include <kdebug.h>

#include <q3dict.h>
#include <qvalidator.h>
#include <q3cstring.h>
#include <QFile>
#include <QDateTime>

#if !defined(__osf__)
#include <inttypes.h>
#else
typedef unsigned short uint16_t;
typedef unsigned int   uint32_t;
typedef unsigned long long uint64_t;
#endif

typedef KGenericFactory<KAviPlugin> AviFactory;

K_EXPORT_COMPONENT_FACTORY(kfile_avi, AviFactory( "kfile_avi" ))

KAviPlugin::KAviPlugin(QObject *parent, 
                       const QStringList &args)

    : KFilePlugin(parent, args)
{
    KFileMimeTypeInfo* info = addMimeTypeInfo( "video/x-msvideo" );

    KFileMimeTypeInfo::GroupInfo* group = 0L;

    group = addGroupInfo(info, "Technical", i18n("Technical Details"));

    KFileMimeTypeInfo::ItemInfo* item;

    item = addItemInfo(group, "Length", i18n("Length"), QVariant::Int);
    setUnit(item, KFileMimeTypeInfo::Seconds);

    item = addItemInfo(group, "Resolution", i18n("Resolution"), QVariant::Size);

    item = addItemInfo(group, "Frame rate", i18n("Frame Rate"), QVariant::Int);
    setSuffix(item, i18n("fps"));

    item = addItemInfo(group, "Video codec", i18n("Video Codec"), QVariant::String);
    item = addItemInfo(group, "Audio codec", i18n("Audio Codec"), QVariant::String);

}

bool KAviPlugin::read_avi()
{
    static const char sig_riff[] = "RIFF";
    static const char sig_avi[]  = "AVI ";
    static const char sig_list[] = "LIST";
    static const char sig_junk[] = "JUNK";
    uint32_t dwbuf1;

    done_avih = false;
    done_audio = false;

    // read AVI header
    char charbuf1[5];
    charbuf1[4] = '\0';

    // this must be RIFF
    f.read(charbuf1, 4);
    if (memcmp(charbuf1, sig_riff, 4) != 0)
        return false;

    dstream >> dwbuf1;

    // this must be AVI
    f.read(charbuf1, 4);
    if (memcmp(charbuf1, sig_avi, 4) != 0)
        return false;


    // start reading AVI file
    int counter = 0;
    bool done = false;
    do {

        // read header
        f.read(charbuf1, 4);

        kDebug(7034) << "about to handle chunk with ID: " << charbuf1 << "\n";

        if (memcmp(charbuf1, sig_list, 4) == 0) {
            // if list
            if (!read_list())
                return false;

        } else if (memcmp(charbuf1, sig_junk, 4) == 0) {
            // if junk

            // read chunk size
            dstream >> dwbuf1;

            kDebug(7034) << "Skipping junk chunk length: " << dwbuf1 << "\n";

            // skip junk
            f.seek( f.pos() + dwbuf1 );

        } else {
            // something we don't understand yet
            kDebug(7034) << "Unknown chunk header found: " << charbuf1 << "\n";
            return false;
        };

        if (
          ((done_avih) && (strlen(handler_vids) > 0) && (done_audio)) ||
          f.atEnd()) {
            kDebug(7034) << "We're done!\n";
            done = true;
        }

        // make sure we don't stay here forever
        ++counter;
        if (counter > 10)
            done = true;

    } while (!done);

    return true;
}


bool KAviPlugin::read_list()
{
    const char sig_hdrl[] = "hdrl";   // header list
    const char sig_strl[] = "strl";   // ...list
    const char sig_movi[] = "movi";   // movie list

    uint32_t dwbuf1;
    char charbuf1[5];
    charbuf1[4] = '\0';

    kDebug(7034) << "In read_list()\n";

    // read size & list type
    dstream >> dwbuf1;
    f.read(charbuf1, 4);

    // read the relevant bits of the list
    if (memcmp(charbuf1, sig_hdrl, 4) == 0) {
        // should be the main AVI header
        if (!read_avih())
            return false;

    } else if (memcmp(charbuf1, sig_strl, 4) == 0) {
        // should be some stream info
        if (!read_strl())
            return false;

    } else if (memcmp(charbuf1, sig_movi, 4) == 0) {
        // movie list

        kDebug(7034) << "Skipping movi chunk length: " << dwbuf1 << "\n";

        // skip past it
        f.seek( f.pos() + dwbuf1 );

    } else {
        // unknown list type
        kDebug(7034) << "Unknown list type found: " << charbuf1 << "\n";
    }

    return true;
}


bool KAviPlugin::read_avih()
{
    static const char sig_avih[] = "avih";   // header list

    uint32_t dwbuf1;
    char charbuf1[5];

    // read header and length
    f.read(charbuf1, 4);
    dstream >> dwbuf1;

    // not a valid avih?
    if (memcmp(charbuf1, sig_avih, 4) != 0) {
        kDebug(7034) << "Chunk ID error, expected avih, got: " << charbuf1 << "\n";
        return false;
    }

    // read all the avih fields
    dstream >> avih_microsecperframe;
    dstream >> avih_maxbytespersec;
    dstream >> avih_reserved1;
    dstream >> avih_flags;
    dstream >> avih_totalframes;
    dstream >> avih_initialframes;
    dstream >> avih_streams;
    dstream >> avih_buffersize;
    dstream >> avih_width;
    dstream >> avih_height;
    dstream >> avih_scale;
    dstream >> avih_rate;
    dstream >> avih_start;
    dstream >> avih_length;

    done_avih = true;

    return true;
}


bool KAviPlugin::read_strl()
{
    static const char sig_strh[] = "strh";
    static const char sig_strf[] = "strf";
    //static const char sig_strd[] = "strd";
    static const char sig_strn[] = "strn";
    static const char sig_list[] = "LIST";
    static const char sig_junk[] = "JUNK";

    kDebug(7034) << "in strl handler\n";

    uint32_t dwbuf1;    // buffer for block sizes
    char charbuf1[5];

    // loop through blocks
    int counter = 0;
    while (true) {

        // read type and size
        f.read(charbuf1, 4);   // type
        dstream >> dwbuf1;          // size

        // detect type
        if (memcmp(charbuf1, sig_strh, 4) == 0) {
            // got strh - stream header
            kDebug(7034) << "Found strh, calling read_strh()\n";
            read_strh(dwbuf1);

        } else if (memcmp(charbuf1, sig_strf, 4) == 0) {
            // got strf - stream format
            kDebug(7034) << "Found strf, calling read_strf()\n";
            read_strf(dwbuf1);

        } else if (memcmp(charbuf1, sig_strn, 4) == 0) {
            // we ignore strn, but it can be recorded incorrectly so we have to cope especially

            // skip it
            kDebug(7034) << "Skipping strn chunk length: " << dwbuf1 << "\n";
            f.seek( f.pos() + dwbuf1 );

            /*
            this is a pretty annoying hack; many AVIs incorrectly report the
            length of the strn field by 1 byte.  It is possible that strn's
            should be word aligned, but no mention in the specs...

            I'll clean/optimise this a touch soon
            */

            bool done = false;
            unsigned char counter = 0;
            while (!done) {
                // read next marker
                f.read(charbuf1, 4);

                // does it look ok?
                if ((memcmp(charbuf1, sig_list, 4) == 0) ||
                    (memcmp(charbuf1, sig_junk, 4) == 0)) {
                    // yes, go back before it
                    f.seek( f.pos() - 4);
                    done = true;
                } else {
                    // no, skip one space forward from where we were
                    f.seek( f.pos() - 3);
                    kDebug(7034) << "Working around incorrectly marked strn length..." << "\n";
                }

                // make sure we don't stay here too long
                ++counter;
                if (counter>10)
                    done = true;
            }

        } else if ((memcmp(charbuf1, sig_list, 4) == 0) || (memcmp(charbuf1, sig_junk, 4) == 0)) {
            // we have come to the end of our stay here in strl, time to leave

            kDebug(7034) << "Found LIST/JUNK, returning...\n";

            // rollback before the id and size
            f.seek( f.pos() - 8 );

            // return back to the main avi parser
            return true;

        } else {
            // we have some other unrecognised block type

            kDebug(7034) << "Sskipping unrecognised block\n";
            // just skip over it
            f.seek( f.pos() + dwbuf1);

        } /* switch block type */

        ++counter;
        if (counter > 10)
            return true;

    } /* while (true) */

    // we should never get here
}


bool KAviPlugin::read_strh(uint32_t blocksize)
{
    static const char sig_vids[] = "vids";   // ...video
    static const char sig_auds[] = "auds";   // ...audio

    uint32_t strh_flags;
    uint32_t strh_reserved1;
    uint32_t strh_initialframes;
    uint32_t strh_scale;
    uint32_t strh_rate;
    uint32_t strh_start;
    uint32_t strh_length;
    uint32_t strh_buffersize;
    uint32_t strh_quality;
    uint32_t strh_samplesize;

    char charbuf1[5];
    char charbuf2[5];


    // get stream info type, and handler id
    f.read(charbuf1, 4);
    f.read(charbuf2, 4);

    // read the strh fields
    dstream >> strh_flags;
    dstream >> strh_reserved1;
    dstream >> strh_initialframes;
    dstream >> strh_scale;
    dstream >> strh_rate;
    dstream >> strh_start;
    dstream >> strh_length;
    dstream >> strh_buffersize;
    dstream >> strh_quality;
    dstream >> strh_samplesize;

    if (memcmp(&charbuf1, sig_vids, 4) == 0) {
        // we are video!

        // save the handler
        memcpy(handler_vids, charbuf2, 4);
        kDebug(7034) << "Video handler: " << handler_vids << "\n";


    } else if (memcmp(&charbuf1, sig_auds, 4) == 0) {
        // we are audio!

        // save the handler
        memcpy(handler_auds, charbuf2, 4);
        kDebug(7034) << "Audio handler: " << handler_auds << "\n";

        // we want strf to get the audio codec
        wantstrf = true;

    } else {
        // we are something that we don't understand

    }

    // do we need to skip ahead any more?  (usually yes , contrary to
    // the AVI specs I've read...)
    // note: 48 is 10 * uint32_t + 2*FOURCC; the 10 fields we read above, plus the two character fields
    if (blocksize > 48)
        f.seek( f.pos() + (blocksize - 48) );

    return true;
}


bool KAviPlugin::read_strf(uint32_t blocksize)
{
    // do we want to do the strf?
    if (wantstrf) {
        // yes.  we want the audio codec identifier out of it

        // get the 16bit audio codec ID
        dstream >> handler_audio;
        kDebug(7034) << "Read audio codec ID: " << handler_audio << "\n";
        // skip past the rest of the stuff here for now
        f.seek( f.pos() + blocksize - 2);
        // we have audio
        done_audio = true;

    } else {
        // no, skip the strf
        f.seek( f.pos() + blocksize );
    }

    return true;
}



const char * KAviPlugin::resolve_audio(uint16_t id)
{
    /*
        this really wants to use some sort of KDE global
        list.  To avoid bloat for the moment it only does
        a few common codecs
    */

    static const char codec_unknown[] = I18N_NOOP("Unknown");
    static const char codec_01[]  = "Microsoft PCM";
    static const char codec_02[]  = "Microsoft ADPCM";
    static const char codec_50[]  = "MPEG";
    static const char codec_55[]  = "MP3";
    static const char codec_92[]  = "AC3";
    static const char codec_160[] = "WMA1";
    static const char codec_161[] = "WMA2";
    static const char codec_162[] = "WMA3";
    static const char codec_2000[] = "DVM";
    switch (id) {
    case 0x000 : return codec_unknown; break;
    case 0x001 : return codec_01; break;
    case 0x002 : return codec_02; break;
    case 0x050 : return codec_50; break;
    case 0x055 : return codec_55; break;
    case 0x092 : return codec_92; break;
    case 0x160 : return codec_160; break;
    case 0x161 : return codec_161; break;
    case 0x162 : return codec_162; break;
    case 0x2000 : return codec_2000; break;
    default : return codec_unknown;
    }

    return NULL;
}


bool KAviPlugin::readInfo( KFileMetaInfo& info, uint /*what*/)
{
    /***************************************************/
    // prep

    memset(handler_vids, 0x00, 5);
    memset(handler_auds, 0x00, 5);


    /***************************************************/
    // sort out the file

    if (f.isOpen())
        f.close();

    if ( info.path().isEmpty() ) // remote file
        return false;

    f.setFileName(info.path());

    // open file, set up stream and set endianness
    if (!f.open(QIODevice::ReadOnly))
    {
        kDebug(7034) << "Couldn't open " << QFile::encodeName(info.path());
        return false;
    }
    //QDataStream dstream(&file);
    dstream.setDevice(&f);

    dstream.setByteOrder(QDataStream::LittleEndian);


    /***************************************************/
    // start reading stuff from it

    wantstrf = false;

    if (!read_avi()) {
        kDebug(7034) << "read_avi() failed!";
    }

    /***************************************************/
    // set up our output

    if (done_avih) {

        KFileMetaInfoGroup group = appendGroup(info, "Technical");

	if (0 != avih_microsecperframe) {
	    appendItem(group, "Frame rate", int(1000000 / avih_microsecperframe));
	}
        appendItem(group, "Resolution", QSize(avih_width, avih_height));

        // work out and add length
        uint64_t mylength = (uint64_t) ((float) avih_totalframes * (float) avih_microsecperframe / 1000000.0);
        appendItem(group, "Length", int(mylength));


        if (strlen(handler_vids) > 0)
            appendItem(group, "Video codec", handler_vids);
        else
            appendItem(group, "Video codec", i18n("Unknown"));

        if (done_audio)
            appendItem(group, "Audio codec", i18n(resolve_audio(handler_audio)));
        else
            appendItem(group, "Audio codec", i18n("None"));

    }

    f.close();
    return true;
}

#include "kfile_avi.moc"
