/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package de.nplusc.izc.izformats;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

/**
 *
 * @author LH
 */
public class IZPackage
{
    
    static final byte[] magicHeader = new byte[]{(byte)0x00, (byte)0x14, (byte)0x19, (byte)0x40, (byte)0xFA ,(byte)0xC4 ,(byte)0x0A};
    public static final int CHUNK_SIZE=4096;
    //Formatspezifikation
    //magicHEader=0x00 00 14 19 40 FA C4 0A
    //länge desy eigentlichen headers in byte >in 4 bytes speichern
    //anzahl eintrage; 4 bytes//wichtig beim einlesen
    //Headerformat: 1 byte Länge des pfades; Pfad (0-255 lang; relativ)8 bytes Offset innerhalb Datei; 8 byte Größe 4 bytes flags
    //Dann dateien in einem Bytestrom aneinandergehängt
    
    private String packageToRead;
    //private boolean archiveIsreadOnly;//TODO: erkennen ob medium Disk ist
    private RandomAccessFile achv=null;
    private String[] header_fnames = null;;
    private long[] header_fsizes=null;
    private long[] header_fcompsizes=null;
    private long[] header_foffsets=null;
    private int[] header_flags = null;
    
    
    
    public IZPackage(String path)
    {
        packageToRead = path;
        try
        {
            //NOP
            achv = new RandomAccessFile(packageToRead, "rwd");//VOID:readonly als absicherung gegen derps
            if(FileTK.checkDirExist(path))
            {
                preloadHeader();
            }
        }
        catch (FileNotFoundException ex)
        {
            ex.printStackTrace();
        }
    }
    
   
    
    void preloadHeader()
    {
        try
        {
            achv.seek(8);//headergröße anspringen
            byte[] hr = new byte[4];
            achv.readFully(hr);
            int hs = ByteBuffer.allocate(4).put(hr).getInt(0);

            achv.seek(12);//elementeanzahl anspringen
            byte[] fc = new byte[4];
            achv.readFully(fc);
            int elemcount= ByteBuffer.allocate(4).put(fc).getInt(0);

            achv.seek(16);
            byte[] hdrr = new byte[hs];
            achv.readFully(hdrr);//einlesen des headers
            ByteBuffer hdr = ByteBuffer.allocate(hs).put(hdrr);
            int pos = 0;//position innerhalb headers
            
            
            String[] filenames = new String[elemcount];
            int[] flags = new int[elemcount];
            long[] filesizes=new long[elemcount],filesizescomp=new long[elemcount],fileoffsets=new long[elemcount];//die 4 arrays initialisieren um den header abzubilden //dateinamenlänge wird nur intern benötigt
            
            
            for (int i = 0; i < elemcount; i++)
            {
                int flength = hdr.get(pos)&0xFF;//vorzeichen rausbügeln; danke javaquirk
                pos++;
                byte[] filenameraw = new byte[flength];
                hdr.position(pos);
                hdr.get(filenameraw);
                pos+=flength;
                filenames[i]=new String(filenameraw);//etwas unkonventionell :P dürfte nicht jeder wissen dass das geht
                filesizes[i]=hdr.getLong(pos);
                pos+=8;
                filesizescomp[i]=hdr.getLong(pos);
                pos+=8;
                fileoffsets[i]=hdr.getLong(pos);
                pos+=8;
                flags[i] = hdr.getInt();
                pos+=4;
            }
            //header eingelesen; 
            //header dumpen
            header_fnames = filenames;
            header_foffsets = fileoffsets;
            header_fsizes = filesizes;
            header_flags = flags;
            header_fcompsizes = filesizescomp;
        }
        catch(IOException e)
        {
        }
    }
    
   public static int FLAG_COMPRESSED = 0;
   public static int FLAG_GZIP = 1;
   String pbasedir,pfn;
   
    /**
     * erstellt ein neues iZpack-archiv unter dem im Objekt hinterlegtren archiv
     * @param baseDir Wurzelverzeichnis der input-files
     * @param files Dateien; relativ zu basedir; da dies die struktur im archiv spieschelt
     */
   PipedInputStream s;
    public void createArchive(String baseDir,String... files)
    {
        byte[] filenamelengths = new byte[files.length];
        byte[][] fileBytes = new byte[files.length][];
        byte[][] fileSizes = new byte[files.length][8];
        byte[][] fileSizesComp = new byte[files.length][8];
        byte[][] flags = new byte[files.length][4];
        //boolean[][]flagsreadable = new boolean[files.length][32];//32 flags maximum
        long[] fileSizesRaw = new long[files.length];//nötig für uncompress
        long[] fileSizesRawComp = new long[files.length];//nötig um Offset auszurechnen und einzuspeixern
        byte[][]  offsets = new byte[files.length][8];//wird erst zum schluss befüllt;
        int headerSize=0;
        //oFFSET0ALLE VORHERIGEN OFFSETS + HEADERSIZE+ 8DA MAGISCHE KENNUNG9
        for (int i = 0; i < files.length; i++)
        {
            String fileToGetSize = baseDir+File.separator+files[i];
            
            long fileSize = new File(fileToGetSize).length();
            fileSizesRaw[i] = fileSize;
            //byte[] filesizearr = new byte[8];
            /*for (int j = 0; j < 8; j++)
            {
                filesizearr[7-j] = (byte)(fileSize%256);//schneidet niederwertigstes bit heraus; 
                fileSize/=256;//ein byte nach rechts dudeln;
                
            }*/
            int flag=3; //TODO compresshandler //zzz_sfx=nocompress; jpg;png;mp3 etc=noCOmpress, too
            
            String ftype = FileTK.getFileExt(files[i]);
            String[] nocompress = new String[]{"docx","rar","7z","gz","bz2","jpg","png","xlsx","wmv","mp4","flv","mov","mp3","flac","m4a","wma"};
            for (String ble : nocompress)
            {
                if(ftype!=null&&(ftype.equalsIgnoreCase(ble)||(files[i].equalsIgnoreCase("zzz_sfxmod.jar"))))//erkennt die gesperrten dateinamen/typen für kkomprimieren
                {
                    flag = flag&(3^0xffffffff);  //rausnehmen der compres und gzip-bits
                }
            }
            
            
            
            fileSizes[i]=ByteBuffer.allocate(8).putLong(fileSize).array();
            fileSizesComp[i]=ByteBuffer.allocate(8).putLong(fileSize).array();
            fileBytes[i]=files[i].getBytes();
            flags[i] = ByteBuffer.allocate(4).putInt(flag).array();
            filenamelengths[i] = (byte)(fileBytes[i].length);
                         //size //compsize //offset   //dateinamenlänge  //längenmarkerbyte //flags
            headerSize += 8+    8+           8+     filenamelengths[i]+1                   +4;
        }
        long baseoffset = headerSize+16; //Header+magischeKennung;
        
        //create_offsets
        for (int i = 0; i < fileSizesRaw.length; i++)
        {
           
                offsets[i] = ByteBuffer.allocate(8).putLong(baseoffset).array();
                baseoffset +=fileSizesRawComp[i]; //offset für nxte datei vorbereiten;
            
        }
        
        byte[] header;//= new byte[headerSize+4];//headerSizeFlag wird mit eingebettet//elemcount auch;
        byte[] headerSizeattrib = ByteBuffer.allocate(4).putInt(headerSize).array();
        ByteBuffer headerAssem = ByteBuffer.allocate(headerSize+8);
        //header zusammensetzen;
        byte[] headerElemCount = ByteBuffer.allocate(4).putInt(files.length).array();
        int bo = 0;
        headerAssem.put(headerSizeattrib);
        bo+=4;//weiterschieben
        headerAssem.position(bo);
        headerAssem.put(headerElemCount);
        bo+=4;
        headerAssem.position(bo);
        
        for (int i = 0; i < offsets.length; i++)
        {
            headerAssem.put(bo,filenamelengths[i]);
            bo++;
            headerAssem.position(bo);
            headerAssem.put(fileBytes[i]);
            bo+=((int)filenamelengths[i]&0xff);
            headerAssem.position(bo);
            headerAssem.put(fileSizes[i]);
            bo+=8;
            headerAssem.position(bo);
            headerAssem.put(fileSizesComp[i]);
            bo+=8;
            headerAssem.position(bo);
            headerAssem.put(offsets[i]);     
            bo+=8;
            headerAssem.position(bo);
            headerAssem.put(flags[i]);     
            bo+=4;
            headerAssem.position(bo);
        }
        header=headerAssem.array();
        //RandomAccessFile achv=null;
        try
        {
            achv.seek(0);//zurücksetzen der datei;
            achv.write(magicHeader);
            achv.seek(8);
            achv.write(header); //header wird am schluss erzeugt da Komprimierung vorhanden
            //achv.write(header);
            System.err.println(files.length);
            try
            {
                //for (String fn : files)
                Thread.sleep(50);
            }
            catch (InterruptedException ex)
            {
                ex.printStackTrace();
            }
            for(int i = 0;i<files.length;i++)
            {
                String fn = files[i];
                int flag = ByteBuffer.allocate(4).put(flags[i]).getInt(0);
                System.err.println(flag+"|"+fn);
                if((flag&0x0000_0001)==0) //compressed-flag ausmaskieren
                {
                    RandomAccessFile readIn = new RandomAccessFile(baseDir+File.separator+fn, "rwd");
                    long readInVol = readIn.length();
                    while(readIn.getFilePointer()<readIn.length())
                    {
                        byte[] buf;
                        if(readInVol>CHUNK_SIZE)
                        {
                        buf = new byte[CHUNK_SIZE];
                        readInVol-=CHUNK_SIZE;
                        }
                        else
                        {
                            buf = new byte[(int)readInVol];
                        }
                        readIn.read(buf, 0, buf.length);  //TODO effizioenteren weg finden!!!
                        achv.write(buf);
                        fileSizesRawComp[i] = fileSizesRaw[i];
                    }
                    readIn.close();
                }
                else
                {
                    System.out.println("compressing "+fn);
                    if((flag&0x0000_0002)==2)//Gzip
                    {
                        s = new PipedInputStream(65536);
                        pbasedir=baseDir;
                        pfn=fn;
                        new Thread(new Runnable(){
                            public void run()
                            {
                                GZIPOutputStream compdata = null;
                                try
                                {
                                    compdata = new GZIPOutputStream(new PipedOutputStream(s));
                                    FileInputStream FI = new FileInputStream(pbasedir+File.separator+pfn);
                                    int returned=4096;
                                    int rw=0;
                                    byte[] bdata = new byte[4096];
                                    do
                                    {
                                        returned = FI.read(bdata);
                                        compdata.write(bdata,0,returned);
                                        compdata.flush();
                                        rw+=returned;
                                        System.out.println(rw);
                                    }
                                    while(returned==4096);
                                }
                                catch (IOException ex)
                                {
                                    ex.printStackTrace();
                                }
                                finally
                                {
                                    try
                                    {
                                        compdata.close();
                                    }
                                    catch (IOException ex)
                                    {
                                        ex.printStackTrace();
                                    }
                                }
                            }
                        }).start();
                        Thread.sleep(10);
                            //komprimiermich :P
                        int returned=4096;
                        long filesizecomp = 0;
                        int rw=0;
                        byte[] bdata = new byte[4096];
                        do
                        {
                            returned = s.read(bdata);
                            achv.write(bdata,0,returned);
                            filesizecomp+=returned;
                            rw+=returned;
                            System.out.println(">>"+returned);
                        }
                        while(returned==4096);
                        fileSizesRawComp[i] = filesizecomp;
                        System.out.println("finished COmpressing");
                    }
                    
                }
            }
            
            
            
            
            
            
        //recreate_offsets
        for (int i = 0; i < fileSizesRaw.length; i++)
        {
            offsets[i] = ByteBuffer.allocate(8).putLong(baseoffset).array();
            baseoffset +=fileSizesRawComp[i]; //offset für nxte datei vorbereiten;
        }
        
        
        headerAssem = ByteBuffer.allocate(headerSize+8);
        //header zusammensetzen;
        bo = 0;
        headerAssem.put(headerSizeattrib);
        bo+=4;//weiterschieben
        headerAssem.position(bo);
        headerAssem.put(headerElemCount);
        bo+=4;
        headerAssem.position(bo);
        
        for (int i = 0; i < offsets.length; i++)
        {
            headerAssem.put(bo,filenamelengths[i]);
            bo++;
            headerAssem.position(bo);
            headerAssem.put(fileBytes[i]);
            bo+=((int)filenamelengths[i]&0xff);
            headerAssem.position(bo);
            headerAssem.put(fileSizes[i]);
            bo+=8;
            headerAssem.position(bo);
            headerAssem.put(fileSizesComp[i]);
            bo+=8;
            headerAssem.position(bo);
            headerAssem.put(offsets[i]);     
            bo+=8;
            headerAssem.position(bo);
            headerAssem.put(flags[i]);     
            bo+=4;
            headerAssem.position(bo);
        }
        header=headerAssem.array();
        achv.seek(8);
        achv.write(header);
        }
        catch(IOException e)
        {
            
        }
        catch (InterruptedException ex)
        {
            ex.printStackTrace();
        }
        preloadHeader();
        
    }
    
    
    public void dumpAllIntoDirectory(String path)
    {
        try
        {
        long[] fileoffsets = header_foffsets;
        long[] filesizes = header_fsizes;
        String[] filenames = header_fnames;
        int[] flags = header_flags;
        long[] cprsize = header_fcompsizes;
        for (int i = 0; i < fileoffsets.length; i++)
            {
                long offs = fileoffsets[i];
                String fn = path+File.separator+filenames[i];
                long fs = cprsize[i];
                achv.seek(offs);
                FileTK.ensuredirExistence(fn);
                final RandomAccessFile dump = new RandomAccessFile(fn,"rwd");
                long dist = fs;
                if((flags[i]&0x0000_0003)!=3)
                {
                    while(achv.getFilePointer()<fs+offs)
                    {
                        byte[] buf;
                        if(dist>CHUNK_SIZE)
                        {
                            buf = new byte[CHUNK_SIZE];
                            dist-=CHUNK_SIZE;
                        }
                        else
                        {
                            buf = new byte[(int)dist];
                        }
                        achv.read(buf, 0, buf.length);  //TODO effizioenteren weg finden!!!
                        dump.write(buf);
                        /*
                        byte[] buf = new byte[1];
                        achv.read(buf, 0, 1);  //TODO effizioenteren weg finden!!!
                        dump.write(buf);*/
                    }
                }
                else
                {
                    final PipedOutputStream s = new PipedOutputStream();
                    new Thread(new Runnable()
                    {
                        public void run()
                        {
                            try
                            {
                                GZIPInputStream decomp = new GZIPInputStream(new PipedInputStream(s,65536));
                                Thread.sleep(30);
                                int readamount = 4096;
                                byte[] buffer = new byte[4096];
                                do
                                {
                                    readamount=decomp.read(buffer);
                                    dump.write(buffer,0,readamount);
                                }
                                while(readamount==4096);
                            }
                            catch (IOException ex)
                            {
                                ex.printStackTrace();
                            }
                            catch (InterruptedException ex)
                            {
                                ex.printStackTrace();
                            }
                        }
                    }).start();
                    while(achv.getFilePointer()<fs+offs)
                    {
                        byte[] buf;
                        if(dist>CHUNK_SIZE)
                        {
                            buf = new byte[CHUNK_SIZE];
                            dist-=CHUNK_SIZE;
                        }
                        else
                        {
                            buf = new byte[(int)dist];
                        }
                        achv.read(buf, 0, buf.length);  //TODO effizioenteren weg finden!!!
                        s.write(buf);
                        /*
                        byte[] buf = new byte[1];
                        achv.read(buf, 0, 1);  //TODO effizioenteren weg finden!!!
                        dump.write(buf);*/
                    }
                }
                dump.close();

            }
        }
        catch(IOException e)
        {
            e.printStackTrace();
        }
    }
    
    
    public static int getElemPosInArray(String[] Searchable,String searched)
    {
	for(int i=0;i<Searchable.length;i++)
	{
	    if(Searchable[i].equals(searched))
	    {
		//i=length;
		return i;
	    }
	}
	return -1;
    }
    
    public void extractFileToDisk(String achvpath,String destdir)
    {
        try
        {
            int pos = getElemPosInArray(header_fnames, achvpath);
            //byte[] target = new byte[(int)header_fsizes[pos]];
            achv.seek(header_foffsets[pos]);
            FileTK.ensuredirExistence(destdir+File.separator+achvpath);
            final RandomAccessFile dump = new RandomAccessFile(destdir+File.separator+achvpath, "rwd");
            long fs=header_fsizes[pos],readInVol=header_fsizes[pos],offs=header_foffsets[pos];
            if((header_flags[pos]&0x0000_0003)!=3)
                {
                    while(achv.getFilePointer()<fs+offs)
                    {
                        byte[] buf;
                        if(fs>CHUNK_SIZE)
                        {
                            buf = new byte[CHUNK_SIZE];
                            fs-=CHUNK_SIZE;
                        }
                        else
                        {
                            buf = new byte[(int)fs];
                        }
                        achv.read(buf, 0, buf.length);  //TODO effizioenteren weg finden!!!
                        dump.write(buf);
                        /*
                        byte[] buf = new byte[1];
                        achv.read(buf, 0, 1);  //TODO effizioenteren weg finden!!!
                        dump.write(buf);*/
                    }
                }
                else
                {
                    final PipedOutputStream s = new PipedOutputStream();
                    new Thread(new Runnable()
                    {
                        public void run()
                        {
                            try
                            {
                                GZIPInputStream decomp = new GZIPInputStream(new PipedInputStream(s,65536));
                                Thread.sleep(30);
                                int readamount = 4096;
                                byte[] buffer = new byte[4096];
                                do
                                {
                                    readamount=decomp.read(buffer);
                                    dump.write(buffer,0,readamount);
                                }
                                while(readamount==4096);
                            }
                            catch (IOException ex)
                            {
                                ex.printStackTrace();
                            }
                            catch (InterruptedException ex)
                            {
                                ex.printStackTrace();
                            }
                        }
                    }).start();
                    while(achv.getFilePointer()<fs+offs)
                    {
                        byte[] buf;
                        if(fs>CHUNK_SIZE)
                        {
                            buf = new byte[CHUNK_SIZE];
                            fs-=CHUNK_SIZE;
                        }
                        else
                        {
                            buf = new byte[(int)fs];
                        }
                        achv.read(buf, 0, buf.length);  //TODO effizioenteren weg finden!!!
                        s.write(buf);
                        /*
                        byte[] buf = new byte[1];
                        achv.read(buf, 0, 1);  //TODO effizioenteren weg finden!!!
                        dump.write(buf);*/
                    }
                }
                dump.close();
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
    
    
    
    
    /**
     * Liest eine Datei in jnen String aus. wird nur für kleine files empfohlen;
     * @param archivefile Datei im Archiv; muss in achvfile sein
     * @return Inhalt als String
     */
    public String getFileContent(String archivefile)
    {
        try
        {
            
            extractFileToDisk(archivefile, System.getenv("temp"));
            RandomAccessFile temp = new RandomAccessFile(System.getenv("temp")+File.separator+archivefile, "r");
            byte[] target = new byte[(int)temp.length()];
            temp.read(target);
            return new String(target);
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
        return "";
    }
    
    public String[] getFileList()
    {
        //hauptarbeit wird beim einlesen der datei gemacht;
        return header_fnames;
    }
   
    
    
    
    
    
    /*
    public void AddFile(String pathOnDisk)
    {
        
        
        
        
        throw new UnsupportedOperationException("Muss dies noch machen");
    }
    

    
    
    public void removeFile(String path)
    {
        throw new UnsupportedOperationException("Muss dies noch machen");
    }*/
    
    
    
    
    public void unload()
    {
        try
        {
            achv.close();
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
    

    
    
    
    
    //################################################################################################################
    //################################################################################################################
    //################################################################################################################
    //################################################################################################################
    //################################################################################################################
    //static utility methods for faster archivehandling
    
    
    public static void ExtractFilesToPath(String archivefile, String path)
    {
        IZPackage temp = new IZPackage(archivefile);
        temp.dumpAllIntoDirectory(path);
        temp.unload();
    }
     
    public static void wrapArchive(String archive,String packdir)
    {
        IZPackage temp = new IZPackage(archive);
        String[] filecontent = FileTK.walkDirectoryRecursively(packdir);
        ArrayList<String> fnp = new ArrayList<>();
        for (int i=0;i<filecontent.length;i++)//String fn : filecontent)
        {
            String fn=filecontent[i];
            
            if(!FileTK.isDir(fn))
            {
                fn=FileTK.getRelativePath(fn, packdir).substring(3);
                fnp.add(fn);
                
            }
        }
        temp.createArchive(packdir, fnp.toArray(new String[0]));
        temp.unload();
    }
    public String getPath()
    {
        return packageToRead;
    }
}
