package org.wikiwebserver.sync;

import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.List;

import org.wikiwebserver.sync.gui.FileSizeCellRenderer;

public class Syncher implements Runnable {
    
    public enum Action { DIFF, SYNC };    
    
    private volatile String id = null;
    
    private volatile Thread runner;
    private volatile Action action;
    private volatile DataLocation[] dataLocations;    
    private volatile FileItemBatch syncCommands;
    
    private volatile String currentTask;    
    private volatile Exception exception;
    private volatile int currentSite;
    private volatile FileItem currentItem;
    
    private volatile boolean complete = true;
    private volatile FileItemBatch synchronizedBatch = new FileItemBatch();
    
    public static void main(String[] args) {
        
        DataLocation[] locations = new DataLocation[args.length];
        for (int i=0; i<args.length; i++) {
            locations[i] = new LocalFileDataLocation(new File(args[i]), i);
        }
        
        try {
            Syncher syncher = new Syncher(locations);
            
            syncher.findDifferences();
            syncher.performSync();
            
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
    
    public Syncher(DataLocation... locations) {
        setDataLocations(locations);        
    }
    
    public void setDataLocations(DataLocation... locations) {
        interrupt();
        this.dataLocations = locations;
    }
    
    public void interrupt() {
        if (runner != null) {
            runner.interrupt();
        }        
    }
    
    private void addSynchronizedItem(FileItem item) {
        synchronizedBatch.addFileItem(item);
    }
    
    public FileItem[] getSynchronizedItems() {
        Collection<FileItem> items = synchronizedBatch.getFileItems();
        return items.toArray(new FileItem[items.size()]);
    }
    
    public FileItemBatch getSyncCommands() {
        return syncCommands;
    }  
    
    public void clearSyncCommands() {
        syncCommands = null;
    }    
    
    public long getTotalSyncBytes() {
        return getProcessingUnits(syncCommands, false);

    }      
    
    public long getTotalSyncProcessingUnits() {
        return getProcessingUnits(syncCommands, true);
    }   
    
    public long getSyncProcessingUnits() {
        long midTransfer = getCurrentTransferPosition();        
        long completedTransfer = getProcessingUnits(synchronizedBatch, true);
        return completedTransfer + midTransfer;
    }   
    
    public String getSyncProgessString(long totalBytesProcessed, int totalOperations) {
        if (syncCommands == null) return "No sync commands available!";

        long midTransfer = getCurrentTransferPosition();
        long completedTransfer = getProcessingUnits(synchronizedBatch, false);        
        int operation = synchronizedBatch.size() + 1;
        if (operation > totalOperations) operation = totalOperations;
        
        
        return FileSizeCellRenderer.formatSize(completedTransfer + midTransfer) + " of " +
               FileSizeCellRenderer.formatSize(totalBytesProcessed) + " (" +
               operation + " of " + totalOperations + " operations)";
    } 
    
    public long getCurrentTransferPosition() {
        if (complete) return 0;
        long midTransfer = 0;
        for (DataLocation location : dataLocations) {
            midTransfer += location.getCurrentTransferPosition();
        }    
        return midTransfer;
    }
    
    public FileItem getCurrentItem() {
        return currentItem;
    }     
    
    public int getCurrentSite() {
        return currentSite;
    }   
    
    public Exception getException() {
        return exception;
    }      
    
    public String getCurrentTask() {
        return currentTask;
    }    
    
    private void setCurrentTask(String task) {
        this.currentTask = task;
        System.out.println(task);        
    }    
    
    private long getProcessingUnits(FileItemBatch batch, boolean bumpUpZeros) {
        if (batch == null) return 0;
        long total = 0;
        List<FileItem> items = batch.getFileItemsInOperationOrder();
        for (FileItem item : items) {
            long len = item.getLength();
            
            // Deleting data is not proportional to size
            if (item.getState() == FileItem.State.DELETED) len = 0;
            
            if (bumpUpZeros) {
                // Need to add a value to folders and small files, 
                // so they show up on the progress bar
                if (len == 0) len = 100024;
            }
            total += len;
        }
        return (dataLocations.length-1) * total;
    }
    
    public void findDifferencesInBackground() {
        this.syncCommands = null;
        this.action = Action.DIFF;
        startRunning();
    } 
    
    public void performSyncInBackground() {
        this.action = Action.SYNC;
        startRunning();
    }    
    
    private void startRunning() {
        interrupt();
        runner = new Thread(this);
        runner.start();
    }

    public void run() {
        complete = false;
        try {
            exception = null;
            switch (action) {
            
                case DIFF:
                    findDifferences();
                    break;
                    
                case SYNC:
                    performSync();
                    saveState();
                    break;
            }       
        } catch (Exception ex) {
            ex.printStackTrace();
            exception = ex;
        }
        complete = true;
    }    

    public void findDifferences() throws Exception, InterruptedException {
        Thread.sleep(500);
        
        FileItemBatch[] batches = new FileItemBatch[dataLocations.length];
        for (int i=0; i<dataLocations.length; i++) {
            int site = dataLocations[i].getSite();
            currentSite = site;
            batches[i] = dataLocations[i].getBatch(id);
            batches[i].markSite(site);
        }

        currentSite = -1;
        currentTask = null;
        currentItem = null;
        exception = null;        
        syncCommands =  FileItemBatch.mergeBatches(batches);
    }    

    
    public void performSync() throws IOException, InterruptedException {
        synchronizedBatch = new FileItemBatch();        
        if (syncCommands != null) {

            for (FileItem item : syncCommands.getFileItemsInOperationOrder()) {
                currentItem = item;
                
                DataLocation source = getDataLocation(item.getSite());          
                
                if (Thread.interrupted()) {
                    throw new InterruptedException("Synchronization Interupted");
                }
    
                for (DataLocation target : dataLocations) {
                    currentSite = target.getSite();
                    if (target == source) continue;
                    if (item.getState() == FileItem.State.NOT_MODIFIED) {
                        // Nothing to do
                    }
                    if (item.getState() == FileItem.State.DELETED) {
                        performDelete(item, target);
                    }
                    else {
                        if (item.getType() == FileItem.Type.DIRECTORY) {
                            performMakeDir(item, target);
                        }                
                        else if (item.getType() == FileItem.Type.FILE) {
                            performTransfer(item, source, target);
                        }
                    }                  
                }    
                addSynchronizedItem(item);
            }
        }
        currentSite = -1;
        currentTask = null;
        currentItem = null;
        exception = null;
    }
    
    private void performTransfer(FileItem item, DataLocation source, DataLocation target) 
        throws IOException, InterruptedException {
        
        setCurrentTask("Writing File " + item.getRelPath() + " at site " + target);    
        target.transferData(source, item);
        
        setCurrentTask("Completing File " + item.getRelPath() + " at site " + target);  
        target.commitTransfer(item);
    }
    
    private void performDelete(FileItem item, DataLocation target) throws IOException {
        setCurrentTask("Deleting " + item.getRelPath() + " at site " + target);
        target.delete(item);
    }
    
    private void performMakeDir(FileItem item, DataLocation target) throws IOException {
        setCurrentTask("Creating Directory " + item.getRelPath() + " at site " + target);
        target.mkdir(item);
    }    
    

    private DataLocation getDataLocation(int site) {
        for (DataLocation location : dataLocations) {
            if (location.getSite() == site) return location;
        }
        return null;
    }    
    
    public void saveState() throws Exception, InterruptedException {
        if (id == null) return;
        for (int i=0; i<dataLocations.length; i++) {
            dataLocations[i].saveBatch(id);
        }
    }    

    public void generateId(String key) {
        int hash = key.hashCode();
        if (hash < 0) hash = -hash;
        String newId = Integer.toHexString(hash);
        String padding = "";
        for (int i=0; i<6-newId.length(); ++i) {
            padding = padding + "0";
        }
        id = padding + newId;
    }
    
    public void clearId() {
        id = null;
    }
    
    public boolean isComplete() {
        return complete;
    }
}

