/* ======================================================
                  Ricardo Martin Manso
                     PFC - UOC 2010
======================================================= */

package backup;

import java.io.File;
import java.io.FileFilter;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.text.ParseException;
import java.util.Date;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

// =======================================================
// API de bajo nivel que implementa los procesimientos basicos
// =======================================================
public class ArchivoDeduplicado {

    File farch;
    RandomAccessFile archivo;
    long globalPtrOut;

    Directorio directorio;
    Directorio directorioActual;

    CabeceraAD cabecera;


    static final int tamBuffers=50*1024*1024;
    static final int tamBuffers2=20*1024*1024;

    public ArchivoDeduplicado(String fichero) throws FileNotFoundException, IOException, ParseException, ADException
    {
        farch=new File(fichero);
        if (!farch.exists())  throw new ADException(ADException.ADE_FICHAD,""+fichero);
        archivo=new RandomAccessFile(farch,"rw");
        globalPtrOut=archivo.length();
        cabecera=new CabeceraAD();
        if (globalPtrOut==0)
        {
            throw new ADException(ADException.ADE_FICHAD,""+fichero);
        }
        else
        {
            cabecera.read(archivo);
            System.out.println("===============================================================");
            System.out.println("Abriendo fichero deduplicado: "+fichero);
            cabecera.print();
            System.out.println("===============================================================");
        }
        directorio=new Directorio();
        directorioActual=directorio;
    }


    public ArchivoDeduplicado(String fichero,long codInicial,int retencion) throws FileNotFoundException, IOException, ParseException
    {
        crearArchivoAD(fichero,codInicial,retencion);
        System.out.println("===============================================================");
        System.out.println("Creando fichero deduplicado: "+fichero);
        cabecera.print();
        System.out.println("===============================================================");
    }

    public void crearArchivoAD(String fichero,long codInicial,int retencion) throws FileNotFoundException, IOException, ParseException
    {
        farch=new File(fichero);
        archivo=new RandomAccessFile(farch,"rw");
        globalPtrOut=archivo.length();
        cabecera=new CabeceraAD();
        cabecera.setCodeInicial(codInicial);
        cabecera.setPeriodoRetencion(retencion);
        //cabecera.setPeriodoRetencionMinimo(retencionMin);
        globalPtrOut = 4096;
        archivo.setLength(globalPtrOut);
        directorio=new Directorio();
        directorioActual=directorio;
    }

    public void loadBackup(int code) throws IOException, ADException
    {
        if (cabecera.isVacio()) throw new ADException(ADException.ADE_VACIO);
        Backup b=cabecera.findBackup(code);
        if (b!=null)
        {
            directorio = new Directorio("");
            directorio.read(archivo,b.getPtr());
            directorioActual=directorio;
        }
        else
            throw new ADException(ADException.ADE_CODEBK,""+code);
    }


    public void createBackup() throws IOException
    {
        Backup ult=cabecera.getLastBackup();
        Backup backup=new Backup(globalPtrOut);
        cabecera.addBackup(backup);
        directorio = new Directorio("");
        if (ult!=null)
        {
            long ptr=ult.getPtr();
            directorio.read(archivo,ptr);
        }
        directorioActual=directorio;
    }

    public void closeBackup() throws IOException
    {
        directorio.purge();
        long t=directorio.write(archivo, globalPtrOut);
        Backup b=cabecera.getBackup();
        b.setParams(globalPtrOut,globalPtrOut+t);
        archivo.seek(0);
        cabecera.write(archivo);
    }

    public void close(boolean write) throws IOException
    {
        if (write)
        {
            archivo.seek(0);
            cabecera.write(archivo);
        }
        archivo.close();
    }

    public Date getFechaBackup()
    {
        return cabecera.getBackup().getFecha();
    }

    public String getStrFechaBackup()
    {
        return cabecera.getBackup().getStrFecha();
    }

    public void mkdir(String dir)
    {
        directorioActual.mkdir(dir);
    }

    public Directorio cdDir(String dir) throws ADException
    {
        if (dir.startsWith("\\"))
        {
            directorioActual=directorio;
            dir=dir.substring(1);
            if (dir.equals("")) return directorioActual;
        }

        Directorio d=directorioActual.cd(dir);
        if (d!=null) 
        {
            directorioActual = d;
            return d;
        }
        else
            throw new ADException(ADException.ADE_CDDIR,dir);
    }

    public Directorio createDir(String dir)
    {
        Directorio d=directorioActual.cd(dir);

        if (d==null)
            d=directorioActual.mkdir(dir);
        else
            d.setExiste(true);

        directorioActual=d;

        return d;
    }

    public long findArchivo(String nombre)
    {
        return directorioActual.findArchivo(nombre);
    }

    public Archivo loadArchivo(String nombre) throws FileNotFoundException, IOException
    {
        long d=directorioActual.findArchivo(nombre);
        if (d!=-1)
        {
            Archivo fichero=new Archivo(nombre);
            fichero.setPtr(d);
            loadBloquesFichero(fichero);
            return fichero;
        }
        else
            return null;
    }

    public void printDirActual()
    {
        directorioActual.print("");
    }

    public void printDirActual(Job trabajo)
    {
        directorioActual.print("",trabajo);
    }

    public void procesaAddFichero(Archivo fichero) throws FileNotFoundException, IOException
    {
        Directorio nodo=directorioActual.findNodo(fichero.getNombre());
        if (nodo==null)
            addFichero(fichero);
        else
        {
            File f=new File(fichero.getPath()+"\\"+fichero.getNombre());
            if (f.lastModified()!=nodo.getLastMod())
            {
                fichero.setPtr(nodo.getPtr());
                loadBloquesFichero(fichero);
                long t=fichero.getDescBloques().getTamTotal();
                if (f.length()<t) 
                {
                    directorioActual.delete(nodo);
                    addFichero(fichero);
                }
                else
                {
                    System.out.print("Actualizando: "+fichero.getPath()+"\\"+fichero.getNombre()+"...");
                    DescBloquesArchivo db=procesaBloquesFichero(fichero);
                    nodo.setPtr(db.getPtr());
                    nodo.setLastMod(f.lastModified());
                    System.out.println("OK");
                    nodo.setExiste(true);
                }
            }
            else
                nodo.setExiste(true);
        }
    }

    public void addFichero(Archivo fichero) throws FileNotFoundException, IOException
    {
        long tmp=globalPtrOut;
        globalPtrOut+=28;
        incorporaFichero(fichero);
        archivo.seek(tmp);
        fichero.getDescBloques().write(archivo);
        directorioActual.create(fichero.getNombre(), fichero.getLastMod(),tmp);
    }

    public void addDirectorio(Job trabajo) throws FileNotFoundException, IOException, ADException
    {
        String path=trabajo.getPath();
        String strDir=path+trabajo.getFichero();

        File dir=new File(strDir);

        if (!dir.exists()) throw new ADException(ADException.ADE_CDDIR,strDir);

        File[] lista=dir.listFiles();
        int i;
        for(i=0;i<lista.length;i++)
        {
            String fichero = lista[i].getName();
            if (!lista[i].isDirectory())
            {
                if (trabajo.testFiltro(fichero))
                {
                    Archivo arch=new Archivo(fichero);
                    arch.setPath(strDir);
                    procesaAddFichero(arch);
                }
            }
            else
            {
                if(trabajo.isRecursivo())
                {
                    Directorio actual=directorioActual;
                    createDir(fichero);
                    trabajo.setPath(strDir+"\\");
                    trabajo.setFichero(fichero);
                    addDirectorio(trabajo);
                    directorioActual=actual;
                }
            }
        }
    }

    public long  write(Archivo fichero) throws IOException
    {
        archivo.seek(globalPtrOut);
        long t=fichero.getDescBloques().write(archivo);
        globalPtrOut+=t;
        return t;
    }



    public void loadBloquesFichero(Archivo fichero) throws FileNotFoundException, IOException
    {
        DescBloquesArchivo descArch=new DescBloquesArchivo();
        archivo.seek(fichero.getPtr());
        descArch.read(archivo);
        descArch.setPtr(fichero.getPtr());
        fichero.setDescBloques(descArch);
    }


    public void incorporaFichero(Archivo fichero) throws FileNotFoundException, IOException
    {
        System.out.print("Incorporando "+fichero.getPath()+"\\"+fichero.getNombre()+"...");
        File archIn=new File(fichero.getPath()+"\\"+fichero.getNombre());
        RandomAccessFile archivoIn=new RandomAccessFile(archIn,"r");
        DescBloquesArchivo descArch=new DescBloquesArchivo();
        archivo.seek(globalPtrOut);
        archivoIn.seek(0);
        byte[] buf = new byte[tamBuffers];
        int len;
        long ini=globalPtrOut;
        long t=0;
        while ((len = archivoIn.read(buf)) > 0)
        {
            archivo.write(buf,0,len);
            t+=len;
        }
        globalPtrOut+=t;
        descArch.addBloque(new DescBloque(ini,t));
        fichero.setLastMod(archIn.lastModified());
        archivoIn.close();
        fichero.setDescBloques(descArch);
        System.out.println("OK");
    }

    public DescBloquesArchivo procesaBloquesFichero(Archivo fichero) throws FileNotFoundException, IOException
    {
        File archIn=new File(fichero.getPath()+"\\"+fichero.getNombre());
        RandomAccessFile archivoIn=new RandomAccessFile(archIn,"r");
        DescBloquesArchivo descArch=new DescBloquesArchivo();
        DescBloquesArchivo dba=fichero.getDescBloques();
        
        long tam=0;
        long ptr=0;
        long globalPtrIn=52;
        int len=0;
        int t=0;
        long tamDist=0;
        long ptrEscritura=globalPtrOut;
        int iBloque=0;
        long ii=0;
        long tamBloque=0;
        int tb=0;
        long leidos=0;
        boolean actualizado=false;
 
        byte[] buf = new byte[tamBuffers2];
        byte[] bufIn = new byte[tamBuffers2];
        byte[] bufOut = new byte[1024*1024];
        int iBufOut=0;
        long proc;

        for(iBloque=0;iBloque<dba.getN();iBloque++)
        {
        //System.out.println("BLOQUE");
        globalPtrIn=dba.getDescBloque(iBloque).ptr;
        ptr=0;
        tamBloque=dba.getDescBloque(iBloque).tam;
        tb=tamBuffers2;
        archivo.seek(globalPtrIn);
        ii=0;
        leidos=0;
        proc=0;
        while(ii<tamBloque)
        {
            
        if ((tamBloque-ii)<tamBuffers2) tb=(int) (tamBloque - ii);
            archivo.seek(globalPtrIn+leidos);
            len = archivo.read(buf,0,tb);
            archivoIn.read(bufIn,0,tb);
            leidos+=len;
            int i=0;
            t=4096;
            while(i<tb)
            {
                if ((tb-i)<4096) t=tb-i;  //t=lenIn-i;
                int r;
                r=compara(buf,bufIn,i,i+t);
                if (r==-1)
                {
                    if(tamDist>0)
                    {
                        descArch.addBloque(new DescBloque(globalPtrOut,tamDist));
                        globalPtrOut+=tamDist;
                        ptr=proc;
                        tamDist=0;
                    }
                    tam+=t;
                }
                else
                {
                    if (tam>0)
                    {
                        descArch.addBloque(new DescBloque(globalPtrIn+ptr,tam));
                        ptr=proc;
                        tam=0;
                    }
                    tamDist+=t;
                    int z;
                    for(z=i;z<(i+t);z++) bufOut[iBufOut++]=bufIn[z];
                    if (iBufOut>1000*1024)
                    {
                        archivo.seek(ptrEscritura);
                        archivo.write(bufOut,0,iBufOut);
                        ptrEscritura+=iBufOut; //t
                        iBufOut=0;
                    }

                    actualizado=true;
                }
                i+=t;
                proc+=t;
            }
        
        ii+=tb;
        }


        if (tam>0)
        {
            //System.out.println("Bloque igual:" + ptr + " (" + tam + " bytes)");
            descArch.addBloque(new DescBloque(globalPtrIn+ptr,tam));
            tam=0;
        }
  
        }


        if(tamDist>0)
        {
            descArch.addBloque(new DescBloque(globalPtrOut,tamDist));
            globalPtrOut+=tamDist;
            tamDist=0;
        }

        if (iBufOut>0)
        {
            archivo.seek(ptrEscritura);
            archivo.write(bufOut,0,iBufOut);
            ptrEscritura+=iBufOut;
            iBufOut=0;
        }

        archivo.seek(globalPtrOut);
        long ini=globalPtrOut;
        t=0;
        while ((len = archivoIn.read(buf)) > 0)
        {
            archivo.write(buf,0,len);
            t+=len;
        }
        globalPtrOut+=t;
        if (t>0)
        {
            descArch.addBloque(new DescBloque(ini,t));
            actualizado=true;
        }



        if (actualizado)
        {
            archivo.seek(globalPtrOut);
            descArch.setPtr(globalPtrOut);
            globalPtrOut+=descArch.write(archivo);
            fichero.setDescBloques(descArch);
        }
        else
            descArch.setPtr(dba.getPtr());

        archivoIn.close();
        return descArch;
    }


     public DescBloquesArchivo procesaBloquesFicheroOLD(Archivo fichero) throws FileNotFoundException, IOException
    {
        File archIn=new File(fichero.getPath()+"\\"+fichero.getNombre());
        RandomAccessFile archivoIn=new RandomAccessFile(archIn,"r");
        DescBloquesArchivo descArch=new DescBloquesArchivo();
        DescBloquesArchivo dba=fichero.getDescBloques();

        long tam=0;
        long ptr=0;
        long globalPtrIn=52;
        int lenIn=0;
        int len=0;
        int t=0;
        long tamDist=0;
        long ptrEscritura=globalPtrOut;
        int iBloque=0;
        long ii=0;
        long tamBloque=0;
        int tb=0;
        long leidos=0;
        boolean actualizado=false;

        byte[] buf = new byte[tamBuffers2];
        byte[] bufIn = new byte[tamBuffers2];
        byte[] bufOut = new byte[1024*1024];
        int iBufOut=0;

        for(iBloque=0;iBloque<dba.getN();iBloque++)
        {
        globalPtrIn=dba.getDescBloque(iBloque).ptr;
        tamBloque=dba.getDescBloque(iBloque).tam;
        tb=tamBuffers2;
        archivo.seek(globalPtrIn);
        ii=0;
        leidos=0;
        while(ii<tamBloque)
        {

        if ((tamBloque-ii)<tamBuffers2) tb=(int) (tamBloque - ii);
            archivo.seek(globalPtrIn+leidos);
            len = archivo.read(buf,0,tb);
            lenIn = archivoIn.read(bufIn,0,tb);
            leidos+=len;
            int i=0;
            t=4096;
            while(i<tb)
            {
                if ((tb-i)<4096) t=tb-i;  //t=lenIn-i;
                int r;
                r=compara(buf,bufIn,i,i+t);
                if (r==-1)
                {
                    if(tamDist>0)
                    {
                        descArch.addBloque(new DescBloque(globalPtrOut,tamDist));
                        globalPtrOut+=tamDist;
                        ptr+=tamDist;
                        tamDist=0;
                    }
                    tam+=t;
                }
                else
                {
                    if (tam>0)
                    {
                        descArch.addBloque(new DescBloque(globalPtrIn+ptr,tam));
                        ptr+=tam;
                        tam=0;
                    }
                    tamDist+=t;
                    int z;
                    for(z=i;z<(i+t);z++) bufOut[iBufOut++]=bufIn[z];
                    if (iBufOut>1000*1024)
                    {
                        archivo.seek(ptrEscritura);
                        archivo.write(bufOut,0,iBufOut);
                        ptrEscritura+=iBufOut; //t
                        iBufOut=0;
                    }

                    actualizado=true;
                }
                i+=t;
            }

        ii+=tb;
        }


        if (iBufOut>0)
        {
            archivo.seek(ptrEscritura);
            archivo.write(bufOut,0,iBufOut);
            ptrEscritura+=iBufOut;
            iBufOut=0;
        }



        if (tam>0)
        {
            descArch.addBloque(new DescBloque(globalPtrIn+ptr,tam));
            tam=0;
        }

        if(tamDist>0)
        {
            descArch.addBloque(new DescBloque(globalPtrOut,tamDist));
            globalPtrOut+=tamDist;
            tamDist=0;
        }
        }


        archivo.seek(globalPtrOut);
        long ini=globalPtrOut;
        t=0;
        while ((len = archivoIn.read(buf)) > 0)
        {
            archivo.write(buf,0,len);
            t+=len;
        }
        globalPtrOut+=t;
        if (t>0)
        {
            descArch.addBloque(new DescBloque(ini,t));
            actualizado=true;
        }



        if (actualizado)
        {
            archivo.seek(globalPtrOut);
            descArch.setPtr(globalPtrOut);
            globalPtrOut+=descArch.write(archivo);
            fichero.setDescBloques(descArch);
        }
        else
            descArch.setPtr(dba.getPtr());

        archivoIn.close();
        return descArch;
    }


    public void extraerFichero(Archivo fichero,String destino) throws FileNotFoundException, IOException
    {
        String ruta=destino+fichero.getNombre();
        System.out.print("Extrayendo "+ruta+"...");
        File archOut=new File(ruta);
        RandomAccessFile archivoOut=new RandomAccessFile(archOut,"rw");
        DescBloquesArchivo dba=fichero.getDescBloques();
        long globalPtrIn;
        int iBloque=0;
        long ii=0;
        long tamBloque=0;
        int tb=0;
        byte[] buf = new byte[tamBuffers];

        for(iBloque=0;iBloque<dba.getN();iBloque++)
        {
            globalPtrIn=dba.getDescBloque(iBloque).ptr;
            tamBloque=dba.getDescBloque(iBloque).tam;
            tb=tamBuffers;
            archivo.seek(globalPtrIn);
            ii=0;
            while(ii<tamBloque)
            {
                if ((tamBloque-ii)<tamBuffers) tb=(int) (tamBloque - ii);
                archivo.read(buf,0,tb);
                archivoOut.write(buf, 0, tb);
                ii+=tb;
            }
        }
        System.out.println("OK");
    }

    public void extraerDirectorio(Job trabajo,String destino) throws FileNotFoundException, IOException, ADException
    {
        List<Directorio> lista=directorioActual.getContenido();
        int i;
        for(i=0;i<lista.size();i++)
        {
            Directorio nodo=lista.get(i);
            if (nodo.getTipo()==Directorio.TIPO_ARCH)
            {
                Archivo arch=new Archivo(nodo.getNombre());
                arch.setPtr(nodo.getPtr());
                loadBloquesFichero(arch);
                extraerFichero(arch,destino);
            }
            else
            {
                Directorio actual=directorioActual;
                cdDir(nodo.getNombre());
                String strDir=destino+nodo.getNombre();
                File f=new File(strDir);
                f.mkdir();
                extraerDirectorio(trabajo,strDir+"\\");
                directorioActual=actual;
            }
        }
    }

    public void extraerFicheroToZIP(Archivo fichero,String destino,ZipOutputStream target) throws FileNotFoundException, IOException
    {
        String ruta=destino+fichero.getNombre();
        System.out.print("Extrayendo y comprimiendo "+ruta+"...");

        target.putNextEntry(new ZipEntry(ruta));

        DescBloquesArchivo dba=fichero.getDescBloques();
        long globalPtrIn;
        int iBloque=0;
        long ii=0;
        long tamBloque=0;
        int tb=0;
        byte[] buf = new byte[tamBuffers];

        for(iBloque=0;iBloque<dba.getN();iBloque++)
        {
            globalPtrIn=dba.getDescBloque(iBloque).ptr;
            tamBloque=dba.getDescBloque(iBloque).tam;
            tb=tamBuffers;
            archivo.seek(globalPtrIn);
            ii=0;
            while(ii<tamBloque)
            {
                if ((tamBloque-ii)<tamBuffers) tb=(int) (tamBloque - ii);
                archivo.read(buf,0,tb);
                target.write(buf, 0, tb);
                ii+=tb;
            }
        }
        target.closeEntry();
        System.out.println("OK");
    }

    public void extraerDirectorioToZIP(Job trabajo,String destino,ZipOutputStream target) throws FileNotFoundException, IOException, ADException
    {
        List<Directorio> lista=directorioActual.getContenido();
        int i;
        for(i=0;i<lista.size();i++)
        {
            Directorio nodo=lista.get(i);
            if (nodo.getTipo()==Directorio.TIPO_ARCH)
            {
                Archivo arch=new Archivo(nodo.getNombre());
                arch.setPtr(nodo.getPtr());
                loadBloquesFichero(arch);
                extraerFicheroToZIP(arch,destino,target);
            }
            else
            {
                Directorio actual=directorioActual;
                cdDir(nodo.getNombre());
                String strDir=destino+nodo.getNombre();
                extraerDirectorioToZIP(trabajo,strDir+"\\",target);
                directorioActual=actual;
            }
        }
    }

    public static int compara2(byte[] v1,byte[] v2,int ini,int tam)
    {
        byte fv1[]=java.util.Arrays.copyOfRange(v1, ini, tam);
        byte fv2[]=java.util.Arrays.copyOfRange(v2, ini, tam);
        if (java.util.Arrays.equals(fv1, fv2)) return -1; else return 0;
    }

    public static int compara(byte[] v1,byte[] v2,int ini,int tam)
    {
        int c;
        for(c=ini;c<tam;c++) if (v1[c]!=v2[c]) return c;
        return -1;
    }

    public boolean isCaducado()
    {
        return cabecera.isCaducado();
    }

    public void doArchivado(boolean write) throws IOException, FileNotFoundException, ParseException
    {
        close(write);
        String nombre,nombreOld;
        nombreOld=farch.getAbsolutePath();
        nombre=nombreOld.substring(0, nombreOld.length()-2)+"bak.ad";
        File nuevo=new File(nombre);
        System.out.println("Archivado en "+nombre);
        farch.renameTo(nuevo);

        crearArchivoAD(nombreOld,0,cabecera.getPeriodoRetencion());
    }

}
