/*
 *  libMirage: CIF image: parser
 *  Copyright (C) 2008-2026 Henrik Stokseth
 *
 *  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.
 */

#include "image-cif.h"

#define __debug__ "CIF-Parser"


static const guint8 riff_signature[4] = {'R', 'I', 'F', 'F'};
static const guint8 imag_signature[4] = {'i', 'm', 'a', 'g'};
static const guint8  ofs_signature[4] = {'o', 'f', 's', ' '};
static const guint8 disc_signature[4] = {'d', 'i', 's', 'c'};


/**********************************************************************\
 *                  Object and its private structure                  *
\**********************************************************************/
struct _MirageParserCifPrivate
{
    MirageDisc *disc;

    MirageStream *cif_stream;

    /* "disc" block offset and length */
    guint64 disc_offset;
    guint32 disc_length;

    /* "ofs " block offset and length */
    guint64 ofs_offset;
    guint32 ofs_length;

    /* Parsed offset entries */
    gint num_offset_entries;
    CIF_OffsetEntry *offset_entries;

    gint track_counter;

    gint prev_track_mode;
};


G_DEFINE_DYNAMIC_TYPE_EXTENDED(
    MirageParserCif,
    mirage_parser_cif,
    MIRAGE_TYPE_PARSER,
    0,
    G_ADD_PRIVATE_DYNAMIC(MirageParserCif)
)

void mirage_parser_cif_type_register (GTypeModule *type_module)
{
    mirage_parser_cif_register_type(type_module);
}


/**********************************************************************\
 *                           Debug helpers                            *
\**********************************************************************/
static inline const gchar *debug_image_type (CIF_Image type)
{
    switch (type) {
        case DATA: return "Data CD";
        case MIXED: return "MixedMode CD";
        case MUSIC: return "Music CD";
        case ENCHANCED: return "Enchanced CD";
        case VIDEO: return "Video CD";
        case BOOTABLE: return "Bootable CD";
        case MP3: return "MP3 CD";
        default: return "UNKNOWN";
    }
}

static inline const gchar *debug_session_type (CIF_Session type)
{
    switch (type) {
        case CDDA: return "CD-DA";
        case CDROM: return "CD-ROM";
        case CDROMXA: return "CD-ROM XA";
        default: return "UNKNOWN";
    }
}

static inline const gchar *debug_track_type (CIF_Track type)
{
    switch (type) {
        case AUDIO: return "Audio";
        case MODE1: return "Mode 1";
        case MODE2_FORM1: return "Mode 2 Form 1";
        case MODE2_MIXED: return "Mode 2 Mixed";
        default: return "UNKNOWN";
    }
}


/**********************************************************************\
 *                             Helpers                                *
\**********************************************************************/
static inline gboolean gap_between_tracks (gint mode_prev, gint mode_cur)
{
    /* We create gaps on transitions between audio and data tracks */
    return (mode_prev == MIRAGE_SECTOR_AUDIO && mode_cur != MIRAGE_SECTOR_AUDIO)  || (mode_prev != MIRAGE_SECTOR_AUDIO && mode_cur == MIRAGE_SECTOR_AUDIO);
}


/**********************************************************************\
 *                   Descriptor reading and parsing                   *
\**********************************************************************/
static gboolean mirage_parser_cif_read_descriptor (MirageParserCif *self, guint8 **data, guint16 *length, GError **error)
{
    guint16 subblock_length;
    guint8 *subblock_data;

    /* Read entry length */
    if (mirage_stream_read(self->priv->cif_stream, &subblock_length, sizeof(subblock_length), NULL) != sizeof(subblock_length)) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: failed to read sub-block length!", __debug__);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_IMAGE_FILE_ERROR, Q_("Failed to read sub-block length!"));
        return FALSE;
    }
    subblock_length = GUINT16_FROM_LE(subblock_length);

    /* Go back */
    mirage_stream_seek(self->priv->cif_stream, -sizeof(subblock_length), G_SEEK_CUR, NULL);

    /* Sanity check */
    if ((gsize)mirage_stream_tell(self->priv->cif_stream) + subblock_length > self->priv->disc_offset + self->priv->disc_length) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: sanity check failed!", __debug__);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_PARSER_ERROR, Q_("Sanity check failed!"));
        return FALSE;
    }

    /* Allocate buffer and read data */
    subblock_data = g_malloc(subblock_length);
    if (mirage_stream_read(self->priv->cif_stream, subblock_data, subblock_length, NULL) != subblock_length) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: failed to read sub-block data (%d bytes)!", __debug__, subblock_length);
        g_free(subblock_data);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_IMAGE_FILE_ERROR, Q_("Failed to read sub-block data (%d bytes)!"), subblock_length);
        return FALSE;
    }

    *data = subblock_data;
    *length = subblock_length;

    return TRUE;
}


static MirageTrack *mirage_parser_cif_parse_track_descriptor (MirageParserCif *self, guint8 *data, guint16 length G_GNUC_UNUSED, GError **error)
{
    MirageTrack *track;
    MirageFragment *fragment;

    CIF_TrackDescriptor *descriptor = (CIF_TrackDescriptor *)data;
    CIF_OffsetEntry *offset_entry = &self->priv->offset_entries[self->priv->track_counter++]; /* Corresponding offset entry; we also increment track counter here */

    CIF_AudioTrackDescriptor *audio_descriptor = (CIF_AudioTrackDescriptor *)(data+sizeof(CIF_TrackDescriptor));

    guint sector_size;
    guint track_length;
    gint track_mode;

    descriptor->descriptor_length = GUINT16_FROM_LE(descriptor->descriptor_length);
    descriptor->dummy1 = GUINT16_FROM_LE(descriptor->dummy1);
    descriptor->num_sectors = GUINT32_FROM_LE(descriptor->num_sectors);
    descriptor->dummy2 = GUINT16_FROM_LE(descriptor->dummy2);
    descriptor->type = GUINT16_FROM_LE(descriptor->type);
    descriptor->dummy3 = GUINT16_FROM_LE(descriptor->dummy3);
    descriptor->dummy4 = GUINT16_FROM_LE(descriptor->dummy4);
    descriptor->dummy5 = GUINT16_FROM_LE(descriptor->dummy5);
    descriptor->dao_mode = GUINT16_FROM_LE(descriptor->dao_mode);
    descriptor->dummy7 = GUINT16_FROM_LE(descriptor->dummy7);
    descriptor->sector_data_size = GUINT16_FROM_LE(descriptor->sector_data_size);

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: Track descriptor:", __debug__);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - descriptor length: %d (0x%X)", __debug__, descriptor->descriptor_length, descriptor->descriptor_length);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - dummy1: %d", __debug__, descriptor->dummy1);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - number of sectors: %d (0x%X)", __debug__, descriptor->num_sectors, descriptor->num_sectors);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - dummy2: %d", __debug__, descriptor->dummy2);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - track type: %d (%s)", __debug__, descriptor->type, debug_track_type(descriptor->type));
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - dummy3: %d", __debug__, descriptor->dummy3);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - dummy4: %d", __debug__, descriptor->dummy4);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - dummy5: %d", __debug__, descriptor->dummy5);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - dao mode: %d", __debug__, descriptor->dao_mode);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - dummy7: %d", __debug__, descriptor->dummy7);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - sector data size: %d (0x%X)", __debug__, descriptor->sector_data_size, descriptor->sector_data_size);

    if ((CIF_Track) descriptor->type == AUDIO) {
        gchar *fadeinlen, *fadeoutlen;

        audio_descriptor->fadein_length = GUINT32_FROM_LE(audio_descriptor->fadein_length);
        audio_descriptor->fadeout_length = GUINT32_FROM_LE(audio_descriptor->fadeout_length);

        fadeinlen = mirage_helper_lba2msf_str(audio_descriptor->fadein_length, FALSE);
        fadeoutlen = mirage_helper_lba2msf_str(audio_descriptor->fadeout_length, FALSE);

        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: Audio track descriptor:", __debug__);
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - ISRC: %.12s", __debug__, audio_descriptor->isrc);
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - fade-out length: %s (%d)", __debug__, fadeoutlen, audio_descriptor->fadeout_length);
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - fade-in length: %s (%d)", __debug__, fadeinlen, audio_descriptor->fadein_length);
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - title: \"%s\"", __debug__, audio_descriptor->title);

        g_free(fadeinlen);
        g_free(fadeoutlen);
    }

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: Offset entry:", __debug__);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - type: %.4s", __debug__, offset_entry->type);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - offset: %d (0x%X)", __debug__, offset_entry->offset, offset_entry->offset);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - length: %d (0x%X)", __debug__, offset_entry->length, offset_entry->length);

    /* Set track mode */
    switch (descriptor->type) {
        case AUDIO: {
            track_mode = MIRAGE_SECTOR_AUDIO;
            sector_size = 2352;
            break;
        }
        case MODE1: {
            track_mode = MIRAGE_SECTOR_MODE1;
            sector_size = 2048;
            break;
        }
        case MODE2_FORM1: {
            track_mode = MIRAGE_SECTOR_MODE2_FORM1;
            sector_size = 2056;
            break;
        }
        case MODE2_MIXED: {
            track_mode = MIRAGE_SECTOR_MODE2_MIXED;
            sector_size = 2332;
            break;
        }
        default: {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: unknown track type (%d)!", __debug__, descriptor->type);
            g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_PARSER_ERROR, Q_("Unknown track type (%d)!"), descriptor->type);
            return NULL;
        }
    }

    /* Compute the actual track length */
    track_length = MAX(descriptor->num_sectors, CIF_MIN_TRACK_LEN);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: computed track length: %d (0x%X)", __debug__, track_length, track_length);

    /* Sanity check */
    if (track_length * sector_size > offset_entry->length) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: sanity check failed!", __debug__);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_PARSER_ERROR, Q_("Sanity check failed!"));
        return FALSE;
    }

    /* Create new track */
    track = g_object_new(MIRAGE_TYPE_TRACK, NULL);

    mirage_track_set_sector_type(track, track_mode);

    /* Create data fragment */
    fragment = g_object_new(MIRAGE_TYPE_FRAGMENT, NULL);

    /* Set file */
    mirage_fragment_main_data_set_stream(fragment, self->priv->cif_stream);
    mirage_fragment_main_data_set_offset(fragment, offset_entry->offset);
    mirage_fragment_main_data_set_size(fragment, sector_size);

    if (descriptor->type == AUDIO) {
        mirage_fragment_main_data_set_format(fragment, MIRAGE_MAIN_DATA_FORMAT_AUDIO);

        /* Validate and set ISRC */
        if (mirage_helper_validate_isrc(audio_descriptor->isrc)) {
            mirage_track_set_isrc(track, audio_descriptor->isrc);
        } else {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to validate ISRC: <%s>!", __debug__, audio_descriptor->isrc);
        }
    } else {
        mirage_fragment_main_data_set_format(fragment, MIRAGE_MAIN_DATA_FORMAT_DATA);
    }

    mirage_fragment_set_length(fragment, track_length);

    mirage_track_add_fragment(track, -1, fragment);

    g_object_unref(fragment);

    return track;
}

static MirageSession *mirage_parser_cif_parse_session_descriptor (MirageParserCif *self, guint8 *data, guint16 length G_GNUC_UNUSED, GError **error)
{
    MirageSession *session;
    CIF_SessionDescriptor *descriptor = (CIF_SessionDescriptor *)data;

    descriptor->descriptor_length = GUINT16_FROM_LE(descriptor->descriptor_length);
    descriptor->num_tracks = GUINT16_FROM_LE(descriptor->num_tracks);
    descriptor->dummy1 = GUINT16_FROM_LE(descriptor->dummy1);
    descriptor->dummy2 = GUINT16_FROM_LE(descriptor->dummy2);
    descriptor->dummy3 = GUINT16_FROM_LE(descriptor->dummy3);
    descriptor->session_type = GUINT16_FROM_LE(descriptor->session_type);
    descriptor->dummy4 = GUINT16_FROM_LE(descriptor->dummy4);
    descriptor->dummy5 = GUINT16_FROM_LE(descriptor->dummy5);
    descriptor->dummy6 = GUINT16_FROM_LE(descriptor->dummy6);

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: Session descriptor:", __debug__);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - descriptor length: %d (0x%X)", __debug__, descriptor->descriptor_length, descriptor->descriptor_length);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - number of tracks: %d", __debug__, descriptor->num_tracks);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - dummy1: %d", __debug__, descriptor->dummy1);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - dummy2: %d", __debug__, descriptor->dummy2);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - dummy3: %d", __debug__, descriptor->dummy3);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - session type: %d (%s)", __debug__, descriptor->session_type, debug_session_type(descriptor->session_type));
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - dummy4: %d", __debug__, descriptor->dummy4);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - dummy5: %d", __debug__, descriptor->dummy5);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - dummy6: %d", __debug__, descriptor->dummy6);

    /* Create new session */
    session = g_object_new(MIRAGE_TYPE_SESSION, NULL);

    /* Set session type */
    switch (descriptor->session_type) {
        case CDDA: {
            mirage_session_set_session_type(session, MIRAGE_SESSION_CDDA);
            break;
        }
        case CDROM: {
            mirage_session_set_session_type(session, MIRAGE_SESSION_CDROM);
            break;
        }
        case CDROMXA: {
            mirage_session_set_session_type(session, MIRAGE_SESSION_CDROM_XA);
            break;
        }
        default: {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: unknown session type (%d)!", __debug__, descriptor->session_type);
            g_object_unref(session);
            g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_PARSER_ERROR, Q_("Unknown session type (%d)!"), descriptor->session_type);
            return NULL;
        }
    }

    /* Process all tracks */
    for (gint i = 0; i < descriptor->num_tracks; i++) {
        guint8 *descriptor_data;
        guint16 descriptor_length;
        MirageTrack *track;
        gint track_mode;

        /* Read track descriptor */
        if (!mirage_parser_cif_read_descriptor(self, &descriptor_data, &descriptor_length, error)) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read track descriptor!", __debug__);
            g_object_unref(session);
            return NULL;
        }

        /* Parse track descriptor */
        track = mirage_parser_cif_parse_track_descriptor(self, descriptor_data, descriptor_length, error);
        if (!track) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to parse track descriptor!", __debug__);
            g_object_unref(session);
            return NULL;
        }

        track_mode = mirage_track_get_sector_type(track);

        /* If it is a first track in session, or if track mode has changed,
           add 150-sector pregap. */
        if (i == 0 || gap_between_tracks(self->priv->prev_track_mode, track_mode)) {
            MirageFragment *fragment;
            gint pregap_length = 150;

            MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: adding pregap: %d (0x%X)", __debug__, pregap_length, pregap_length);

            /* Create NULL fragment */
            fragment = g_object_new(MIRAGE_TYPE_FRAGMENT, NULL);

            mirage_fragment_set_length(fragment, pregap_length);

            mirage_track_add_fragment(track, 0, fragment);
            g_object_unref(fragment);

            /* Set new track start */
            mirage_track_set_track_start(track, pregap_length);
        }

        /* Add track */
        mirage_session_add_track_by_index(session, i, track);

        g_object_unref(track);
        g_free(descriptor_data);

        self->priv->prev_track_mode = track_mode; /* Store track mode */
    }

    return session;
}


static gboolean mirage_parser_cif_parse_disc_descriptor (MirageParserCif *self, guint8 *data, guint16 length G_GNUC_UNUSED, GError **error)
{
    CIF_DiscDescriptor *descriptor = (CIF_DiscDescriptor *)data;

    descriptor->descriptor_length = GUINT16_FROM_LE(descriptor->descriptor_length);
    descriptor->num_sessions = GUINT16_FROM_LE(descriptor->num_sessions);
    descriptor->num_tracks = GUINT16_FROM_LE(descriptor->num_tracks);
    descriptor->title_length = GUINT16_FROM_LE(descriptor->title_length);
    descriptor->descriptor_length2 = GUINT16_FROM_LE(descriptor->descriptor_length2);
    descriptor->dummy1 = GUINT16_FROM_LE(descriptor->dummy1);
    descriptor->image_type = GUINT16_FROM_LE(descriptor->image_type);
    descriptor->dummy2 = GUINT16_FROM_LE(descriptor->dummy2);

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: Disc descriptor:", __debug__);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - descriptor length: %d (0x%X)", __debug__, descriptor->descriptor_length, descriptor->descriptor_length);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - number of sessions: %d", __debug__, descriptor->num_sessions);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - number of tracks: %d", __debug__, descriptor->num_tracks);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - title length: %d (0x%X)", __debug__, descriptor->title_length, descriptor->title_length);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - descriptor length 2: %d (0x%X)", __debug__, descriptor->descriptor_length2, descriptor->descriptor_length2);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - dummy1: %d (0x%X)", __debug__, descriptor->dummy1, descriptor->dummy1);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - image type: %d (%s)", __debug__, descriptor->image_type, debug_image_type(descriptor->image_type));
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - dummy2: %d (0x%X)", __debug__, descriptor->dummy2, descriptor->dummy2);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - title: \"%.*s\"", __debug__, descriptor->title_length, descriptor->title_and_artist);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s:  - artist: \"%s\"", __debug__, descriptor->title_and_artist+descriptor->title_length);

    /* Process all sessions */
    for (gint i = 0; i < descriptor->num_sessions; i++) {
        guint8 *descriptor_data;
        guint16 descriptor_length;
        MirageSession *session;

        /* Read session descriptor */
        if (!mirage_parser_cif_read_descriptor(self, &descriptor_data, &descriptor_length, error)) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read session descriptor!", __debug__);
            return FALSE;
        }

        /* Parse session descriptor */
        session = mirage_parser_cif_parse_session_descriptor(self, descriptor_data, descriptor_length, error);
        if (!session) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to parse session descriptor!", __debug__);
            return FALSE;
        }

        mirage_disc_add_session_by_index(self->priv->disc, i, session);

        g_object_unref(session);
        g_free(descriptor_data);

        /* Set leadout of the previous session */
        if (i > 0) {
            MirageSession *prev_session;
            gint leadout_length;

            prev_session = mirage_disc_get_session_by_index(self->priv->disc, i-1, error);
            if (!prev_session) {
                MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: failed to get previous session!", __debug__);
                return FALSE;
            }

            /* Second session has index 1... */
            if (i == 1) {
                leadout_length = 11250; /* Actually, it should be 6750 previous leadout, 4500 current leadin */
            } else {
                leadout_length = 6750; /* Actually, it should be 2250 previous leadout, 4500 current leadin */
            }

            mirage_session_set_leadout_length(prev_session, leadout_length);

            g_object_unref(prev_session);
        }
    }


    return TRUE;
}


static gboolean mirage_parser_cif_parse_disc_block (MirageParserCif *self, GError **error)
{
    guint8 *descriptor_data;
    guint16 descriptor_length;

    /* Seek to the content of "disc" block */
    mirage_stream_seek(self->priv->cif_stream, self->priv->disc_offset, G_SEEK_SET, NULL);

    /* Skip first 8 dummy bytes */
    mirage_stream_seek(self->priv->cif_stream, 8, G_SEEK_CUR, NULL);

    /* Read disc descriptor */
    if (!mirage_parser_cif_read_descriptor(self, &descriptor_data, &descriptor_length, error)) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read disc descriptor!", __debug__);
        return FALSE;
    }

    /* Parse disc descriptor */
    if (!mirage_parser_cif_parse_disc_descriptor(self, descriptor_data, descriptor_length, error)) {
        g_free(descriptor_data);
        return FALSE;
    }

    g_free(descriptor_data);

    /* Sanity check */
    if ((gsize)mirage_stream_tell(self->priv->cif_stream) != (self->priv->disc_offset + self->priv->disc_length)) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: WARNING: did not finish at the end of disc block!", __debug__);
    }

    return TRUE;
}

static gboolean mirage_parser_cif_parse_ofs_block (MirageParserCif *self, GError **error)
{
    guint16 num_entries;

    /* Seek to the content of "ofs " block */
    mirage_stream_seek(self->priv->cif_stream, self->priv->ofs_offset, G_SEEK_SET, NULL);

    /* Skip first 8 dummy bytes */
    mirage_stream_seek(self->priv->cif_stream, 8, G_SEEK_CUR, NULL);

    /* Read number of entries */
    if (mirage_stream_read(self->priv->cif_stream, &num_entries, sizeof(num_entries), NULL) != sizeof(num_entries)) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read number of entries!", __debug__);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_IMAGE_FILE_ERROR, Q_("Failed to read number of entries!"));
        return FALSE;
    }
    num_entries = GUINT16_FROM_LE(num_entries);

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: number of entries: %d", __debug__, num_entries);

    self->priv->offset_entries = g_new0(CIF_OffsetEntry, num_entries);
    self->priv->num_offset_entries = num_entries;

    for (guint i = 0; i < num_entries && (gsize)mirage_stream_tell(self->priv->cif_stream) < (self->priv->ofs_offset + self->priv->ofs_length); i++) {
        CIF_OffsetEntry entry;

        /* Read whole entry */
        if (mirage_stream_read(self->priv->cif_stream, &entry, sizeof(entry), NULL) != sizeof(entry)) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read \"ofs\" entry!", __debug__);
            g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_IMAGE_FILE_ERROR, Q_("Failed to read \"ofs\" entry!"));
            return FALSE;
        }

        /* Match "RIFF" */
        if (memcmp(entry.riff, riff_signature, sizeof(riff_signature))) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: expected 'RIFF', got '%.4s'", __debug__, entry.riff);
            g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_PARSER_ERROR, Q_("Invalid FOURCC; expected 'RIFF', got '%.4s'"), entry.riff);
            return FALSE;
        }

        entry.length = GUINT32_FROM_LE(entry.length) - 4; /* Since it includes the size of length field */
        entry.offset = GUINT32_FROM_LE(entry.offset) + 4; /* Since it's offset to type field, which we need to skip */

        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: entry #%d: type '%.4s', offset %d (0x%X), length %d (0x%X)", __debug__, i, entry.type, entry.offset, entry.offset, entry.length, entry.length);

        /* Store */
        self->priv->offset_entries[i] = entry;
    }

    /* Sanity check */
    if ((gsize)mirage_stream_tell(self->priv->cif_stream) != (self->priv->ofs_offset + self->priv->ofs_length)) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: WARNING: did not finish at the end of ofs block!", __debug__);
    }

    return TRUE;
}


static gboolean mirage_parser_cif_parse_blocks (MirageParserCif *self, GError **error)
{
    guint64 file_size;
    CIF_Header header;
    guint64 offset;

    /* Get file length */
    mirage_stream_seek(self->priv->cif_stream, 0, G_SEEK_END, NULL);
    file_size = mirage_stream_tell(self->priv->cif_stream);

    /* Build blocks list */
    mirage_stream_seek(self->priv->cif_stream, 0, G_SEEK_SET, NULL);
    while ((gsize)mirage_stream_tell(self->priv->cif_stream) < file_size) {
        /* Read whole header */
        if (mirage_stream_read(self->priv->cif_stream, &header, sizeof(header), NULL) != sizeof(header)) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: failed to read header!", __debug__);
            g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_IMAGE_FILE_ERROR, Q_("Failed to read header!"));
            return FALSE;
        }

        /* Match "RIFF" */
        if (memcmp(header.riff, riff_signature, sizeof(riff_signature))) {
            MIRAGE_DEBUG(self, MIRAGE_DEBUG_WARNING, "%s: expected 'RIFF', got '%.4s'", __debug__, header.riff);
            g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_PARSER_ERROR, Q_("Invalid FOURCC; expected 'RIFF', got '%.4s'"), header.riff);
            return FALSE;
        }

        header.length = GUINT32_FROM_LE(header.length) - 4;

        /* Store data offset */
        offset = mirage_stream_tell(self->priv->cif_stream);

        /* Skip the contents */
        mirage_stream_seek(self->priv->cif_stream, header.length, G_SEEK_CUR, NULL);
        if (header.length % 2) {
            mirage_stream_seek(self->priv->cif_stream, 1, G_SEEK_CUR, NULL); /* Pad byte */
        }

        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: RIFF chunk of type '%.4s': offset %" G_GINT64_MODIFIER "d (0x%" G_GINT64_MODIFIER "X), length %d (0x%X)", __debug__, header.type, offset, offset, header.length, header.length);

        /* We need to store "disc" and "ofs " offsets */
        if (!memcmp(header.type, disc_signature, sizeof(disc_signature))) {
            self->priv->disc_offset = offset;
            self->priv->disc_length = header.length;
        } else if (!memcmp(header.type, ofs_signature, sizeof(ofs_signature))) {
            self->priv->ofs_offset = offset;
            self->priv->ofs_length = header.length;
        }
    }

    /* Make sure we got "disc" and "ofs " blocks */
    if (!self->priv->disc_offset || !self->priv->disc_length) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: 'disc' block not found!", __debug__);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_PARSER_ERROR, Q_("Descriptor does not contain 'disc' block!"));
        return FALSE;
    }

    if (!self->priv->ofs_offset || !self->priv->ofs_length) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: 'ofs ' block not found!", __debug__);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_PARSER_ERROR, Q_("Descriptor does not contain 'ofs ' block!"));
        return FALSE;
    }

    return TRUE;
}


static gboolean mirage_parser_cif_load_disc (MirageParserCif *self, GError **error)
{
    /* Parse blocks */
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: parsing RIFF blocks", __debug__);
    if (!mirage_parser_cif_parse_blocks(self, error)) {
        return FALSE;
    }

    /* CIF format is CD-only */
    mirage_disc_set_medium_type(self->priv->disc, MIRAGE_MEDIUM_CD);

    /* CD-ROMs start at -150 as per Red Book... */
    mirage_disc_layout_set_start_sector(self->priv->disc, -150);

    /* Parse "ofs " block */
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: parsing 'ofs ' block", __debug__);
    if (!mirage_parser_cif_parse_ofs_block(self, error)) {
        return FALSE;
    }

    /* Parse "disc" block */
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: parsing 'disc' block", __debug__);
    if (!mirage_parser_cif_parse_disc_block(self, error)) {
        return FALSE;
    }

    return TRUE;
}


/**********************************************************************\
 *                MirageParser methods implementation                *
\**********************************************************************/
static MirageDisc *mirage_parser_cif_load_image (MirageParser *_self, MirageStream **streams, GError **error)
{
    MirageParserCif *self = MIRAGE_PARSER_CIF(_self);
    const gchar *cif_filename;
    gboolean succeeded = TRUE;
    CIF_Header header;

    /* Check file signature */
    self->priv->cif_stream = g_object_ref(streams[0]);

    mirage_stream_seek(self->priv->cif_stream, 0, G_SEEK_SET, NULL);

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_IMAGE_ID, "%s: checking if parser can handle given image...", __debug__);
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_IMAGE_ID, "%s: veryfing image header...", __debug__);
    if (mirage_stream_read(self->priv->cif_stream, &header, sizeof(header), NULL) != sizeof(header)) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_IMAGE_ID, "%s: parser cannot handle given image: failed to read header!", __debug__);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_CANNOT_HANDLE, Q_("Parser cannot handle given image: failed to read header!"));
        return FALSE;
    }

    /* Match "RIFF" at the beginning and "imag" that comes after length
     * field. We could probably also match length, since it appears to
     * be fixed, but this should be sufficient... */
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_IMAGE_ID, "%s: checking if image begins with 'RIFF'...", __debug__);
    if (memcmp(header.riff, riff_signature, sizeof(riff_signature))) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_IMAGE_ID, "%s: parser cannot handle given image: image does not begin with 'RIFF'!", __debug__);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_CANNOT_HANDLE, Q_("Parser cannot handle given image: image does not begin with 'RIFF'!"));
        return FALSE;
    }
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_IMAGE_ID, "%s: checking if first RIFF tag is 'imag'...", __debug__);
    if (memcmp(header.type, imag_signature, sizeof(imag_signature))) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_IMAGE_ID, "%s: parser cannot handle given image: first RIFF tag is not 'imag'!", __debug__);
        g_set_error(error, MIRAGE_ERROR, MIRAGE_ERROR_CANNOT_HANDLE, Q_("Parser cannot handle given image: invalid header!"));
        return FALSE;
    }
    MIRAGE_DEBUG(self, MIRAGE_DEBUG_IMAGE_ID, "%s: parser can handle given image!", __debug__);

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: parsing the image...", __debug__);

    /* Create disc */
    self->priv->disc = g_object_new(MIRAGE_TYPE_DISC, NULL);
    mirage_object_set_parent(MIRAGE_OBJECT(self->priv->disc), self);

    cif_filename = mirage_stream_get_filename(self->priv->cif_stream);
    mirage_disc_set_filename(self->priv->disc, cif_filename);

    MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: CIF filename: %s", __debug__, cif_filename);

    /* Load disc */
    succeeded = mirage_parser_cif_load_disc(self, error);

    /* Return disc */
    if (succeeded) {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: parsing completed successfully", __debug__);
        return self->priv->disc;
    } else {
        MIRAGE_DEBUG(self, MIRAGE_DEBUG_PARSER, "%s: parsing failed!", __debug__);
        g_object_unref(self->priv->disc);
        return NULL;
    }
}


/**********************************************************************\
 *                             Object init                            *
\**********************************************************************/
static void mirage_parser_cif_init (MirageParserCif *self)
{
    self->priv = mirage_parser_cif_get_instance_private(self);

    mirage_parser_generate_info(MIRAGE_PARSER(self),
        "PARSER-CIF",
        Q_("CIF Image Parser"),
        1,
        Q_("Adaptec Easy CD/DVD Creator images (*.cif)"), "application/x-cif"
    );

    self->priv->offset_entries = NULL;

    self->priv->track_counter = 0;
}

static void mirage_parser_cif_dispose (GObject *gobject)
{
    MirageParserCif *self = MIRAGE_PARSER_CIF(gobject);

    if (self->priv->cif_stream) {
        g_object_unref(self->priv->cif_stream);
        self->priv->cif_stream = NULL;
    }

    /* Chain up to the parent class */
    G_OBJECT_CLASS(mirage_parser_cif_parent_class)->dispose(gobject);
}

static void mirage_parser_cif_finalize (GObject *gobject)
{
    MirageParserCif *self = MIRAGE_PARSER_CIF(gobject);

    g_free(self->priv->offset_entries);

    /* Chain up to the parent class */
    G_OBJECT_CLASS(mirage_parser_cif_parent_class)->finalize(gobject);
}

static void mirage_parser_cif_class_init (MirageParserCifClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    MirageParserClass *parser_class = MIRAGE_PARSER_CLASS(klass);

    gobject_class->dispose = mirage_parser_cif_dispose;
    gobject_class->finalize = mirage_parser_cif_finalize;

    parser_class->load_image = mirage_parser_cif_load_image;
}

static void mirage_parser_cif_class_finalize (MirageParserCifClass *klass G_GNUC_UNUSED)
{
}
