/* dinfo.c for !DiscEx */


#include "main.h"
#include "dinfo.h"
#include "discmacros.h"
#include "auxui.h"
#include "setup.h"
#include "alloc.h"
#include "map.h"
#include "client.h"


#define  DISCINFO  "DiscInfo"         /* name of Disc Info dbox template */

#define  EPSILON  1e-12               /* used when converting double values
                                         that are supposed to be integral */
#define  SCSIFS_SectorDiscOp  0x4098d /* should be in "swis.h" ... */


typedef struct _DIList {
    struct _DIList *next;
    int FS;
    int drive;
    int usecount;
    ObjectId dbox;
    DiscInfoRec di;
} DIListRec, *DIListPtr;

static DIListPtr dinfolist = NULL;   /* list of current disc information */


/* component IDs for disc info dialogue box */
#define  ID_discname       0x2f
#define  ID_FS             0x37
#define  ID_drive          0x3f
#define  ID_secsize        0x30
#define  ID_allocsize      0x38
#define  ID_idlen          0x40
#define  ID_secspertrack   0x31
#define  ID_heads          0x39
#define  ID_density        0x41
#define  ID_secsperdisc    0x32
#define  ID_discsize_Mb    0x3a
#define  ID_discsize       0x42
#define  ID_nzones         0x33
#define  ID_zonespare      0x3b
#define  ID_lowsector      0x43
#define  ID_disctype       0x34
#define  ID_discid         0x3c
#define  ID_bootoption     0x44
#define  ID_skew           0x35
#define  ID_idsperzone     0x3d
#define  ID_maxfrags       0x45
#define  ID_rootaddr       0x36
#define  ID_mapaddr        0x3e
#define  ID_zonesize       0x48
#define  ID_zone0size      0x46
#define  ID_lastzonesize   0x4d
#define  ID_riscosdiscsize 0x4a
#define  ID_minfragsize    0x50
#define  ID_allocunit      0x52
#define  ID_shareunit      0x54


/* event codes */
#define  Event_Refresh        9  /* click on "Refresh" button */


/* holds the filename to be used for saving */
static char filename[256] = "discinfo";


/* forward reference */
static Bool dinfo_refresh (int c, ToolboxEvent *e, IdBlock *i, void *h);



/*
 * Called from the saveas_save(..) handler to write out the window's content
 *  to a given open file.
 */

static error * dinfo_print (FILE *f, IdBlock *idblock, void *pp)
{
    DIListPtr p  = (DIListPtr) pp;
    DiscInfoPtr di = &p->di;
    DiscRecordPtr dr = &di->dr;    
    char *ff = "%30s  %10s\n";

    IGNORE(idblock);

    fprintf (f, "Disc record and associated data for %s::%d\n\n",
                    p->FS == FS_ADFS ? "ADFS" : "SCSI", p->drive);

    fprintf(f, "Disc record:\n");
    fprintf(f, ff, "Disc name", di->discname);
    fprintf(f, ff, "Sector size", num(di->secsize));
    fprintf(f, ff, "Allocation unit", num(di->allocsize));
    fprintf(f, ff, "Fragment id len", num(dr->idlen));
    fprintf(f, ff, "Sectors per track", num(dr->secspertrack));
    fprintf(f, ff, "Heads", num(dr->heads));
    fprintf(f, ff, "Density", trans_density(dr->density));
    fprintf(f, ff, "Disc size (sectors)", num(di->secsperdisc));
    fprintf(f, ff, "Disc size (Mb)",
                 num((unsigned)((di->discsize+512.0*1024.0)/1048576.0)));
    fprintf(f, ff, "Disc size (bytes)", numdf(di->discsize));
    fprintf(f, ff, "Number of zones", num(dr->nzones));
    fprintf(f, ff, "Zone spare bits", num(di->zonespare));
    fprintf(f, ff, "Low sector", num(dr->lowsector));
    fprintf(f, ff, "Disc type", numh3(dr->disctype));
    fprintf(f, ff, "Disc cycle id", num(di->discid));
    fprintf(f, ff, "Boot option", num(dr->bootoption));
    fprintf(f, ff, "Skew", num(dr->skew));
    fprintf(f, ff, "Ids per zone", num(di->idsperzone));
    fprintf(f, ff, "Max fragments", num(di->maxfrags));
    fprintf(f, ff, "Root directory address", numh8(dr->root));
    fprintf(f, ff, "Map address", num(di->mapaddr));

    fprintf(f, "\n");
    fprintf(f, ff, "RISC OS disc size (sectors)", num(di->riscosdiscsize));

    fprintf(f, "\nZone sizes in sectors:\n");
    fprintf(f, ff, "First zone",
             p->drive < 4 ? "" : num(SECS_IN_ALLOCS (di, di->zone0size)));
    fprintf(f, ff, "Middle zones",
             p->drive < 4 ? "" : num(SECS_IN_ALLOCS (di, di->zonesize)));
    fprintf(f, ff, "Last zone",
             p->drive < 4 ? "" : num(SECS_IN_ALLOCS (di, di->lastzonesize)));

    fprintf(f, "\nFragment parameters in sectors:\n");
    fprintf(f, ff, "Minimum size", num(di->minfragsize));
    fprintf(f, ff, "Minimum increment", num(di->allocunit));
    fprintf(f, ff, "Unit of sharing", num(di->shareunit));

    return NULL;
}


/*
 * Called by the saveas handlers to discover where the filename for the
 *  data to be saved is stored.
 *
 * Note that all disc information windows share the same file name for
 *  storing data.
 */

static char * dinfo_saveas_filename (IdBlock *idblock, void *pp)
{
    IGNORE(idblock);
    IGNORE(pp);

    return filename;
}


/*
 * Callback function used by dinfo_readdiscrec(..) when scanning the last
 *  map block to find out where it really ends.
 */

static error * trap_last_frag
(
    DiscInfoPtr di,
    int zone,
    int fragid,
    int pos,
    int len,
    void *handle
)
{
    int *trap = (int *) handle;

    IGNORE(di);
    IGNORE(zone);

    trap[0] = fragid;
    trap[1] = pos;
    trap[2] = len;

    return NULL;
}


/*
 * Read the disc record for the FS/drive defined by p, and complete the
 *  disc information record p->info.
 */

static error * dinfo_read_disc_rec (DIListPtr p)
{
    char *buff = NULL;
    DiscInfoPtr di = &p->di;
    DiscRecordPtr dr = &di->dr;

    /* set up DiscOp SWI according to FS */
    switch (p->FS)
    {
    case FS_ADFS:
        di->FS_DiscOp = ADFS_DiscOp;
        di->FS_BigDiscOp = ADFS_SectorDiscOp;
        break;

    case FS_SCSIFS:
        di->FS_DiscOp = SCSIFS_DiscOp;
        di->FS_BigDiscOp = SCSIFS_SectorDiscOp;
        break;
    }

    /* determine address of map, sector size and whether it's "big" */
    if (p->drive < 4)           /* floppy */
    {
        di->mapaddr = 0;    /* assume E-format floppy for now */
        di->secsize = 1024;
        di->bigdisc = FALSE;
    }
    else                     /* hard disc */
    {
        DiscRecordPtr bdr;

        /* allocate space to read in boot block */
        ER ( check_alloc ((void **)&buff, 512) );

        /* read boot block, and hence base disc record */
        ER ( _swix (di->FS_DiscOp, I1|I2|I3|I4,
                         1,                     /* read sectors */
                         (p->drive << 29) + 0xc00,
                         (int) buff,
                         512) );

        /* disc record is at offset &1c0 */
        bdr = (DiscRecordPtr) (buff + 0x1c0);
         
        /* determine address of map in sectors */
        {
            int secshift = bdr->log2secsize;
            int allocshift = bdr->log2bpmb;
            int numspare = (bdr->zonespare[1] * 256 + bdr->zonespare[0]);
            int zonesize = ((1 << secshift) * 8 - numspare);
            int zone0size = zonesize - 60 * 8;
            int mapzone = (bdr->nzones) / 2;

            di->mapaddr = zone0size + (mapzone - 1) * zonesize;

            /* convert to sectors from allocation units */
            if (allocshift > secshift)
                di->mapaddr <<= (allocshift - secshift);
            else
                di->mapaddr >>= (secshift - allocshift);

            di->secsize = 1 << secshift;
        }

        /* determine whether this is a "big disc" or not */
        di->bigdisc = (bdr->discsize2 != 0) ||
                                    (bdr->discsize >> 29) != 0;

        /* the following value is needed for the next read below ... */
        dr->log2secsize = bdr->log2secsize;

        free(buff);
    }

    /* allocate buffer for first map block */
    ER ( check_alloc ((void **)&buff, di->secsize) );

    /* read first map block */
    ER ( _swix (di->bigdisc ? di->FS_BigDiscOp : di->FS_DiscOp, I1|I2|I3|I4,
                    1,                           /* read sectors */
                    (p->drive << 29) +
                          (di->bigdisc ? di->mapaddr :
                                         di->mapaddr << dr->log2secsize),
                    (int) buff,
                    di->secsize) );

    /* copy real disc record into disc info record */
    *dr = * (DiscRecordPtr) (buff + 4);

    /* construct derived values */
    di->zonespare = dr->zonespare[1] * 256 + dr->zonespare[0];
    di->discsize = (double) (dr->discsize) +
                   (double) (dr->discsize2) * (double) (1 << 30) * 4.0;
    di->bigdisc = (di->discsize - (double) (1 << 29)) >= 0.0;
    di-> discid = dr->discid[1] * 256 + dr->discid[0];
    strncpy (di->discname, dr->discname, 10);
    di->discname[10] = 0;  /* just in case */

    di->secsize = 1 << dr->log2secsize;
    di->allocsize = 1 << dr->log2bpmb;
    di->secsperdisc = (unsigned) (di->discsize/di->secsize + EPSILON);
    di->idsperzone = (di->secsize * 8 - di->zonespare) / (dr->idlen + 1);
    di->maxfrags = dr->nzones * di->idsperzone;

    di->zonesize = di->secsize * 8 - di->zonespare;
    di->zone0size = di->zonesize - 60 * 8;

    /* determine disc size, and the useful end of the final map block */
    if (p->drive < 4)  /* set explicitly for floppy disc (assume E-format) */
        di->riscosdiscsize = 800;    /* 800 1k sectors */
    else             /* must examine last map block for hard disc */
    {
        int trap[3];
        int lastmap = di->mapaddr + (dr->nzones - 1);

        /* read last map block */
        ER ( _swix (di->bigdisc ? di->FS_BigDiscOp : di->FS_DiscOp,
                      I1|I2|I3|I4,
                      1,
                      (p->drive << 29) +
                        (di->bigdisc ? lastmap : lastmap << dr->log2secsize),
                      (int) buff,
                      di->secsize) );

        /* scan this map block - just to record the final fragment, if any */
        di->lastzonemapend = di->secsize * 8;
        ER ( map_scan_block (di, buff, dr->nzones - 1,
                             trap_last_frag, (void *) trap) );
          /* now have trap[] = {fragid, pos, len} for last fragment in map */

        if (trap[0] == 1)
            di->lastzonemapend -= trap[2];

        di->lastzonesize = di->lastzonemapend - (4 * 8);
        di->riscosdiscsize = di->zone0size +
                                 (dr->nzones - 2) * di->zonesize +
                                 di->lastzonesize;
        di->riscosdiscsize = SECS_IN_ALLOCS (di, di->riscosdiscsize);
    }

    /* calculate fragment parameters */
    di->minfragsize = SECS_FOR (di, (dr->idlen + 1) << dr->log2bpmb);
    di->allocunit = SECS_FOR (di, 1 << dr->log2bpmb);
    di->shareunit = 1 << dr->shareshift;

    free (buff);

    return NULL;
}


/*
 * Update the fields of the dbox p->dbox according to the information in the
 *  disc information record p->info.
 */

static error * dinfo_update_dbox (DIListPtr p)
{
    ObjectId w = p->dbox;
    DiscInfoPtr di = &p->di;
    DiscRecordPtr dr = &di->dr;

    displayfield_set_value (0, w, ID_discname, di->discname);
    displayfield_set_value (0, w, ID_FS,
                                     (p->FS == FS_ADFS) ? "ADFS" : "SCSIFS");
    displayfield_set_value (0, w, ID_drive, num(p->drive));
    displayfield_set_value (0, w, ID_secsize, num(di->secsize));
    displayfield_set_value (0, w, ID_allocsize, num(di->allocsize));
    displayfield_set_value (0, w, ID_idlen, num(dr->idlen));
    displayfield_set_value (0, w, ID_secspertrack, num(dr->secspertrack));
    displayfield_set_value (0, w, ID_heads, num(dr->heads));
    displayfield_set_value (0, w, ID_density, trans_density(dr->density));
    displayfield_set_value (0, w, ID_secsperdisc, num(di->secsperdisc));
    displayfield_set_value (0, w, ID_discsize_Mb,
                     num((unsigned)((di->discsize+512.0*1024.0)/1048576.0)));
    displayfield_set_value (0, w, ID_discsize, numdf(di->discsize));
    displayfield_set_value (0, w, ID_nzones, num(dr->nzones));
    displayfield_set_value (0, w, ID_zonespare, num(di->zonespare));
    displayfield_set_value (0, w, ID_lowsector, num(dr->lowsector));
    displayfield_set_value (0, w, ID_disctype, numh3(dr->disctype));
    displayfield_set_value (0, w, ID_discid, num(di->discid));
    displayfield_set_value (0, w, ID_bootoption, num(dr->bootoption));
    displayfield_set_value (0, w, ID_skew, num(dr->skew));
    displayfield_set_value (0, w, ID_idsperzone, num(di->idsperzone));
    displayfield_set_value (0, w, ID_maxfrags, num(di->maxfrags));
    displayfield_set_value (0, w, ID_rootaddr, numh8(dr->root));
    displayfield_set_value (0, w, ID_mapaddr, num(di->mapaddr));

    displayfield_set_value (0, w, ID_zonesize,
             p->drive < 4 ? "" : num(SECS_IN_ALLOCS (di, di->zonesize)));
    displayfield_set_value (0, w, ID_zone0size,
             p->drive < 4 ? "" : num(SECS_IN_ALLOCS (di, di->zone0size)));
    displayfield_set_value (0, w, ID_lastzonesize,
             p->drive < 4 ? "" : num(SECS_IN_ALLOCS (di, di->lastzonesize)));
    displayfield_set_value (0, w, ID_riscosdiscsize,
                                        num(di->riscosdiscsize));

    gadget_fade (w, ID_zonesize, p->drive < 4);
    gadget_fade (w, ID_zone0size, p->drive < 4);
    gadget_fade (w, ID_lastzonesize, p->drive < 4);

    displayfield_set_value (0, w, ID_minfragsize, num(di->minfragsize));
    displayfield_set_value (0, w, ID_allocunit, num(di->allocunit));
    displayfield_set_value (0, w, ID_shareunit, num(di->shareunit));

    return NULL;
}


/*
 * Scan list of known disc information records to find one for FS/drive;
 *  returns NULL if not found.
 */

static DIListPtr dinfo_find_drive_rec (int FS, int drive)
{
    DIListPtr p = dinfolist;

    while (p)
    {
        if (p->FS == FS && p->drive == drive)
            return p;
        p = p->next;
    }

    return NULL;
}


/*
 * Create and add an initialised disc information record for FS/drive to the
 *  list of known disc information records.
 */

static error * dinfo_create_drive_rec (DIListPtr *p, int FS, int drive)
{
    /* allocate space for a new one */
    ER (check_alloc ((void **)p, sizeof (DIListRec)));

    /* initialise it, and add it to the front of the list */
    (*p)->next = dinfolist;
    dinfolist = *p;

    (*p)->FS = FS;
    (*p)->drive = drive;
    (*p)->usecount = 0;
    (*p)->dbox = 0;

    return NULL;
}


/*
 * Delink and free drive information record.
 */

static error * dinfo_free_drive_rec (DIListPtr p)
{
    DIListPtr prev = dinfolist;

    /* check no dbox is present */
    if (p->dbox)
        return error_lookup ("BadFreeDI");

    /* delink */
    if (p == dinfolist)
        dinfolist = p->next;
    else
    {
        while (prev->next != p)
            prev = prev->next;
        prev->next = p->next;
    }

    /* free */
    free (p);

    return NULL;
}


/*
 * Called when window is about to be hidden:
 *  - after user clicks "Cancel" button
 *  - after user clicks close icon
 */

static Bool dinfo_delete
(
    int code,
    ToolboxEvent *event,
    IdBlock *idblock,
    void *handle
)
{
    DIListPtr p = (DIListPtr) handle;

    IGNORE(code);
    IGNORE(event);
    IGNORE(idblock);

    /* deregister handlers */
    EDG (ret, event_deregister_toolbox_handler
              (
                  p->dbox,
                  Window_HasBeenHidden,
                  dinfo_delete,
                  handle
              ));
    EDG (ret, event_deregister_toolbox_handler
              (
                  p->dbox,
                  Event_Refresh,
                  dinfo_refresh,
                  handle
              ));

    /* free space occupied by ClientHandleRec */
    {
        void *h;

        EDG (ret, toolbox_get_client_handle (0, p->dbox, &h));
        free (h);
    }

    /* delete the dialogue box */
    EDG (ret, toolbox_delete_object (0, p->dbox));

    /* and note dbox no longer exists */
    p->dbox = 0;

    /* reduce usecount */
    p->usecount--;

    /* and free DIListRec if no longer in use */
    if (p->usecount == 0)
        EDG (ret, dinfo_free_drive_rec (p));

  ret:
    return TRUE;
}


/*
 * Called when the user clicks on "Refresh" button.
 */

static Bool dinfo_refresh
(
    int code,
    ToolboxEvent *event,
    IdBlock *idblock,
    void *handle
)
{
    DIListPtr p = (DIListPtr) handle;

    IGNORE(code);
    IGNORE(event);
    IGNORE(idblock);

    EDG (ret, dinfo_read_disc_rec (p));
    EDG (ret, dinfo_update_dbox (p));

  ret:
    return TRUE;
}


/*
 * Called when the user chooses "Disc info..." from the icon bar icon menu.
 */

Bool dinfo_request
(
    int code,
    ToolboxEvent *event,
    IdBlock *idblock,
    void *handle
)
{
    int FS = setup_FS ();
    int drive = setup_drive ();
    DIListPtr p = dinfo_find_drive_rec (FS, drive);

    IGNORE(code);
    IGNORE(event);
    IGNORE(idblock);
    IGNORE(handle);


    /* create new DIListRec if one not already found */
    if (!p)
        EDG (ret, dinfo_create_drive_rec (&p, FS, drive));

    /* if dbox does not already exist, create it */
    if (!p->dbox)
    {
        /* create new object */
        EDG (ret, toolbox_create_object (0, DISCINFO, &p->dbox));

        /* create client handle, initialise and attach it */
        {
            ClientHandlePtr h;

            EDG (ret, check_alloc ((void **)&h, sizeof (ClientHandleRec)));

            h->p = (void *) p;
            h->pf = dinfo_print;
            h->filename = dinfo_saveas_filename;

            EDG (ret, toolbox_set_client_handle (0, p->dbox, (void *) h));
        }

        /* increase use count */
        p->usecount++;

        /* register handlers */

        /* Click on "Cancel" button or close icon */
        EDG (ret, event_register_toolbox_handler
                  (
                      p->dbox,
                      Window_HasBeenHidden,
                      dinfo_delete,
                      (void *) p
                  ));

        /* Click on "Refresh" button */
        EDG (ret, event_register_toolbox_handler
                  (
                      p->dbox,
                      Event_Refresh,
                      dinfo_refresh,
                      (void *) p
                  ));
    }
    
    /* re-cache disc information */
    EDG (ret, dinfo_read_disc_rec (p));

    /* update the contents of the dbox */
    EDG (ret, dinfo_update_dbox (p));

    /* show the dbox (perhaps just bring to the front) */
    EDG (ret, toolbox_show_object (0, p->dbox, 0, 0, 0, 0));

  ret:
    return TRUE;
}


/*
 * Returns (via *di) a pointer to information about the specified drive;
 *  this information remains valid until the corresponding call of
 *  dinfo_freeinfo (info).
 *
 * If 'refresh' is TRUE, the disc record will be re-read even if information
 *  is already available.
 */
 
error * dinfo_getinfo
(
    int FS,
    int drive,
    Bool refresh,
    DiscInfoPtr *di
)
{
    DIListPtr p = dinfo_find_drive_rec (FS, drive);

    if (!p)
    {
        EDG (ret, dinfo_create_drive_rec (&p, FS, drive));
        refresh = TRUE;
    }

    /* (re)read disc record if necessary */
    if (refresh)
        EDG (ret, dinfo_read_disc_rec (p));

    /* increase usecount for this disc information node */
    p->usecount++;

    *di = &p->di;

  ret:
    return NULL;
}


/*
 * Called to note that the information referenced by 'di' is no longer
 *  required.
 */

error * dinfo_freeinfo
(
    DiscInfoPtr di
)
{
    DIListPtr p = dinfolist;

    /* look for corresponding DIListRec */
    while (p)
    {
        if (&p->di == di)
        {
            /* decrease usecount */
            p->usecount--;

            /* de-link record and free if no one is using it */
            if (p->usecount == 0)
                return dinfo_free_drive_rec (p);
            else
                return NULL;
        }

        p = p->next;
    }

    return error_lookup ("BadFreeInfo");
}
