package org.wikiwebserver.core;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPOutputStream;

public class WikiMapSynchronizer {
	
	// The initial sync will send the last 24 hours of modifications
	private static final long INITIAL_SYNC_CATCH_UP = 24 * 60 * 60 * 1000;
	
	private WikiMap rootMap;
	private String target, password;
	private long lastSyncTime = System.currentTimeMillis() - INITIAL_SYNC_CATCH_UP;
	
	public WikiMapSynchronizer(WikiMap rootMap, String target, String password) {
		this.rootMap = rootMap;
		this.target = target;
		this.password = password;
	}
	
	public boolean synchronize() throws IOException {
		
		boolean syncComplete = false;
		long syncStartTime = System.currentTimeMillis();
		List<WikiMap> mapsToTransfer = new LinkedList<WikiMap>();
		populateTransferList(rootMap, mapsToTransfer);		
		
		if (mapsToTransfer.size() > 0) {
		
			ByteArrayOutputStream compressed = new ByteArrayOutputStream();
			GZIPOutputStream gzos = new GZIPOutputStream(compressed);
			ObjectOutputStream oos = new ObjectOutputStream(gzos);
	
			boolean dataToSend = writeMapData(mapsToTransfer, oos);
			oos.writeBoolean(false); // no more maps follow	
			oos.close();
			
			if (dataToSend) {
				byte[] syncData = compressed.toByteArray();					
				System.out.println("Posting modified maps to DR site (" + WareHouse.formatSize(syncData.length) + ")");			
				syncComplete = postMapData(syncData);
				if (syncComplete) {
					lastSyncTime = syncStartTime;
				}
			}
		}
		
		return syncComplete;
	}
	
	
	private void populateTransferList(WikiMap map, List<WikiMap> list) {
		
		if (requiresTransfer(map)) list.add(map);	
		
		for (Object value : map.values()) {
			if (value instanceof WikiMap) {
				// Check if children need to be transferred
				WikiMap innerMap = (WikiMap)value;
				if (requiresTransfer(innerMap) || childrenRequireScan(innerMap)) {
					populateTransferList(innerMap, list);
				}
			}
		}
	}
	
	private boolean writeMapData(List<WikiMap> list, ObjectOutputStream out) throws IOException {
		
		boolean dataWritten = false;
		
		for (WikiMap map : list) {
			
			Map<String, Object> plainMap = map.getPlainMapForSync();	
			
			if (plainMap.size() > 0) {
				// System.out.println("Sending " + map.getPath() + " (" + map.size() + ")");		
				out.writeBoolean(true);	// Map follows			
				out.writeObject(map.getHeirarchy());
				out.writeObject(map.getAccessorClassName());
				out.writeInt(map.getMaximumSize());	
				out.writeObject(plainMap);
				dataWritten = true;	
			}
		}
		
		return dataWritten;
	}
	
	private boolean postMapData(byte[] syncData) throws MalformedURLException, IOException {

		//System.out.println("Posting " + syncData.length + " bytes");

		HttpURLConnection drConn = (HttpURLConnection) new URL(target).openConnection();
		drConn.setRequestMethod("POST");
		drConn.setRequestProperty("X-Server-Password", password);
		drConn.setRequestProperty("User-Agent", getClass().getName());
		drConn.setRequestProperty("Content-Type", "application/octet-stream");
		drConn.setDoOutput(true);			
		drConn.setFixedLengthStreamingMode(syncData.length);
		
		// It may take a while to post the data
		drConn.setReadTimeout(60000);
		
		OutputStream out = drConn.getOutputStream();
		out.write(syncData);
		out.close();

		BufferedReader reader = new BufferedReader(new InputStreamReader(drConn.getInputStream()));
		StringBuilder response = new StringBuilder();
		String line = reader.readLine();
		while (line != null) {
			response.append(line + "\r\n");
			line = reader.readLine();
		}
		
		if (response.toString().startsWith("Accepted")) {
			return true;
		}		
		
		return false;
	}

	private boolean requiresTransfer(WikiMap map) {
		
	    if (lastSyncTime > map.getLastModifiedTime()) return false;		
		
		String[] heirachy = map.getHeirarchy();
		
		// Must be unique on each server if running active-active
	    if (map.getName().equals("Identities")) return false;	    
	    
		// No need to propagate status information
	    if (map.getName().equals("WikiWebServer")) return false;		    
		
	    // Don't sync all statistics
	    if (map.getName().equals("Statistics")) return false;
		if (heirachy.length == 2) {
			if (heirachy[0].equals("Statistics")) {
				if (heirachy[1].endsWith("Time")) return false;
				if (heirachy[1].endsWith("Total")) return false;
			}
		}
		
	    
		// Don't sync main browser list
	    if (map.getName().equals("Browser")) return false;				

	    return true;
	}
	
	private boolean childrenRequireScan(WikiMap map) {
	    return (lastSyncTime < map.getChildModifiedTime());
	}	
	
}

