#include <u.h>
#include <libc.h>
#include <bio.h>
#include <flate.h>
#include <mp.h>
#include <libsec.h>
#include "paqfs.h"

enum {
       OffsetSize = 4, /* size of block offset */
};

void paqfs(char *root, char *label);
PaqDir *paqFile(char *name, Dir *dir);
PaqDir *paqDir(char *name, Dir *dir);
PaqDir *paqDirAlloc(Dir *d, ulong offset);
void paqDirFree(PaqDir *pd);
void writeHeader(char *label);
void writeTrailer(ulong root);
ulong writeBlock(uchar *buf, int type);
void usage(void);
void outWrite(void *buf, int n);
int paqDirSize(PaqDir *dir);
void putDir(uchar *p, PaqDir *dir);
void putHeader(uchar *p, PaqHeader *h);
void putBlock(uchar *p, PaqBlock *h);
void putTrailer(uchar *p, PaqTrailer *t);
void putl(uchar *p, ulong v);
void puts(uchar *p, int x);
uchar *putstr(uchar *p, char *s);
void *emallocz(int size);
void warn(char *fmt, ...);

int uflag=0;                    /* uncompressed */
long blocksize = 4*1024;

Biobuf *out;
DigestState *outdg;

void
main(int argc, char *argv[])
{
       char *s, *ss;
       char *outfile = nil;
       char *label = nil;
       char *file;

       ARGBEGIN {
       case 'u':
               uflag=1;
               break;
       case 'o':
               outfile = ARGF();
               break;
       case 'l':
               label = ARGF();
               if(label == nil)
                       usage();
               break;
       case 'b':
               s = ARGF();
               if(s) {
                       blocksize = strtoul(s, &ss, 0);
                       if(s == ss)
                               usage();
                       if(*ss == 'k')
                               blocksize *= 1024;
               }
               if(blocksize < MinBlockSize)
                       sysfatal("blocksize too small: must be at least %d", MinBlockSize);
               if(blocksize > MaxBlockSize)
                       sysfatal("blocksize too large: must be no greater than %d", MaxBlockSize);
               break;
       } ARGEND

       if(outfile == nil) {
               out = emallocz(sizeof(Biobuf));
               Binit(out, 1, OWRITE);
       } else {
               out = Bopen(outfile, OWRITE|OTRUNC);
               if(out == nil)
                       sysfatal("could not create file: %s: %r", outfile);
       }

       deflateinit();

       file = argv[0];
       if(file == nil)
               file = ".";

       if(label == nil) {
               if(strrchr(file, '/'))
                       label = strrchr(file, '/') + 1;
               else
                       label = file;
       }

       paqfs(file, label);

       Bterm(out);

       exits(0);
}

void
usage(void)
{
       fprint(2, "usage: %s [-u] [-b blocksize] -o output [root]\n", argv0);
       exits("usage");
}

void
paqfs(char *root, char *label)
{
       Dir *dir;
       PaqDir *pd;
       ulong offset;
       uchar *buf;

       dir = dirstat(root);
       if(dir == nil)
               sysfatal("could not stat root: %s: %r", root);
       writeHeader(label);
       if(dir->mode & DMDIR)
               pd = paqDir(root, dir);
       else
               pd = paqFile(root, dir);
       buf = emallocz(blocksize);
       putDir(buf, pd);
       offset = writeBlock(buf, DirBlock);
       writeTrailer(offset);
       paqDirFree(pd);
       free(dir);
}


PaqDir *
paqFile(char *name, Dir *dir)
{
       int fd, n, nn, nb;
       vlong tot;
       uchar *block, *pointer;
       ulong offset;

       fd = open(name, OREAD);
       if(fd < 0) {
               warn("could not open file: %s: %r", name);
               return nil;
       }

       block = emallocz(blocksize);
       pointer = emallocz(blocksize);
       nb = 0;
       n = 0;
       tot = 0;
       for(;;) {
               nn = read(fd, block+n, blocksize-n);
               if(nn < 0) {
                       warn("read failed: %s: %r", name);
                       goto Err;
               }
               tot += nn;
               if(nn == 0) {
                       if(n == 0)
                               break;
                       /* pad out last block */
                       memset(block+n, 0, blocksize-n);
                       nn = blocksize - n;
               }
               n += nn;
               if(n < blocksize)
                       continue;
               if(nb >= blocksize/OffsetSize) {
                       warn("file too big for blocksize: %s", name);
                       goto Err;
               }
               offset = writeBlock(block, DataBlock);
               putl(pointer+nb*OffsetSize, offset);
               nb++;
               n = 0;
       }

       offset = writeBlock(pointer, PointerBlock);

       close(fd);
       free(pointer);
       free(block);
       dir->length = tot;
       return paqDirAlloc(dir, offset);
Err:
       close(fd);
       free(pointer);
       free(block);
       return nil;
}

PaqDir *
paqDir(char *name, Dir *dir)
{
       Dir *dirs, *p;
       PaqDir *pd;
       int i, n, nb, fd, ndir;
       uchar *block, *pointer;
       char *nname;
       ulong offset;

       fd = open(name, OREAD);
       if(fd < 0) {
               warn("could not open directory: %s: %r", name);
               return nil;
       }

       ndir = dirreadall(fd, &dirs);
       close(fd);

       if(ndir < 0) {
               warn("could not read directory: %s: %r", name);
               return nil;
       }

       block = emallocz(blocksize);
       pointer = emallocz(blocksize);
       nb = 0;
       n = 0;
       nname = nil;
       pd = nil;

       for(i=0; i<ndir; i++) {
               p = dirs + i;
               free(nname);
               nname = emallocz(strlen(name) + strlen(p->name) + 2);
               sprint(nname, "%s/%s", name, p->name);
               if(p->mode & DMDIR)
                       pd = paqDir(nname, p);
               else
                       pd = paqFile(nname, p);
               if(pd == nil)
                       continue;

               if(n+paqDirSize(pd) >= blocksize) {

                       /* zero fill the block */
                       memset(block+n, 0, blocksize-n);
                       offset = writeBlock(block, DirBlock);
                       n = 0;
                       if(nb >= blocksize/OffsetSize) {
                               warn("directory too big for blocksize: %s", nname);
                               goto Err;
                       }
                       putl(pointer+nb*OffsetSize, offset);
                       nb++;
               }
               if(n+paqDirSize(pd) >= blocksize) {
                       warn("directory entry does not fit in a block: %s", nname);
                       paqDirFree(pd);
                       continue;
               }
               putDir(block+n, pd);
               n += paqDirSize(pd);
               paqDirFree(pd);
               pd = nil;
       }

       if(n > 0) {
               /* zero fill the block */
               memset(block+n, 0, blocksize-n);
               offset = writeBlock(block, DirBlock);
               if(nb >= blocksize/OffsetSize) {
                       warn("directory too big for blocksize: %s", nname);
                       goto Err;
               }
               putl(pointer+nb*OffsetSize, offset);
       }
       offset = writeBlock(pointer, PointerBlock);

       free(nname);
       free(dirs);
       paqDirFree(pd);
       free(block);
       free(pointer);
       return paqDirAlloc(dir, offset);
Err:
       free(nname);
       free(dirs);
       paqDirFree(pd);
       free(block);
       free(pointer);
       return nil;
}

PaqDir *
paqDirAlloc(Dir *dir, ulong offset)
{
       PaqDir *pd;
       static ulong qid = 1;

       pd = emallocz(sizeof(PaqDir));

       pd->name = strdup(dir->name);
       pd->qid = qid++;
       pd->mode = dir->mode & (DMDIR|DMAPPEND|0777);
       pd->mtime = dir->mtime;
       pd->length = dir->length;
       pd->uid = strdup(dir->uid);
       pd->gid = strdup(dir->gid);
       pd->offset = offset;

       return pd;
}

void
paqDirFree(PaqDir *pd)
{
       if(pd == nil)
               return;
       free(pd->name);
       free(pd->uid);
       free(pd->gid);
       free(pd);
}


void
writeHeader(char *label)
{
       PaqHeader hdr;
       uchar buf[HeaderSize];

       memset(&hdr, 0, sizeof(hdr));
       hdr.magic = HeaderMagic;
       hdr.version = Version;
       hdr.blocksize = blocksize;
       hdr.time = time(nil);
       strncpy(hdr.label, label, sizeof(hdr.label));
       hdr.label[sizeof(hdr.label)-1] = 0;
       putHeader(buf, &hdr);
       outWrite(buf, sizeof(buf));
}

void
writeTrailer(ulong root)
{
       PaqTrailer tlr;
       uchar buf[TrailerSize];

       memset(&tlr, 0, sizeof(tlr));
       tlr.magic = TrailerMagic;
       tlr.root = root;
       putTrailer(buf, &tlr);
       outWrite(buf, sizeof(buf));
}

ulong
writeBlock(uchar *b, int type)
{
       uchar *cb, *ob;
       int n;
       PaqBlock bh;
       uchar buf[BlockSize];
       ulong offset;

       offset = Boffset(out);

       bh.magic = BlockMagic;
       bh.size = blocksize;
       bh.type = type;
       bh.encoding = NoEnc;
       bh.adler32 = adler32(0, b, blocksize);
       ob = b;

       if(!uflag) {
               cb = emallocz(blocksize);
               n = deflateblock(cb, blocksize, b, blocksize, 6, 0);
               if(n > 0 && n < blocksize) {
                       bh.encoding = DeflateEnc;
                       bh.size = n;
                       ob = cb;
               }
       }

       putBlock(buf, &bh);
       outWrite(buf, sizeof(buf));
       outWrite(ob, bh.size);

       if(ob != b)
               free(ob);
       return offset;
}


void
outWrite(void *buf, int n)
{
       if(Bwrite(out, buf, n) < n)
               sysfatal("write failed: %r");
       outdg = sha1((uchar*)buf, n, nil, outdg);
}

int
paqDirSize(PaqDir *d)
{
       return MinDirSize + strlen(d->name) + strlen(d->uid) + strlen(d->gid);
}

void
putHeader(uchar *p, PaqHeader *h)
{
       if(h->blocksize < 65536){
               putl(p, h->magic);
               puts(p+4, h->version);
               puts(p+6, h->blocksize);
       }else{
               assert(h->magic == HeaderMagic);
               puts(p, BigHeaderMagic);
               puts(p+2, h->version);
               putl(p+4, h->blocksize);
       }
       putl(p+8, h->time);
       memmove(p+12, h->label, sizeof(h->label));
}

void
putTrailer(uchar *p, PaqTrailer *h)
{
       putl(p, h->magic);
       putl(p+4, h->root);
       outdg = sha1(p, 8, p+8, outdg);
}

void
putBlock(uchar *p, PaqBlock *b)
{
       if(b->size < 65536){
               putl(p, b->magic);
               puts(p+4, b->size);
       }else{
               assert(b->magic == BlockMagic);
               puts(p, BigBlockMagic);
               putl(p+2, b->size);
       }
       p[6] = b->type;
       p[7] = b->encoding;
       putl(p+8, b->adler32);
}

void
putDir(uchar *p, PaqDir *d)
{
       uchar *q;

       puts(p, paqDirSize(d));
       putl(p+2, d->qid);
       putl(p+6, d->mode);
       putl(p+10, d->mtime);
       putl(p+14, d->length);
       putl(p+18, d->offset);
       q = putstr(p+22, d->name);
       q = putstr(q, d->uid);
       q = putstr(q, d->gid);
       assert(q-p == paqDirSize(d));
}

void
putl(uchar *p, ulong v)
{
       p[0] = v>>24;
       p[1] = v>>16;
       p[2] = v>>8;
       p[3] = v;
}

void
puts(uchar *p, int v)
{
       assert(v < (1<<16));
       p[0] = v>>8;
       p[1] = v;
}

uchar *
putstr(uchar *p, char *s)
{
       int n = strlen(s);
       puts(p, n+2);
       memmove(p+2, s, n);
       return p+2+n;
}


void *
emallocz(int size)
{
       void *p;

       p = malloc(size);
       if(p == nil)
               sysfatal("malloc failed");
       memset(p, 0, size);
       return p;
}

void
warn(char *fmt, ...)
{
       char buf[1024];
       va_list arg;

       va_start(arg, fmt);
       vseprint(buf, buf+sizeof(buf), fmt, arg);
       va_end(arg);
       fprint(2, "%s: %s\n", argv0, buf);
}