package org.wikiwebserver.core;

import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.*;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;

import org.wikiwebserver.core.WareHouse;

/**
 * This class contains the main method used for starting WikiWebServer.
 *
 * Changes to this class, together with other classes in the core package
 * will only take effect after WikiWebServer is restarted.
 * 
 * @author Michael Gardiner
 */
public class WikiWebServer {
    
    public static final String VERSION_CODE = "1.7";   

    public static String getCommandLineOptions() {
        String sep = File.pathSeparator;
        String clo = 
            "Usage: java -jar WikiWebServerCore.jar listen-port|config-file\n\n" +    
            " Tip: Specify a config file that does not exist and it will be populated with\n" +
            " default values automatically.\n\n" +
            " Additional JVM options recommended:\n\n" +
            " WikiWebServer's classloader will automatically look in the library path two\n" +
            " levels deep and on backup servers for required resources. Some libraries that\n" +
            " do not use this classloader will need to be specified on the Java classpath.\n\n" +
            " -cp \"lib/freetts-1.2.1/cmu_us_kal.jar" + sep + ".\n\n" +                
            "  -Djavax.net.ssl.keyStore=KEY_STORE_FILE\n" +
            "  -Djavax.net.ssl.keyStorePassword=KEY_STORE_PASSWORD\n" +
            "  -Djava.awt.headless=true\n" +
            "  -Xms16m\n" +
            "  -Xmx256m\n" +
            "  -Xss128k\n\n" +
            " The SecurityManager will only permit reading resources within the classpath\n" +
            " it is therefore important to include additional paths that need to be\n" +
            " accessible at runtime.";
        
        return clo;
    }
    
    private static WikiMap getWikiData() {
        WikiMap map = WareHouse.getWikiMap("WikiWebServer");
        if (map == null) map = WareHouse.initWikiMap("WikiWebServer");        
        return map;
    }
    
    public static void main(String[] args) {
        
        if (args.length == 0) {
            System.out.println(getCommandLineOptions());
            System.exit(1);
        }
        
        int port = 8080;
        try {
        	// The first argument is a port number        	
        	port = Integer.parseInt(args[0]);
        }
        catch (NumberFormatException ex) {
        	// The first argument is a configuration file
        	ConfigManager.load(new File(args[0]));
        	port = ConfigManager.getInt("wikiwebserver-handler-bind-port");
        }
  
        try {        
            
            // Configure some system defaults
            // The Internet changes, and it is a bad assumption that a
            // DNS entry always points at the same IP address.
            System.setProperty("networkaddress.cache.ttl", 
            		ConfigManager.getString("networkaddress.cache.ttl"));
            System.setProperty("networkaddress.cache.negative.ttl", 
            		ConfigManager.getString("networkaddress.cache.negative.ttl"));
            
            // More network related optimisations
            System.setProperty("http.keepAlive", ConfigManager.getString("http.keepAlive"));
            System.setProperty("http.maxConnections", ConfigManager.getString("http.maxConnections"));
            System.setProperty("sun.net.client.defaultConnectTimeout", 
            		ConfigManager.getString("sun.net.client.defaultConnectTimeout"));
            System.setProperty("sun.net.client.defaultReadTimeout", 
            		ConfigManager.getString("sun.net.client.defaultReadTimeout"));
            
            String serverId = WareHouse.staticIdentity("ServerID");
            String userAgent = "WikiWebServer/" + VERSION_CODE + " (" + serverId + ")";
            System.setProperty("http.agent", userAgent);
            
            // Before applying a custom security manager we need to set
            // up a few things to avoid exceptions later on
            prepare();        
            
            // Populate backup servers
            String classServersString = ConfigManager.getString("class-servers");
            URL[] classServers = null;
            if (classServersString != null && classServersString.startsWith("http")) {
                String[] urls = classServersString.split(",");
                classServers = new URL[urls.length];
                for (int i=0; i<urls.length; i++) {
                	classServers[i] = new URL(urls[i]);
                }            
            }            
            classUrls = classServers;
            
            // Location to look for libs
            libsDir = new File(ConfigManager.getString("library-location"));           
            


            // Echo Server
            ConnectionListener echoServer = 
                ServiceFactory.createService("Echo", 
                			   ConfigManager.getInt("echo-handler-bind-port"), 
                			   ConfigManager.getString("echo-handler-bind-address"),
                			   ConfigManager.getString("echo-handler-class"),
                			   null,
                			   false);
            
            // IP to country lookup
            ConnectionListener ipToCounrtyServer = 
                ServiceFactory.createService("IPToCountry", 
         			   ConfigManager.getInt("iptocountry-handler-bind-port"), 
         			   ConfigManager.getString("iptocountry-handler-bind-address"),
        			   ConfigManager.getString("iptocountry-handler-class"),
        			   null,
        			   false);         

            
            // Plain text WikiWebServer
            ConnectionListener wikiWebServer = 
                ServiceFactory.createService("WikiWebServer", port, 
                	   ConfigManager.getString("wikiwebserver-handler-bind-address"),
                	   ConfigManager.getString("wikiwebserver-handler-class"),
        			   ConfigManager.getString("wikiwebserver-handler-configuration-class"),
        			   false);     
            
            // Secure WikiWebServer
            ConnectionListener sslWikiWebServer = 
                ServiceFactory.createService("SSL WikiWebServer", 
           			   ConfigManager.getInt("ssl-wikiwebserver-handler-bind-port"), 
           			   ConfigManager.getString("ssl-wikiwebserver-handler-bind-address"), 
        			   ConfigManager.getString("ssl-wikiwebserver-handler-class"), 
        			   ConfigManager.getString("wikiwebserver-handler-configuration-class"),
        			   true);
           

            // Enable the security manager
            System.setSecurityManager(new SecurityMan(
            		ConfigManager.getString("writable-location"), 
            		ConfigManager.getString("not-writable-location"), 
            		ConfigManager.getString("server-password")));   
            
            // A shutdown must trigger saving unsaved data
            Runtime.getRuntime().addShutdownHook(new Thread() {
                public void run() {
                    System.out.println("WikiWebServer shutting down");
                    WareHouse.persistMaps();
                    WareHouse.flushLogs();
                }
            });   
            
            
            WikiMapSynchronizer disasterRecoverySynchronizer = null;
            if (ConfigManager.getBoolean("disaster-recovery-enabled")) {
            	disasterRecoverySynchronizer = new WikiMapSynchronizer(
            			WareHouse.getRootWikiMap(),
            			ConfigManager.getString("disaster-recovery-end-point"),
            			ConfigManager.getString("disaster-recovery-server-password"));
            }
            
        
            
            // Remove the SSL key store password now that SSL server socket configuration complete
            System.setProperty("javax.net.ssl.keyStorePassword", "Removed for security");  
            ConfigManager.clearSensitiveConfigurationData();
            
            
            ServiceFactory.startServices(new ConnectionListener[] { 
                    wikiWebServer, sslWikiWebServer, echoServer, ipToCounrtyServer } 
            );
            
            // The main thread is used for maintenance operations
            Thread.currentThread().setName("Maintenance");
            maintenance(new ConnectionListener[] { 
                    wikiWebServer, sslWikiWebServer, echoServer, ipToCounrtyServer },
                    disasterRecoverySynchronizer
            );
            
        } catch (MalformedURLException e) {
            RuntimeException re = new RuntimeException("Malformed backup URL");
            re.initCause(e);
            throw re;
        }
    }
    
   
    
    private static void prepare() {
        
        // Somewhere to store logs
        File logRoot = new File(WareHouse.LOG_ROOT);
        logRoot.mkdirs();     
        
        // The following are often blocked by the security manager or custom class
        // loader during initialisation.  To avoid this problem, perform initialisation 
        // before applying the security manager and custom class loader.
        
        try {
        
            // Initialise AWT graphics rendering
            BufferedImage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
            Graphics2D g2d = image.createGraphics();
            Font font = g2d.getFont();
            FontMetrics fm = g2d.getFontMetrics();
            g2d.setFont(new Font(font.getFontName(), fm.getAscent(), Font.PLAIN));
            image.flush();
            
            // Initialise regional date settings
            DateFormat dt = new SimpleDateFormat();
            dt.format(new Date());
            
            // Initialise time zones
            dt.setTimeZone(TimeZone.getTimeZone("GMT"));
            
            // Initialise HTTP factory
            URL http = new URL("http://www.google.com");
            Reader httpReader = new InputStreamReader(http.openStream(), "UTF8");
            WareHouse.readerToString(httpReader);
            
            // Initialise HTTPS factory
            URL https = new URL("https://www.google.com");
            Reader httpsReader = new InputStreamReader(https.openStream(), "UTF8");
            WareHouse.readerToString(httpsReader);       
            
            // Initialise ImageIO for image processing
            javax.imageio.ImageIO.read(new URL("http://www.google.com/intl/en_ALL/images/logo.gif"));        
        }
        catch (Exception ex) {
            System.err.println("Failed to complete WikiWebServer preparation");
            ex.printStackTrace();
        }
        
    }
    
    public static String getMaintenceTask() {
        return (String) getWikiData().get("maintenanceTask");
    }
    
    public static long getMaintenceTaskStartTime() {
        Long st = (Long) getWikiData().get("maintenanceTaskStartTime");
        if (st == null) st = new Long(0);
        return st;
    }      
    
    private static void maintenance(ConnectionListener[] servers, WikiMapSynchronizer drSyncer) {

        Runtime tim = Runtime.getRuntime();
        
        int persistMapPeriod = ConfigManager.getInt("persist-map-period");
        int flushLogPeriod = ConfigManager.getInt("flush-log-period");
        int unloadDelay = ConfigManager.getInt("unaccessed-map-unload-delay"); 
        int lowMemoryTimeAllowance = ConfigManager.getInt("low-memory-time-allowance"); 
        int serviceTestPeriod = ConfigManager.getInt("service-test-period");     
        int drSyncPeriod = ConfigManager.getInt("disaster-recovery-sync-period");           

        long memLimitTime = 0;
        long time = System.currentTimeMillis();  
        long serviceTestTime = time; 
        long drSyncTime = time;
        long persistMapTime = time;
        long flushLogTime = time;
        long classLoaderSetTime = time;
        
        try {
            while (!Thread.currentThread().isInterrupted()) {
                
                String generalMessage = "Sleeping"; // default message when sleeping
                
                time = System.currentTimeMillis();    
    
                // If VM is low on memory for a prolonged period, end maintenance
                long dangerouslyLowMemory = tim.maxMemory() / 8;
                long availableMemory = tim.freeMemory() + (tim.maxMemory() - tim.totalMemory());
                if (availableMemory < dangerouslyLowMemory) {
                	generalMessage = "Warning: Free VM memory is low.";
                    System.out.println(generalMessage);
                    // Empty WikiCache to free some memory
                    WareHouse.getWikiCache().clear();
                    if (memLimitTime == 0) memLimitTime = time;
                    if (time > memLimitTime + lowMemoryTimeAllowance) {
                        throw new Exception("Available VM memory for low for a prolonged period.");                   
                    }
                    // Suggest garbage collection
                    System.gc();
                // Memory available above threshold, cancel
                } else memLimitTime = 0;
                

                // Test services
                if (time > serviceTestTime + serviceTestPeriod) {
                    // If a server is not accessible, end maintenance                    
                    for (ConnectionListener server : servers) {
                        if (server != null) {
                            String name = server.getName();
                            int port = server.getPort();
                            Socket test = null;
                            try {
                                setMaintenanceTask("Testing service " + name + " on port " + port);
                                test = new Socket();
                                test.bind(null);
                                test.connect(server.getAddress(), 500);  
                                test.getOutputStream().write("\r\n".getBytes());
                            } catch (IOException ex) {
                                String msg = name + " on port " + port + " is not accessible.";
                                IOException sex = new IOException(msg);
                                sex.initCause(ex);
                                throw sex;
                            } finally {
                                if (test != null) test.close();
                            }
                        }
                    }
                    serviceTestTime = time;
                }
                
                // Save persistent maps
                if (time > persistMapTime + persistMapPeriod) {
                    setMaintenanceTask("Persisting map data");
                    WareHouse.persistMaps();
                    persistMapTime = time;
                }
                
                // Flush persistent logs                
                if (time > flushLogTime + flushLogPeriod) {
                    setMaintenanceTask("Flushing logs");
                    WareHouse.flushLogs();
                    flushLogTime = time;
                }
                
                
                // Clear old persistent data from memory
                setMaintenanceTask("Unloading unused persistent data");
                long expirePoint = time - unloadDelay;
                WareHouse.getWikiMap().unloadIfNotAccessedSince(expirePoint);
                
                
                // Service connections
                for (ConnectionListener server : servers) {
                    if (server != null) {
                        String name = server.getName();
                        int port = server.getPort();
                        setMaintenanceTask("Cleaning service " + name + " on port " + port);                       
                        server.serviceConnections();
                    }
                }
                
                // Synchronise disaster recovery site
                if (drSyncer != null && time > drSyncTime + drSyncPeriod) {
                	try {
                		setMaintenanceTask("Synchronizing data with disaster recovery site");  
                		drSyncer.synchronize();
                	}
                	catch (IOException ex) {
                		generalMessage = "Failed to synchronize with disaster recovery site";
                		System.err.println(generalMessage);
                		ex.printStackTrace();
                	}
                	drSyncTime = time;
                }
                
                
                // If a change is detected issue new class loader
                boolean modified = (WareHouse.getLastFileChangeTime() > classLoaderSetTime);
                if (modified) {
                    ClassLoader parent = Thread.currentThread().getContextClassLoader();
                    WikiClassLoader classLoader = new WikiClassLoader(getClassUrls(), parent); 
                    classLoaderSetTime = System.currentTimeMillis();
                    for (ConnectionListener server : servers) {
                        if (server != null) {
                            server.setHandlerClassLoader(classLoader);
                        }
                    }
                }                 
    
                setMaintenanceTask(generalMessage);
                Thread.sleep(500); 
            }
        }
        catch (Throwable t) {
        	String msg = "Maintenance failed, VM exiting.";
            setMaintenanceTask(msg);
            System.err.println(msg);
            t.printStackTrace();  
            WareHouse.persistMaps();
            WareHouse.flushLogs();
            Runtime.getRuntime().halt(0);
        }            
    }    
    
    private static void setMaintenanceTask(String task) {
        Long time = new Long(System.currentTimeMillis());
        
        getWikiData().put("maintenanceTaskStartTime", time);
        getWikiData().put("maintenanceTask", task);
    }     
    
    public static URL[] getClassUrls() throws MalformedURLException {
        List<URL> urls = new ArrayList<URL>();
        urls.add(new File(".").toURI().toURL());

        // Search the library directory for archives, 2 deep
        if (libsDir != null && libsDir.isDirectory()) {
            for (File libFile : libsDir.listFiles()) {
                if (libFile.isDirectory()) {
                    for (File subLibFile : libFile.listFiles()) {
                        String name = subLibFile.getName();
                        if (name.endsWith(".jar") || name.endsWith(".zip")) {
                            urls.add(subLibFile.toURI().toURL());
                        }
                    }
                }                
                String name = libFile.getName();
                if (name.endsWith(".jar") || name.endsWith(".zip")) {
                    urls.add(libFile.toURI().toURL());
                }
            }
        }        
        
        // Add additional URLs eg backup servers
        if (classUrls != null) {
            for (URL classUrl : classUrls) {
                urls.add(classUrl);
            } 
        }

        return urls.toArray(new URL[urls.size()]);
    }    

    // Where the custom class loader should look for classes
    private static URL[] classUrls;
    private static File libsDir; 
}
