package org.wikiwebserver.sync;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

public class LocalFileDataLocation implements DataLocation {
    
    private static final String SYNC_FILE_SUFFIX = ".syncdata.xml";
    private static final String TEMP_SUFFIX = ".sync.tmp";

    private int site;
    private File basePath;
    private long transferPosition;
    private List<File> excludedFiles = new ArrayList<File>();

    public LocalFileDataLocation() {
        
    }

    public LocalFileDataLocation(File baseFile, int site) {
        setBasePath(baseFile);        
        setSite(site);
    }
    
    public LocalFileDataLocation clone() {
        return new LocalFileDataLocation(getBasePath(), getSite());
    }
    
    public long getTimeOffset() throws IOException {
        return 0;
    }    
    
    public FileItemBatch getBatch(String id) throws InterruptedException, FileNotFoundException {
        
        FileItemBatch newBatch = getNewBatch();
        FileItemBatch previousBatch = loadDataLocationState(id);
        
        if (previousBatch != null) {

            // Finds files that have been deleted since last sync
            FileItemBatch deletedBatch = FileItemBatch.subtractBatch(newBatch, previousBatch);
            deletedBatch.markState(FileItem.State.DELETED);
            deletedBatch.setAllLastModified(previousBatch.getCreateTime());
            
            // Finds files that have been created since last sync
            FileItemBatch createdBatch = FileItemBatch.subtractBatch(previousBatch, newBatch);
            createdBatch.markState(FileItem.State.EXISTS);            
            
            // Finds files that have been modified since last sync
            FileItemBatch notCreatedBatch = FileItemBatch.subtractBatch(createdBatch, newBatch);
            FileItemBatch modifiedBatch = FileItemBatch.recentBatch(notCreatedBatch, previousBatch);
            modifiedBatch.markState(FileItem.State.MODIFIED);
            
            // List of known changes at this site since last sync
            return FileItemBatch.joinBatches(deletedBatch, modifiedBatch, createdBatch);
        }
        
        newBatch.setNewBatch(true);
        return newBatch;
    }     
    
    public InputStream getInputStream(FileItem item) throws IOException {
        return getInputStream(item.getRelPath());
    }
    
    public InputStream getInputStream(String relPath) throws IOException {
        File file = new File(getBasePath(), relPath);
        return new FileInputStream(file);
    }    
    
    public void transferData(DataLocation source, FileItem item) 
        throws IOException, InterruptedException {
        
        InputStream in = null;
        try {
            in = source.getInputStream(item);
            transferData(in, item.getLength(), item.getRelPath());
        } finally {
            if (in != null) in.close();
        }
    }
    
    public void transferData(InputStream in, long len, String relPath) 
        throws IOException, InterruptedException {

        File tmpFile = new File(getBasePath(), relPath + TEMP_SUFFIX);        
        transferPosition = 0;
        OutputStream out = null;
        try {
            if (tmpFile.getParentFile() != null && !tmpFile.exists()) {
                tmpFile.getParentFile().mkdirs();
            }        
            out = new FileOutputStream(tmpFile);
            int readSize = (int) Math.min(32 * 1024, len);
            byte[] buffer = new byte[readSize];
            int r = in.read(buffer);          
            while (r > 0) {
                out.write(buffer, 0, r);
                transferPosition += r;                
                len -= r;
                readSize = (int) Math.min(32 * 1024, len);
                if (readSize == 0) break;
                if (Thread.interrupted()) {
                    throw new InterruptedException("Synchronization Interrupted");
                }                   
                r = in.read(buffer, 0, readSize);
            }
        } catch (IOException ex) {
            tmpFile.delete();
            throw ex;
        } finally {
            if (out != null) out.close();
        } 
    }    
    
    public void commitTransfer(FileItem item) throws IOException {
        commitTransfer(item.getRelPath(), item.getLastModified());
    }
    
    public void commitTransfer(String relPath, long lastModified) throws IOException {
        new File(getBasePath(), relPath).delete();
        File tmpFile = new File(getBasePath(), relPath + TEMP_SUFFIX);        
        File targetFile = new File(getBasePath(), relPath);
        tmpFile.renameTo(targetFile);
        targetFile.setLastModified(lastModified - getTimeOffset());
    }
    
    public void delete(FileItem item) throws IOException {
        delete(item.getRelPath());
    }
    
    public void delete(String relPath) throws IOException {
        File file = new File(getBasePath(), relPath);
        file.delete();
    }
    
    public void mkdir(FileItem item) throws IOException {
        mkdir(item.getRelPath(), item.getLastModified());
    }
    
    public void mkdir(String relPath, long lastModified) throws IOException {
        File file = new File(getBasePath(), relPath);
        file.mkdirs();
        if (lastModified > 0) {
            file.setLastModified(lastModified - getTimeOffset());
        }
    }    
    
    
    private FileItemBatch loadDataLocationState(String id) {
        if (id == null) return null;
        FileItemBatch previousBatch = null;
        try {
            File syncDataFile = getSyncFile(id);
            InputStream in = new FileInputStream(syncDataFile);
            previousBatch = FileItemBatch.readBatch(in);
        } catch (FileNotFoundException ex) {
            // First run
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        return previousBatch;
    }    
    
    public void saveBatch(String id) throws Exception {
        if (id == null) return;
        OutputStream out = null;
        try {
            out = new FileOutputStream(getSyncFile(id));
            getNewBatch().writeBatch(out);
        } finally {
            out.close();
        }
    }    
    
    private FileItemBatch getNewBatch() 
        throws InterruptedException, FileNotFoundException {
        
        FileItemBatch newBatch = new FileItemBatch(getBasePath());
        addLocalFileItems(newBatch);
        newBatch.markState(FileItem.State.UNKNOWN);
        
        return newBatch;
    }      
    
    private void addLocalFileItems(FileItemBatch batch) 
        throws InterruptedException, FileNotFoundException {
        
        addLocalFileItems(getBasePath(), getBasePath(), batch);
    }
    
    private void addLocalFileItems(File dir, File base, FileItemBatch batch) 
        throws InterruptedException, FileNotFoundException {
        
        File[] files = dir.listFiles();
        if (files == null) {
            throw new FileNotFoundException("Directory does not exist");
        }
        
        for (File file : files) {         
            System.out.println(file);
            if (Thread.interrupted()) {
                throw new InterruptedException("Analysis Interrupted");
            }        
            
            // Don't sync the sync files
            if (file.getPath().endsWith(SYNC_FILE_SUFFIX)) continue;
            
            // Don't sync hidden or config files
            if (file.getName().startsWith(".")) continue;
            
            if (getExcludedFiles() != null) {
                if (getExcludedFiles().contains(file)) continue;
            }
            
            FileItem fileItem = new FileItem(file, base);
            try {
                if (file.isDirectory()) {
                    addLocalFileItems(file, base, batch);
                }
                if (!file.canRead()) {
                    throw new SecurityException("File can not be read");
                }
            } catch (SecurityException ex) {
                fileItem.setType(FileItem.Type.UNREADABLE);
            }
            batch.addFileItem(fileItem);            
        }
    }
    
    private File getSyncFile(String id) {
        return new File(getBasePath(), id + SYNC_FILE_SUFFIX);
    }
    
    public String toString() {
        return site + " (" + (getBasePath()).getAbsolutePath() + ")";
    }
    
    
    public int getSite() {
        return this.site;
    }

    public void setSite(int site) {
        this.site = site;
    }

    public File getBasePath() {
        return this.basePath;
    }

    public void setBasePath(File file) {
        this.basePath = file;
    }  
    
    public long getCurrentTransferPosition() {
        return transferPosition;
    }

    public List<File> getExcludedFiles() {
        return this.excludedFiles;
    }

    public void setExcludedFiles(List<File> excludedFiles) {
        this.excludedFiles = excludedFiles;
    }    
}

