/*
 *  CDEmu daemon: device - disc load/unload
 *  Copyright (C) 2006-2026 Rok Mandeljc
 *
 *  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 "cdemu.h"
#include "device-private.h"

#define __debug__ "Device"


/**********************************************************************\
 *                              Load disc                             *
\**********************************************************************/
static gboolean cdemu_device_load_disc_private (CdemuDevice *self, gchar **filenames, GVariant *options, GError **error)
{
    gint medium_type;

    /* Well, we won't do anything if we're already loaded */
    if (self->priv->loaded) {
        CDEMU_DEBUG(self, DAEMON_DEBUG_MMC, "%s: device already loaded", __debug__);
        g_set_error(error, CDEMU_ERROR, CDEMU_ERROR_ALREADY_LOADED, Q_("Device is already loaded!"));
        return FALSE;
    }

    /* Set options to the context */
    mirage_context_clear_options(self->priv->mirage_context);
    for (guint i = 0; i < g_variant_n_children(options); i++) {
        gchar *key;
        GVariant *value;

        g_variant_get_child(options, i, "{sv}", &key, &value);
        mirage_context_set_option(self->priv->mirage_context, key, value);
    }

    /* Load... */
    self->priv->disc = mirage_context_load_image(self->priv->mirage_context, filenames, error);

    /* Check if loading succeeded */
    if (!self->priv->disc) {
        return FALSE;
    }

    /* Mark loaded discs as non-writable */
    self->priv->recordable_disc = FALSE;
    self->priv->rewritable_disc = FALSE;
    self->priv->disc_closed = TRUE;

    /* Loading succeeded */
    self->priv->loaded = TRUE;

    /* Set current profile (and modify feature flags accordingly */
    medium_type = mirage_disc_get_medium_type(self->priv->disc);
    switch (medium_type) {
        case MIRAGE_MEDIUM_CD: {
            cdemu_device_set_profile(self, ProfileIndex_CDROM);
            break;
        }
        case MIRAGE_MEDIUM_DVD: {
            cdemu_device_set_profile(self, ProfileIndex_DVDROM);
            break;
        }
        case MIRAGE_MEDIUM_BD: {
            cdemu_device_set_profile(self, ProfileIndex_BDROM);
            break;
        }
        default: {
            CDEMU_DEBUG(self, DAEMON_DEBUG_WARNING, "%s: unknown media type: 0x%X!", __debug__, medium_type);
            break;
        }
    }

    /* Signal event */
    self->priv->media_event = MEDIA_EVENT_NEW_MEDIA;

    /* Send notification */
    g_signal_emit_by_name(self, "status-changed", NULL);

    return TRUE;
}

gboolean cdemu_device_load_disc (CdemuDevice *self, gchar **filenames, GVariant *options, GError **error)
{
    gboolean succeeded = TRUE;

    /* Load */
    g_mutex_lock(self->priv->device_mutex);
    succeeded = cdemu_device_load_disc_private(self, filenames, options, error);
    g_mutex_unlock(self->priv->device_mutex);

    return succeeded;
}

/**********************************************************************\
 *                         Create (blank) disc                        *
\**********************************************************************/
static gboolean cdemu_device_create_blank_disc_private (CdemuDevice *self, const gchar *filename, GVariant *options, GError **error)
{
    const gchar *writer_id = NULL;
    gint medium_type = MIRAGE_MEDIUM_CD;
    gint medium_capacity = 80 * 60 * 75; /* 80 min */
    gint profile_index = ProfileIndex_NONE;

    /* Well, we won't do anything if we're already loaded */
    if (self->priv->loaded) {
        CDEMU_DEBUG(self, DAEMON_DEBUG_MMC, "%s: device already loaded", __debug__);
        g_set_error(error, CDEMU_ERROR, CDEMU_ERROR_ALREADY_LOADED, Q_("Device is already loaded!"));
        return FALSE;
    }

    /* Extract and parse some of options */
    GHashTable *writer_parameters = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free, (GDestroyNotify)g_variant_unref);

    for (guint i = 0; i < g_variant_n_children(options); i++) {
        gchar *key;
        GVariant *value;

        g_variant_get_child(options, i, "{sv}", &key, &value);

        if (!g_ascii_strcasecmp(key, "writer-id")) {
            writer_id = g_variant_get_string(value, NULL);
        } else if (!g_ascii_strcasecmp(key, "medium-type")) {
            const gchar *medium_string = g_variant_get_string(value, NULL);
            if (!g_ascii_strcasecmp(medium_string, "cdr74")) {
                /* 74-minute CD-R (650 MB) */
                medium_type = MIRAGE_MEDIUM_CD;
                medium_capacity = 74 * 60 * 75;
                profile_index = ProfileIndex_CDR;
            } else if (!g_ascii_strcasecmp(medium_string, "cdr80")) {
                /* 80-minute CD-R (700 MB) */
                medium_type = MIRAGE_MEDIUM_CD;
                medium_capacity = 80 * 60 * 75;
                profile_index = ProfileIndex_CDR;
            } else if (!g_ascii_strcasecmp(medium_string, "cdr90")) {
                /* 90-minute CD-R (800 MB) */
                medium_type = MIRAGE_MEDIUM_CD;
                medium_capacity = 90 * 60 * 75;
                profile_index = ProfileIndex_CDR;
            } else if (!g_ascii_strcasecmp(medium_string, "cdr99")) {
                /* 99-minute CD-R (900 MB) */
                medium_type = MIRAGE_MEDIUM_CD;
                medium_capacity = 99 * 60 * 75;
                profile_index = ProfileIndex_CDR;
            } else if (!g_ascii_strcasecmp(medium_string, "dvd+r")) {
                /* DVD+R Single-Layer */
                medium_type = MIRAGE_MEDIUM_DVD;
                medium_capacity = 2295104;
                profile_index = ProfileIndex_DVDPLUSR;
            } else if (!g_ascii_strcasecmp(medium_string, "bdr")) {
                /* BD-R Single-Layer */
                medium_type = MIRAGE_MEDIUM_BD;
                medium_capacity = 12219392;
                profile_index = ProfileIndex_BDR_SRM;
            } else {
                g_set_error(error, CDEMU_ERROR, CDEMU_ERROR_INVALID_ARGUMENT, Q_("Invalid medium type '%s'!"), medium_string);
                g_free(key);
                g_variant_unref(value);
                g_hash_table_unref(writer_parameters);
                return FALSE;
            }
        } else if (g_str_has_prefix(key, "writer.")) {
            /* Create a copy of key and ref the value, because of the
             * generic cleanup below. */
            g_hash_table_insert(writer_parameters, g_strdup(key), g_variant_ref(value));
        }

        g_free(key);
        g_variant_unref(value);
    }

    /* Image writer ID must be provided in parameters */
    if (!writer_id) {
        CDEMU_DEBUG(self, DAEMON_DEBUG_WARNING, "%s: no image writer ID provided in parameters!", __debug__);
        g_set_error(error, CDEMU_ERROR, CDEMU_ERROR_DAEMON_ERROR, Q_("no image writer ID provided in parameters!"));
        g_hash_table_unref(writer_parameters);
        return FALSE;
    }

    /* Create writer and attach our context to it */
    self->priv->image_writer = mirage_create_writer(writer_id, error);
    if (!self->priv->image_writer) {
        g_hash_table_unref(writer_parameters);
        return FALSE;
    }
    mirage_contextual_set_context(MIRAGE_CONTEXTUAL(self->priv->image_writer), self->priv->mirage_context);

    /* Create blank disc */
    self->priv->disc = g_object_new(MIRAGE_TYPE_DISC, NULL);
    mirage_contextual_set_context(MIRAGE_CONTEXTUAL(self->priv->disc), self->priv->mirage_context);

    /* Set filenames */
    mirage_disc_set_filename(self->priv->disc, filename);

    /* Emulate 80-min CD-R or DVD+R SL for now */
    self->priv->recordable_disc = TRUE;
    self->priv->rewritable_disc = FALSE;
    self->priv->disc_closed = FALSE;

    mirage_disc_set_medium_type(self->priv->disc, medium_type);
    self->priv->medium_capacity = medium_capacity;
    if (medium_type == MIRAGE_MEDIUM_CD) {
        self->priv->medium_leadin = -11077;
        mirage_disc_layout_set_start_sector(self->priv->disc, -150);
    } else {
        self->priv->medium_leadin = 0;
        mirage_disc_layout_set_start_sector(self->priv->disc, 0);
    }

    /* Initialize image writer with this disc */
    if (!mirage_writer_open_image(self->priv->image_writer, self->priv->disc, writer_parameters, error)) {
        g_object_unref(self->priv->disc);
        self->priv->disc = NULL;

        g_object_unref(self->priv->image_writer);
        self->priv->image_writer = NULL;

        g_hash_table_unref(writer_parameters);

        return FALSE;
    }
    g_hash_table_unref(writer_parameters);


    self->priv->num_written_sectors = 0;

    self->priv->open_session = NULL;
    self->priv->open_track = NULL;

    /* Loading succeeded */
    self->priv->loaded = TRUE;

    /* Set profile */
    cdemu_device_set_profile(self, profile_index);

    /* Set default recording mode */
    cdemu_device_recording_set_mode(self, 1); /* TAO */

    /* Signal event */
    self->priv->media_event = MEDIA_EVENT_NEW_MEDIA;

    /* Send notification */
    g_signal_emit_by_name(self, "status-changed", NULL);

    return TRUE;
}


gboolean cdemu_device_create_blank_disc (CdemuDevice *self, const gchar *filename, GVariant *options, GError **error)
{
    gboolean succeeded = TRUE;

    /* Create */
    g_mutex_lock(self->priv->device_mutex);
    succeeded = cdemu_device_create_blank_disc_private(self, filename, options, error);
    g_mutex_unlock(self->priv->device_mutex);

    return succeeded;
}


/**********************************************************************\
 *                              Unload disc                           *
\**********************************************************************/
gboolean cdemu_device_unload_disc_private (CdemuDevice *self, GError **error)
{
    /* Check if the door is locked */
    if (self->priv->locked) {
        CDEMU_DEBUG(self, DAEMON_DEBUG_MMC, "%s: device is locked", __debug__);
        g_set_error(error, CDEMU_ERROR, CDEMU_ERROR_DEVICE_LOCKED, Q_("Device is locked!"));
        return FALSE;
    }

    /* Unload only if we're loaded */
    if (self->priv->loaded) {
        /* Delete disc */
        g_object_unref(self->priv->disc);
        self->priv->disc = NULL;

        /* We're not loaded anymore, and media got changed */
        self->priv->loaded = FALSE;
        self->priv->media_event = MEDIA_EVENT_MEDIA_REMOVAL;

        /* Clear burning emulation stuff */
        if (self->priv->open_track) {
            g_object_unref(self->priv->open_track);
            self->priv->open_track = NULL;
        }

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

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

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

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

        self->priv->disc_closed = FALSE;
        self->priv->recordable_disc = FALSE;
        self->priv->rewritable_disc = FALSE;
        self->priv->num_written_sectors = 0;

        if (self->priv->leadin_cdtext_packs) {
            g_slist_free_full(self->priv->leadin_cdtext_packs, (GDestroyNotify)g_free);
            self->priv->leadin_cdtext_packs = NULL;
        }
        self->priv->num_leadin_cdtext_packs = 0;

        /* Current profile: None */
        cdemu_device_set_profile(self, ProfileIndex_NONE);

        /* Send notification */
        g_signal_emit_by_name(self, "status-changed", NULL);
    }

    return TRUE;
}

gboolean cdemu_device_unload_disc (CdemuDevice *self, GError **error)
{
    g_mutex_lock(self->priv->device_mutex);

    /* This call is the equivalent of the user pressing the mechanical
     * switch on the logical unit to eject the disc, so it needs to
     * generate the media eject request event. The actual unload will
     * likely not happen at this point, due to device being locked;
     * instead, HAL/udev/udisksd2 may pick up the request, unlock the
     * device, and proceed with ejection again... */
    self->priv->media_event = MEDIA_EVENT_EJECTREQUEST;

    /* Attempt the actual unload */
    cdemu_device_unload_disc_private(self, error);

    g_mutex_unlock(self->priv->device_mutex);

    /* Ignore the command's return value to avoid bothering the clients
     * with errors when device is most likely locked, and will be
     * unloaded by HAL/udev/udisksd2 upon reception of the above-set
     * eject request event ...*/
    return TRUE;
}
