package org.wikiwebserver.sync;

import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import static org.wikiwebserver.sync.FileItem.State;

public class FileItemBatch implements Serializable {

    private static final long serialVersionUID = 1L;
    
    private String basePath;
    private long createTime = System.currentTimeMillis();
    private boolean isNewBatch = false;
    private Map<String, FileItem> fileItemMap = new HashMap<String, FileItem>();
    
    public FileItemBatch() {
    }
    
    public FileItemBatch(String basePath) {
        setBasePath(basePath);
    }
    
    public FileItemBatch(File file) {   
        setBasePath(file.getPath());
    }    
    
    public FileItemBatch clone() {
        FileItemBatch clone = new FileItemBatch();
        clone.setBasePath(getBasePath());
        
        Map<String, FileItem> clonedItemMap = new HashMap<String, FileItem>();
        for (FileItem item : getFileItemMap().values()) {
            clonedItemMap.put(item.getRelPath(), item.clone());
        }
        clone.setFileItemMap(clonedItemMap);
        clone.setNewBatch(isNewBatch());
        
        return clone;
    }    
    
    public synchronized void markState(FileItem.State state) {
        for (FileItem item : fileItemMap.values()) {
            item.setState(state);
        }
    }
    
    public synchronized void markSite(int site) {
        for (FileItem item : fileItemMap.values()) {
            item.setSite(site);
        }
    }    
    
    public synchronized void setAllLastModified(long lastModified) {
        for (FileItem item : fileItemMap.values()) {
            item.setLastModified(lastModified);
        }
    }    
     
    
    public synchronized FileItem getFileItem(FileItem item) {
        return fileItemMap.get(item.getRelPath());
    }  
    
    public synchronized int size() {
        return fileItemMap.size();
    }
    
    public synchronized List<FileItem> getFileItems() {
        List<FileItem> list = new ArrayList<FileItem>();
        for (FileItem item : fileItemMap.values()) {
            list.add(item.clone());
        }
        return list;
    }
    
    public static List<FileItem> getFileItems(Collection<FileItem> items, FileItem.State... states) {
        List<FileItem> list = new ArrayList<FileItem>();
        for (FileItem item : items) {
            for (FileItem.State state : states) {
                if (item.getState() == state) {
                    list.add(item.clone());
                }
            }
        }
        return list;
    }
    
    public static List<FileItem> getFileItems(Collection<FileItem> items, FileItem.Type type) {
        List<FileItem> list = new ArrayList<FileItem>();
        for (FileItem item : items) {
            if (item.getType() == type) {
                list.add(item.clone());
            }
        }
        return list;
    }
    
    public List<FileItem> getFileItemsInOperationOrder() {
        
        List<FileItem> list = new ArrayList<FileItem>();
        
        // Directories structure needs to be created first
        List<FileItem> directories = getFileItems(getFileItems(), FileItem.Type.DIRECTORY);
        // Order by depth, inner directories must be created after outer ones
        Comparator<FileItem> pathLengthComparator = new Comparator<FileItem>() {
            public int compare(FileItem item1, FileItem item2) {
                return item1.getRelPath().length() - item2.getRelPath().length();
            }
        };
        Collections.sort(directories, pathLengthComparator);
        list.addAll(getFileItems(directories, State.EXISTS, State.UNKNOWN));
        
        // Add the files
        List<FileItem> files = getFileItems(getFileItems(), FileItem.Type.FILE); 
        Collections.sort(files, pathLengthComparator);        
        List<FileItem> changedfiles = getFileItems(files, State.EXISTS, State.MODIFIED, State.UNKNOWN); 
        list.addAll(changedfiles);
        
        // Delete files last
        Collections.reverse(files);  
        list.addAll(getFileItems(files, State.DELETED));
        
        // When deleting directories, delete outer ones before inner ones
        Collections.reverse(directories);
        list.addAll(getFileItems(directories, State.DELETED));
        
        return list;
    }    
    
    public synchronized void addFileItem(FileItem item) {
        fileItemMap.put(item.getRelPath(), item.clone());
    }   
    
    public synchronized void removeFileItem(FileItem item) {
        fileItemMap.remove(item.getRelPath());
    }   
    
    public synchronized void applyTimeOffset(long timeOffset) {
        for (FileItem item : fileItemMap.values()) {
            item.setLastModified(item.getLastModified() + timeOffset);
        }
    }   
    
    public synchronized void toggleState(FileItem.State oldState, FileItem.State newState) {
        for (FileItem item : fileItemMap.values()) {
            if (item.getState() == oldState) {
                item.setState(newState);
            }
        }
    }    
    
    public static FileItemBatch joinBatches(FileItemBatch... batches) {
        FileItemBatch joinedBatch = batches[0];        
        for (int i=1; i<batches.length; i++) {
            joinedBatch = joinBatch(joinedBatch, batches[i]);
        }
        return joinedBatch;
    }
    
    private static FileItemBatch joinBatch(FileItemBatch batch1, FileItemBatch batch2) {
        FileItemBatch joinedBatch = batch1.clone();
        
        for (FileItem batch2Item : batch2.getFileItems()) {
            joinedBatch.addFileItem(batch2Item);            
        }
        return joinedBatch;
    }          
    
    public static FileItemBatch mergeBatches(FileItemBatch... batches) {
        FileItemBatch mergedBatch = batches[0];        
        for (int i=1; i<batches.length; i++) {
            mergedBatch = mergeBatch(mergedBatch, batches[i]);
        }
        return mergedBatch;
    }
    
    private static FileItemBatch mergeBatch(FileItemBatch batch1, FileItemBatch batch2) {
        FileItemBatch mergedBatch = batch1.clone();
        mergedBatch.toggleState(FileItem.State.UNKNOWN, FileItem.State.EXISTS);
        
        for (FileItem batch2Item : batch2.getFileItems()) {
            FileItem item = batch1.getFileItem(batch2Item);
            if (item == null) {
                item = batch2Item;
                if (item.getState() == FileItem.State.UNKNOWN) {
                    item.setState(FileItem.State.EXISTS);
                }
            } else {
                long item1ModKey = item.getLastModified() / 1000;
                long item2ModKey = batch2Item.getLastModified() / 1000;
                if (item1ModKey > item2ModKey) {
                    item.setState(FileItem.State.MODIFIED);
                } else if (item1ModKey < item2ModKey) {
                    item = batch2Item;
                    item.setState(FileItem.State.MODIFIED);
                } else if (item1ModKey == item2ModKey && item.getLength() == batch2Item.getLength()) {
                    if (item.getState() == FileItem.State.UNKNOWN) {
                        item.setState(FileItem.State.NOT_MODIFIED);
                    }
                }
            }
            mergedBatch.addFileItem(item);            
        }
        return mergedBatch;
    }      
    
    public static FileItemBatch subtractBatch(FileItemBatch batch, FileItemBatch fromBatch) {
        FileItemBatch resultBatch = fromBatch.clone();
        for (FileItem item : batch.getFileItems()) {
            resultBatch.removeFileItem(item);
        }
        return resultBatch;
    }
    
    public static FileItemBatch recentBatch(FileItemBatch newBatch, FileItemBatch fromBatch) {
        FileItemBatch modifiedBatch = new FileItemBatch(fromBatch.getBasePath());
        for (FileItem compItem : newBatch.getFileItems()) {
            FileItem item = fromBatch.getFileItem(compItem);
            long compItemModTime = compItem.getLastModified();
            if (item == null || compItemModTime > item.getLastModified()) {
                modifiedBatch.addFileItem(compItem.clone());
            }
        }
        return modifiedBatch;
    }
    
    public String toString() {
        StringBuilder bill = new StringBuilder();
        for (FileItem item : getFileItems()) {
            bill.append(item.toString() + "\r\n");
        }
        return bill.toString();
    }    
    
    public static FileItemBatch readBatch(InputStream in) throws Exception {
        

        DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance();
        DocumentBuilder docBuilder = dbfac.newDocumentBuilder();
        Document doc = docBuilder.parse(in);
        
        NodeList batches = doc.getElementsByTagName("fileItemBatch");
        Element batch = (Element) batches.item(0);
        
        FileItemBatch fileItemBatch = new FileItemBatch(batch.getAttribute("basePath"));
        fileItemBatch.setCreateTime(Long.parseLong(batch.getAttribute("createdTime")));
        fileItemBatch.setNewBatch(Boolean.parseBoolean(batch.getAttribute("isNewBatch")));
        
        NodeList items = doc.getElementsByTagName("fileItem");
        for (int i=0; i<items.getLength(); i++) {
            Element item = (Element) items.item(i);
            FileItem fileItem = new FileItem(item.getAttribute("relPath"));
            fileItem.setLastModified(Long.parseLong(item.getAttribute("modified")));
            fileItem.setLength(Long.parseLong(item.getAttribute("length")));
            fileItem.setType(FileItem.Type.valueOf(item.getAttribute("type")));
            fileItem.setState(FileItem.State.valueOf(item.getAttribute("state")));     
            fileItemBatch.addFileItem(fileItem);
        }

        return fileItemBatch;
    }
    
    public void writeBatch(OutputStream out) throws Exception {

        DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance();
        DocumentBuilder docBuilder = dbfac.newDocumentBuilder();
        Document doc = docBuilder.newDocument();

        Element batch = doc.createElement("fileItemBatch");
        
        Comment comment = doc.createComment("This file is used by JSiteSync to maintain" +
        		" a record of files in this directory and sub directories. This information" +
        		" can be used on a subsequent sync to work out which files have changed." +
        		" You may delete this file, but knowledge of which files have been deleted" +
        		" will be lost.");
        doc.appendChild(comment);
        
        batch.setAttribute("createdTime", String.valueOf(getCreateTime()));
        batch.setAttribute("basePath", getBasePath());
        batch.setAttribute("isNewBatch", String.valueOf(isNewBatch()));
        
        for (FileItem fileItem : getFileItems()) {
            Element item = doc.createElement("fileItem");
            item.setAttribute("type", fileItem.getType().toString());
            item.setAttribute("relPath", fileItem.getRelPath());
            item.setAttribute("modified", String.valueOf(fileItem.getLastModified()));
            item.setAttribute("length", String.valueOf(fileItem.getLength()));
            item.setAttribute("type", fileItem.getType().toString());   
            item.setAttribute("state", fileItem.getState().toString());
            batch.appendChild(item);
        }
        
        doc.appendChild(batch);

        TransformerFactory transfac = TransformerFactory.newInstance();
        Transformer trans = transfac.newTransformer();
        trans.setOutputProperty(OutputKeys.INDENT, "yes");

  
        StreamResult result = new StreamResult(out);
        trans.transform(new DOMSource(doc), result);

    }

    public synchronized Map<String, FileItem> getFileItemMap() {
        return new HashMap<String, FileItem>(this.fileItemMap);
    }

    public void setFileItemMap(Map<String, FileItem> fileItemMap) {
        this.fileItemMap = fileItemMap;
    }

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

    public void setBasePath(String basePath) {
        this.basePath = basePath;
    }

    public boolean isNewBatch() {
        return this.isNewBatch;
    }

    public void setNewBatch(boolean isNewBatch) {
        this.isNewBatch = isNewBatch;
    }

    public long getCreateTime() {
        return this.createTime;
    }

    public void setCreateTime(long createTime) {
        this.createTime = createTime;
    }    
}

