/* cdfs - CD, DVD and BD reader and writer file system */
#include <u.h>
#include <libc.h>
#include <auth.h>
#include <fcall.h>
#include <thread.h>
#include <9p.h>
#include <disk.h>
#include "dat.h"
#include "fns.h"

typedef struct Aux Aux;
struct Aux {
       int     doff;
       Otrack  *o;
};

ulong   getnwa(Drive *);

static void checktoc(Drive*);

int vflag;

static Drive *drive;

enum {
       Qdir = 0,
       Qctl = 1,
       Qwa = 2,
       Qwd = 3,
       Qtrack = 4,
};

char*
geterrstr(void)
{
       static char errbuf[ERRMAX];

       rerrstr(errbuf, sizeof errbuf);
       return errbuf;
}

void*
emalloc(ulong sz)
{
       void *v;

       v = mallocz(sz, 1);
       if(v == nil)
               sysfatal("malloc %lud fails", sz);
       return v;
}

static void
fsattach(Req *r)
{
       char *spec;

       spec = r->ifcall.aname;
       if(spec && spec[0]) {
               respond(r, "invalid attach specifier");
               return;
       }

       checktoc(drive);
       r->fid->qid = (Qid){Qdir, drive->nchange, QTDIR};
       r->ofcall.qid = r->fid->qid;
       r->fid->aux = emalloc(sizeof(Aux));
       respond(r, nil);
}

static char*
fsclone(Fid *old, Fid *new)
{
       Aux *na;

       na = emalloc(sizeof(Aux));
       *na = *((Aux*)old->aux);
       if(na->o)
               na->o->nref++;
       new->aux = na;
       return nil;
}

static char*
fswalk1(Fid *fid, char *name, Qid *qid)
{
       int i;

       checktoc(drive);
       switch((ulong)fid->qid.path) {
       case Qdir:
               if(strcmp(name, "..") == 0) {
                       *qid = (Qid){Qdir, drive->nchange, QTDIR};
                       return nil;
               }
               if(strcmp(name, "ctl") == 0) {
                       *qid = (Qid){Qctl, 0, 0};
                       return nil;
               }
               if(strcmp(name, "wa") == 0 && drive->writeok &&
                   (drive->mmctype == Mmcnone ||
                    drive->mmctype == Mmccd)) {
                       *qid = (Qid){Qwa, drive->nchange, QTDIR};
                       return nil;
               }
               if(strcmp(name, "wd") == 0 && drive->writeok) {
                       *qid = (Qid){Qwd, drive->nchange, QTDIR};
                       return nil;
               }
               for(i=0; i<drive->ntrack; i++)
                       if(strcmp(drive->track[i].name, name) == 0)
                               break;
               if(i == drive->ntrack)
                       return "file not found";
               *qid = (Qid){Qtrack+i, 0, 0};
               return nil;

       case Qwa:
       case Qwd:
               if(strcmp(name, "..") == 0) {
                       *qid = (Qid){Qdir, drive->nchange, QTDIR};
                       return nil;
               }
               return "file not found";
       default:        /* bug: lib9p could handle this */
               return "walk in non-directory";
       }
}

static void
fscreate(Req *r)
{
       int omode, type;
       Otrack *o;
       Fid *fid;

       fid = r->fid;
       omode = r->ifcall.mode;

       if(omode != OWRITE) {
               respond(r, "bad mode (use OWRITE)");
               return;
       }

       switch((ulong)fid->qid.path) {
       case Qdir:
       default:
               respond(r, "permission denied");
               return;

       case Qwa:
               if (drive->mmctype != Mmcnone &&
                   drive->mmctype != Mmccd) {
                       respond(r, "audio supported only on cd");
                       return;
               }
               type = TypeAudio;
               break;

       case Qwd:
               type = TypeData;
               break;
       }

       if((drive->cap & Cwrite) == 0) {
               respond(r, "drive does not write");
               return;
       }

       o = drive->create(drive, type);
       if(o == nil) {
               respond(r, geterrstr());
               return;
       }
       drive->nchange = -1;
       checktoc(drive);        /* update directory info */
       o->nref = 1;
       ((Aux*)fid->aux)->o = o;

       fid->qid = (Qid){Qtrack+(o->track - drive->track), drive->nchange, 0};
       r->ofcall.qid = fid->qid;
       respond(r, nil);
}

static void
fsremove(Req *r)
{
       switch((ulong)r->fid->qid.path){
       case Qwa:
       case Qwd:
               if(drive->fixate(drive) < 0)
                       respond(r, geterrstr());
// let us see if it can figure this out:        drive->writeok = No;
               else
                       respond(r, nil);
               checktoc(drive);
               break;
       default:
               respond(r, "permission denied");
               break;
       }
}

/* result is one word, so it can be used as a uid in Dir structs */
char *
disctype(Drive *drive)
{
       char *type, *rw;

       switch (drive->mmctype) {
       case Mmccd:
               type = "cd-";
               break;
       case Mmcdvdminus:
       case Mmcdvdplus:
               type = drive->dvdtype;
               break;
       case Mmcbd:
               type = "bd-";
               break;
       case Mmcnone:
               type = "no-disc";
               break;
       default:
               type = "**GOK**";               /* traditional */
               break;
       }
       rw = "";
       if (drive->mmctype != Mmcnone && drive->dvdtype == nil)
               if (drive->erasable == Yes)
                       rw = drive->mmctype == Mmcbd? "re": "rw";
               else if (drive->recordable == Yes)
                       rw = "r";
               else
                       rw = "rom";
       return smprint("%s%s", type, rw);
}

int
fillstat(ulong qid, Dir *d)
{
       char *ty;
       Track *t;
       static char buf[32];

       nulldir(d);
       d->type = L'M';
       d->dev = 1;
       d->length = 0;
       ty = disctype(drive);
       strncpy(buf, ty, sizeof buf);
       free(ty);
       d->uid = d->gid = buf;
       d->muid = "";
       d->qid = (Qid){qid, drive->nchange, 0};
       d->atime = time(0);
       d->mtime = drive->changetime;

       switch(qid){
       case Qdir:
               d->name = "/";
               d->qid.type = QTDIR;
               d->mode = DMDIR|0777;
               break;

       case Qctl:
               d->name = "ctl";
               d->mode = 0666;
               break;

       case Qwa:
               if(drive->writeok == No ||
                   drive->mmctype != Mmcnone &&
                   drive->mmctype != Mmccd)
                       return 0;
               d->name = "wa";
               d->qid.type = QTDIR;
               d->mode = DMDIR|0777;
               break;

       case Qwd:
               if(drive->writeok == No)
                       return 0;
               d->name = "wd";
               d->qid.type = QTDIR;
               d->mode = DMDIR|0777;
               break;

       default:
               if(qid-Qtrack >= drive->ntrack)
                       return 0;
               t = &drive->track[qid-Qtrack];
               if(strcmp(t->name, "") == 0)
                       return 0;
               d->name = t->name;
               d->mode = t->mode;
               d->length = t->size;
               break;
       }
       return 1;
}

static ulong
cddb_sum(int n)
{
       int ret;
       ret = 0;
       while(n > 0) {
               ret += n%10;
               n /= 10;
       }
       return ret;
}

static ulong
diskid(Drive *d)
{
       int i, n;
       ulong tmp;
       Msf *ms, *me;

       n = 0;
       for(i=0; i < d->ntrack; i++)
               n += cddb_sum(d->track[i].mbeg.m*60+d->track[i].mbeg.s);

       ms = &d->track[0].mbeg;
       me = &d->track[d->ntrack].mbeg;
       tmp = (me->m*60+me->s) - (ms->m*60+ms->s);

       /*
        * the spec says n%0xFF rather than n&0xFF.  it's unclear which is
        * correct.  most CDs are in the database under both entries.
        */
       return ((n % 0xFF) << 24 | (tmp << 8) | d->ntrack);
}

static void
readctl(Req *r)
{
       int i, isaudio;
       ulong nwa;
       char *p, *e, *ty;
       char s[1024];
       Msf *m;

       isaudio = 0;
       for(i=0; i<drive->ntrack; i++)
               if(drive->track[i].type == TypeAudio)
                       isaudio = 1;

       p = s;
       e = s + sizeof s;
       *p = '\0';
       if(isaudio){
               p = seprint(p, e, "aux/cddb query %8.8lux %d", diskid(drive),
                       drive->ntrack);
               for(i=0; i<drive->ntrack; i++){
                       m = &drive->track[i].mbeg;
                       p = seprint(p, e, " %d", (m->m*60 + m->s)*75 + m->f);
               }
               m = &drive->track[drive->ntrack].mbeg;
               p = seprint(p, e, " %d\n", m->m*60 + m->s);
       }

       if(drive->readspeed == drive->writespeed)
               p = seprint(p, e, "speed %d\n", drive->readspeed);
       else
               p = seprint(p, e, "speed read %d write %d\n",
                       drive->readspeed, drive->writespeed);
       p = seprint(p, e, "maxspeed read %d write %d\n",
               drive->maxreadspeed, drive->maxwritespeed);

       if (drive->Scsi.changetime != 0 && drive->ntrack != 0) { /* have disc? */
               ty = disctype(drive);
               p = seprint(p, e, "%s", ty);
               free(ty);
               if (drive->mmctype != Mmcnone) {
                       nwa = getnwa(drive);
                       p = seprint(p, e, " next writable sector ");
                       if (nwa == ~0ul)
                               p = seprint(p, e, "none; disc full");
                       else
                               p = seprint(p, e, "%lud", nwa);
               }
               seprint(p, e, "\n");
       }
       readstr(r, s);
}

static void
fsread(Req *r)
{
       int j, n, m;
       uchar *p, *ep;
       Dir d;
       Fid *fid;
       Otrack *o;
       vlong offset;
       void *buf;
       long count;
       Aux *a;

       fid = r->fid;
       offset = r->ifcall.offset;
       buf = r->ofcall.data;
       count = r->ifcall.count;

       switch((ulong)fid->qid.path) {
       case Qdir:
               checktoc(drive);
               p = buf;
               ep = p+count;
               m = Qtrack+drive->ntrack;
               a = fid->aux;
               if(offset == 0)
                       a->doff = 1;    /* skip root */

               for(j=a->doff; j<m; j++) {
                       if(fillstat(j, &d)) {
                               if((n = convD2M(&d, p, ep-p)) <= BIT16SZ)
                                       break;
                               p += n;
                       }
               }
               a->doff = j;

               r->ofcall.count = p - (uchar*)buf;
               break;
       case Qwa:
       case Qwd:
               r->ofcall.count = 0;
               break;
       case Qctl:
               readctl(r);
               break;
       default:
               /* a disk track; we can only call read for whole blocks */
               o = ((Aux*)fid->aux)->o;
               if((count = o->drive->read(o, buf, count, offset)) < 0) {
                       respond(r, geterrstr());
                       return;
               }
               r->ofcall.count = count;
               break;
       }
       respond(r, nil);
}

static char Ebadmsg[] = "bad cdfs control message";

static char*
writectl(void *v, long count)
{
       char buf[256];
       char *f[10], *p;
       int i, nf, n, r, w, what;

       if(count >= sizeof(buf))
               count = sizeof(buf)-1;
       memmove(buf, v, count);
       buf[count] = '\0';

       nf = tokenize(buf, f, nelem(f));
       if(nf == 0)
               return Ebadmsg;

       if(strcmp(f[0], "speed") == 0){
               what = 0;
               r = w = -1;
               if(nf == 1)
                       return Ebadmsg;
               for(i=1; i<nf; i++){
                       if(strcmp(f[i], "read") == 0 || strcmp(f[i], "write") == 0){
                               if(what!=0 && what!='?')
                                       return Ebadmsg;
                               what = f[i][0];
                       }else{
                               if (strcmp(f[i], "best") == 0)
                                       n = (1<<16) - 1;
                               else {
                                       n = strtol(f[i], &p, 0);
                                       if(*p != '\0' || n <= 0)
                                               return Ebadmsg;
                               }
                               switch(what){
                               case 0:
                                       if(r >= 0 || w >= 0)
                                               return Ebadmsg;
                                       r = w = n;
                                       break;
                               case 'r':
                                       if(r >= 0)
                                               return Ebadmsg;
                                       r = n;
                                       break;
                               case 'w':
                                       if(w >= 0)
                                               return Ebadmsg;
                                       w = n;
                                       break;
                               default:
                                       return Ebadmsg;
                               }
                               what = '?';
                       }
               }
               if(what != '?')
                       return Ebadmsg;
               return drive->setspeed(drive, r, w);
       }
       return drive->ctl(drive, nf, f);
}

static void
fswrite(Req *r)
{
       Otrack *o;
       Fid *fid;

       fid = r->fid;
       r->ofcall.count = r->ifcall.count;
       if(fid->qid.path == Qctl) {
               respond(r, writectl(r->ifcall.data, r->ifcall.count));
               return;
       }

       if((o = ((Aux*)fid->aux)->o) == nil || o->omode != OWRITE) {
               respond(r, "permission denied");
               return;
       }

       if(o->drive->write(o, r->ifcall.data, r->ifcall.count) < 0)
               respond(r, geterrstr());
       else
               respond(r, nil);
}

static void
fsstat(Req *r)
{
       fillstat((ulong)r->fid->qid.path, &r->d);
       r->d.name = estrdup9p(r->d.name);
       r->d.uid = estrdup9p(r->d.uid);
       r->d.gid = estrdup9p(r->d.gid);
       r->d.muid = estrdup9p(r->d.muid);
       respond(r, nil);
}

static void
fsopen(Req *r)
{
       int omode;
       Fid *fid;
       Otrack *o;

       fid = r->fid;
       omode = r->ifcall.mode;
       checktoc(drive);
       r->ofcall.qid = (Qid){fid->qid.path, drive->nchange, fid->qid.vers};

       switch((ulong)fid->qid.path){
       case Qdir:
       case Qwa:
       case Qwd:
               if(omode != OREAD) {
                       respond(r, "permission denied");
                       return;
               }
               break;
       case Qctl:
               if(omode & ~(OTRUNC|OREAD|OWRITE|ORDWR)) {
                       respond(r, "permission denied");
                       return;
               }
               break;
       default:
               if(fid->qid.path >= Qtrack+drive->ntrack) {
                       respond(r, "file no longer exists");
                       return;
               }

               /*
                * allow the open with OWRITE or ORDWR if the
                * drive and disc are both capable?
                */
               if(omode != OREAD ||
                   (o = drive->openrd(drive, fid->qid.path-Qtrack)) == nil) {
                       respond(r, "permission denied");
                       return;
               }

               o->nref = 1;
               ((Aux*)fid->aux)->o = o;
               break;
       }
       respond(r, nil);
}

static void
fsdestroyfid(Fid *fid)
{
       Aux *aux;
       Otrack *o;

       aux = fid->aux;
       if(aux == nil)
               return;
       o = aux->o;
       if(o && --o->nref == 0) {
               bterm(o->buf);
               drive->close(o);
               checktoc(drive);
       }
}

static void
checktoc(Drive *drive)
{
       int i;
       Track *t;

       drive->gettoc(drive);
       if(drive->nameok)
               return;

       for(i=0; i<drive->ntrack; i++) {
               t = &drive->track[i];
               if(t->size == 0)        /* being created */
                       t->mode = 0;
               else
                       t->mode = 0444;
               sprint(t->name, "?%.3d", i);
               switch(t->type){
               case TypeNone:
                       t->name[0] = 'u';
//                      t->mode = 0;
                       break;
               case TypeData:
                       t->name[0] = 'd';
                       break;
               case TypeAudio:
                       t->name[0] = 'a';
                       break;
               case TypeBlank:
                       t->name[0] = '\0';
                       break;
               default:
                       print("unknown track type %d\n", t->type);
                       break;
               }
       }

       drive->nameok = 1;
}

long
bufread(Otrack *t, void *v, long n, vlong off)
{
       return bread(t->buf, v, n, off);
}

long
bufwrite(Otrack *t, void *v, long n)
{
       return bwrite(t->buf, v, n);
}

Srv fs = {
attach= fsattach,
destroyfid=     fsdestroyfid,
clone=          fsclone,
walk1=          fswalk1,
open=           fsopen,
read=           fsread,
write=          fswrite,
create= fscreate,
remove= fsremove,
stat=           fsstat,
};

void
usage(void)
{
       fprint(2, "usage: cdfs [-Dv] [-d /dev/sdC0] [-m mtpt]\n");
       exits("usage");
}

void
main(int argc, char **argv)
{
       Scsi *s;
       int fd;
       char *dev, *mtpt;

       dev = "/dev/sdD0";
       mtpt = "/mnt/cd";

       ARGBEGIN{
       case 'D':
               chatty9p++;
               break;
       case 'd':
               dev = EARGF(usage());
               break;
       case 'm':
               mtpt = EARGF(usage());
               break;
       case 'v':
               if((fd = create("/tmp/cdfs.log", OWRITE, 0666)) >= 0) {
                       dup(fd, 2);
                       dup(fd, 1);
                       if(fd != 1 && fd != 2)
                               close(fd);
                       vflag++;
                       scsiverbose = 2; /* verbose but no Readtoc errs */
               }
               break;
       default:
               usage();
       }ARGEND

       if(dev == nil || mtpt == nil || argc > 0)
               usage();

       werrstr("");
       if((s = openscsi(dev)) == nil)
               sysfatal("openscsi '%s': %r", dev);
       if((drive = mmcprobe(s)) == nil)
               sysfatal("mmcprobe '%s': %r", dev);
       checktoc(drive);

       postmountsrv(&fs, nil, mtpt, MREPL|MCREATE);
       exits(nil);
}