package org.wikiwebserver.html;

import static org.wikiwebserver.html.HTMLHelper.LF;
import static org.wikiwebserver.html.HTMLHelper.h;
import static org.wikiwebserver.html.HTMLHelper.incorporateCSS;
import static org.wikiwebserver.html.HTMLHelper.incorporateJavaScript;
import static org.wikiwebserver.html.HTMLHelper.javaScript;
import static org.wikiwebserver.html.HTMLHelper.p;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Semaphore;

import org.wikiwebserver.core.WareHouse;
import org.wikiwebserver.core.WikiMap;
import org.wikiwebserver.handler.http.HTTPException;
import org.wikiwebserver.handler.http.HTTPHandler;
import org.wikiwebserver.handler.http.interfaces.HTTPResponder;
import org.wikiwebserver.html.plugin.Plugin;

public abstract class TemplatedPage extends BasicTemplatedPage implements HTTPResponder {
    
    private String siteTitle;
    private String siteSlogan;    
    private String title;    
    
    private static final int SLOW_RESPONSE_THRESHOLD = 2000;
    private static final int SLOW_RESPONSE_CONCURRENCY_LIMIT = 2;
    

    protected abstract void generate() throws HTTPException;

    protected void ajax() throws HTTPException {

    }    
   
    public void init(HTTPHandler conn) throws HTTPException {
        super.init(conn);
        
        setResourceRoot(DEFAULT_TEMPLATE_ROOT);
        setTemplatePath(DEFAULT_TEMPLATE_PATH);

        if (getPageType() == PAGE_TYPE_UNKNOWN) {
            // Force mobile page type if sub domain is m
            String host = getRequest().getHeaders().getFirst("Host");
            if (host != null && host.startsWith("m.")) {
                setPageType(PAGE_TYPE_MOBILE);
            }   
            // Force audio type if sub domain is audio
            /*
            else if (host != null && host.startsWith("audio.")) {
                setPageType(PAGE_TYPE_AUDIO);
            } 
            */    
            else {
                setPageType(PAGE_TYPE_DESKTOP);
            }
        }
        
        addCSSLink(DEFAULT_CSS_FILENAME);   
              
        addPlugin(page.plugin.ClassDetails.class);
        addPlugin(page.plugin.ClassComments.class);
              
    }     


    public Object respond(HTTPHandler conn) throws IOException {

/*
        if (getPageType() == TemplatedPage.PAGE_TYPE_AUDIO) {
            generate();
            AudioPageReformatter formatter = new AudioPageReformatter();
            String text = formatter.reformat(getBody());
            return new Say().respond(text, conn);
        }
*/

        if (getFormData() != null) {
            String ajax = getFormData().getFirst("ajax");
            if (ajax != null) {
                try {
                    ajax();
                } catch (Exception ex) {
                    String message = ex.getMessage() + "\\n\\n" +
                        WareHouse.formatStackTrace(ex.getStackTrace(), false, true);                    
                    append("alert('" + message + "');");
                }
                return getBody();
            }
        }
        
        int avgResponse = 0;
        synchronized (totalResponseTimes) {
            Long totalResp = totalResponseTimes.get(getUrl());
            Integer numResp = totalResponses.get(getUrl());
            if (totalResp != null && numResp != null) {
                avgResponse = (int) (totalResp / numResp);
            }
        }
        if (avgResponse < SLOW_RESPONSE_THRESHOLD) {
            long startTime = System.currentTimeMillis();
            generate();

            synchronized (totalResponseTimes) {
                Long totalResp = totalResponseTimes.get(getUrl());
                Integer numResp = totalResponses.get(getUrl());
                if (totalResp == null) totalResp = new Long(0);      
                if (numResp == null) numResp = new Integer(0);   
                totalResp = totalResp.longValue() + (System.currentTimeMillis() - startTime);
                numResp = numResp.intValue() + 1;
                totalResponseTimes.put(getUrl(), totalResp);
                totalResponses.put(getUrl(), numResp);
            }
        }
        // Slow classes have the level of concurrency limited
        else {
            boolean permitAquired = concurrencyLimiter.tryAcquire();
            if (permitAquired) {
                // Invoke the generate method implemented by the subclass                
                try { generate(); }
                finally { concurrencyLimiter.release(); }
            } else {
                getResponse().setCode(500);
                getResponse().setInfo("Slow page concurrency limit reached");
                canBeCached = false;
                setTitle("Server Busy");
                append(h(1, "Server Busy") +
                       h(2, "Slow page concurrency limit reached")+
                       p("This page often takes a while to complete and WikiWebServer is" +
                		" currently processing " + SLOW_RESPONSE_CONCURRENCY_LIMIT + 
                		" slow requests. To reduce server load, this page request" +
                		" has been ignored. Please refresh the page in a few seconds."));
            }
        }
        

        return getPopulatedTemplate();
    }

    public void addResourceRoot(String path) {
        if (!path.endsWith("/")) path += "/";
        this.resourceRoots.add(path);
    }      
    
    public String getAbsolutePath(String resource) {

        if (resource == null) return null;
        
        if (resource.startsWith("http://")) return resource;
        
        if (resource.startsWith("?")) return resource;
        
        if (resource.startsWith("/")) return resource;
        
        // Search resource roots for resource
        List<String> roots = new ArrayList<String>(resourceRoots);
        Collections.reverse(roots);
        
        // Search template roots locally
        Iterator<String> i = roots.iterator();
        while (i.hasNext()) {
            String abs = i.next() + resource;
            File file = new File(abs.substring(1));
            try {
                if (file.exists()) return abs;

            } catch (SecurityException ex) { 
                ex.printStackTrace(); 
            }
        }
        
        // Search basic template root
        String parentAbs = super.getAbsolutePath(resource);
        if (new File(parentAbs.substring(1)).exists()) {
            return parentAbs;
        }
        
        // Search roots remotely
        i = roots.iterator();
        while (i.hasNext()) {
            String abs = i.next() + resource;
            try {
                File file = WareHouse.getResourceFile(abs.substring(1));
                if (file != null && file.exists()) {
                    return abs;
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }

        // Use basic resource root
        return parentAbs;
    }     

    public void setTitle(String title) {
        this.title = title;
    }

    public void setSiteTitle(String title) {
        this.siteTitle = title;
    }

    public void setSiteSlogan(String slogan) {
        this.siteSlogan = slogan;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public void setKeywords(Collection<String> keywords) {
        this.keywords = keywords;
    }

    public void addCSSLink(String url) {
        cssLinks.add(url);
    }

    public void addJavascriptLink(String url) {
        javascriptLinks.add(url);
    }
    
    public void addPlugin(Class<? extends Plugin> clazz) {
    	if (!pluginClasses.contains(clazz)) {
    		pluginClasses.add(clazz);
    	}
    }  
    
    public void removePlugin(Class<? extends Plugin> clazz) {
        Collection<Class<? extends Plugin>> copy 
                = new ArrayList<Class<? extends Plugin>>(pluginClasses);
        
        for (Class<? extends Plugin> pluginClass : copy) {
            if (pluginClass.equals(clazz)) {
                pluginClasses.remove(pluginClass);
            }
        }
    }

    public String include(String path) {
        path = getAbsolutePath(path);        
        return HTMLHelper.include(path);
    }

    public String include(String path, String errorIfFail) {
        path = getAbsolutePath(path);
        return HTMLHelper.include(path, errorIfFail);
    }

    public TemplatedPage setPeriodicAjaxUpdateEnabled(boolean enabled) {      
        addJavascriptLink("/page/tools/html/ajax.js");
        if (enabled) {
            append(javaScript("startPeriodicAjaxUpdate();" + LF));
            // Stop updating after 10 minutes to prevent bandwidth consumption
            int delay = 600000;
            try {
               delay = Integer.parseInt(getFormData().getFirst("timeout"));
            } catch (Exception ignored) {}

            if (delay > 0) {
                String safe = getServiceAddress();
                append(javaScript("setTimeout(\"window.location='" + safe + "'\", " + delay + ");"));
            } 
        } 
        else {
            append(javaScript("stopPeriodicAjaxUpdate();" + LF));
        }
        return this;
    }

    protected void setUpdatePeriod(int period) {
        append(javaScript("updatePeriod = " + period + ";" + LF));
        this.updatePeriod = period;
    }

    protected int getUpdatePeriod() {
        return updatePeriod;
    } 
    
    public void voidReturnToReferrerIfPossible() throws HTTPException {
        String referer = getRequest().getHeaders().getFirst("Referer");
        if (referer != null) {
            throw new HTTPException(307, "Return to referer", referer);
        }        
    }    
    
    public String image(String src, String alt) {
        return image(src, alt, null);
    }

    public String image(String src, String alt, String additional) {

        src = getAbsolutePath(src);   
        if (getPageType() == TemplatedPage.PAGE_TYPE_MOBILE) {
            // Resize static images
            if (!src.endsWith(".class") && !src.contains(".class?")) {
                src = WareHouse.getUrlPathForClass(page.image.ImageResizer.class)
                    + "?path=" + WareHouse.formDataEncode(src) + "&amp;s=80";
            }
        }
      
        return HTMLHelper.image(src, alt, additional);
    }

    public String thumbnail(String src, String alt, int maxDim) {
        src = getAbsolutePath(src);        
        return HTMLHelper.thumbnail(src, alt, maxDim);
    }

    public String thumbnail(String src, String alt, int maxDim, String additional) {
        src = getAbsolutePath(src);    
        return HTMLHelper.thumbnail(src, alt, maxDim, additional);
    }
    
    public String form(String content) {
        return HTMLHelper.form(content, this.getUri());
    }
    
    public boolean isSet(String s) {
        return (s != null && s.length() > 0);
    }    

    public void setPageType(int pageType) {
        this.pageType = pageType;
        if (pageType == PAGE_TYPE_MOBILE) {
            addResourceRoot(DEFAULT_MOBILE_TEMPLATE_ROOT);
        }
        else if (pageType == PAGE_TYPE_DESKTOP) {
            if (getHandler().getProtocol().equals("https")) {
                addJavascriptLink(JQUERY_PATH_LOCAL);
            }
            else addJavascriptLink(JQUERY_PATH_CDN);
        }
    }

    public int getPageType() {
        return pageType;
    }
    
    public String getCacheKey() {
        if (canBeCached) { 
            return super.getCacheKey() + pageType;
        }
        else return String.valueOf(Math.random());
    }     
    
    public long getExpireTime() {
        if (canBeCached) { 
            return super.getExpireTime();
        }
        else return System.currentTimeMillis();
    }    
    
    public WikiMap getPageData() {
        String pageClassName = getResponse().getData().getClass().getName();        
        WikiMap data = WareHouse.getWikiMap("PageData", pageClassName);
        
        if (data == null) {
            data = WareHouse.initWikiMap(10000, getClass().getName(), "PageData", pageClassName);
        }
        
        return data;
    }    

    protected String getHead() {
        StringBuilder head = new StringBuilder(1024);
        if (this.title != null && head.indexOf("<title>") == -1) {
            head.append("<title>" + title + "</title>" + LF);
        }
        if (this.description != null) {
            head.append("<meta name='description' content='" + this.description
                    + "'>" + LF);
        }

        if (keywords.size() > 0) {
            head.append("<meta name='keywords' content='");
            int count = 0;
            for (String keyword : keywords) {
                if (count++ > 0) head.append(", ");
                head.append(keyword);
            }   
            head.append("'>" + LF);
        }        

        // If the browser is unknown, embed all css and javascript into the page
        boolean embed = getHandler().getRequest().getHeaders().getRequestCookies().size() == 0 && 
                        EMBED_EXTERNALS_FOR_BASIC_BROWSER;
        
        for (String url : cssLinks) {
            url = getAbsolutePath(url);
            head.append(incorporateCSS(url, embed));
        }
        for (String url : javascriptLinks) {
            url = getAbsolutePath(url);
            head.append(incorporateJavaScript(url, embed));
        }
        


        head.append(super.getHead().toString());

        return head.toString();
    }
    
    protected void addTemplateModifications(Map<String, String> changes) throws IOException {

        String className = getResponse().getData().getClass().getName();

        changes.put("<!-- SITE_TITLE -->", siteTitle);
        
        changes.put("<!-- SITE_TITLE -->", siteTitle);
        changes.put("<!-- SITE_SLOGAN -->", siteSlogan);

        changes.put("<!-- THIS_URL -->", WareHouse.escapeHTMLEntities(getUrl()));
        changes.put("<!-- THIS_URI -->", WareHouse.escapeHTMLEntities(getUri()));
        changes.put("<!-- THIS_QUERY -->", WareHouse.escapeHTMLEntities(getQuery()));
        
        String millis = String.valueOf(System.currentTimeMillis());
        changes.put("<!-- CURRENT_TIME_MILLIS -->", millis);



        String sourceLocation = "/" + className.replace('.', '/') + ".java";
        String editURL = WareHouse.SOURCE_EDITOR_URL + "?path=" + sourceLocation;
        changes.put("<!-- CLASS_NAME -->", className);
        changes.put("<!-- EDIT_SOURCE_URL -->", WareHouse.escapeHTMLEntities(editURL));


        
        if (getPageType() != PAGE_TYPE_MOBILE) {
            addJavascriptLink("/page/tools/html/ajax.js");
            
            Collection<Plugin> plugins = new ArrayList<Plugin>();
            StringBuilder pluginButtons = new StringBuilder();
            for (Class<? extends Plugin> pluginClass : pluginClasses) {
                try {
                    Plugin plugin = (Plugin) pluginClass.newInstance();
                    String button = plugin.getActivationButton(this);
                    if (button != null) {
                        pluginButtons.append(button);
                        plugins.add(plugin);                        
                    }
                } catch (Exception ex) {
                    pluginButtons.append("[FAILED TO INIT PLUGIN - " + pluginClass + "]");
                    ex.printStackTrace();
                }
            }       
            changes.put("<!-- PLUGIN_ACTIVATION_BUTTONS -->", pluginButtons.toString());
            
            StringBuilder pluginComponents = new StringBuilder();
            for (Plugin plugin : plugins) {
                String component = plugin.getComponent();
                if (component != null) pluginComponents.append(component);
            }       
            changes.put("<!-- PLUGINS -->", pluginComponents.toString());                   
        }        
        
        super.addTemplateModifications(changes);
    }



    
    private String description = null;
    
    private Collection<String> keywords = new LinkedHashSet<String>();
    private Collection<String> cssLinks = new LinkedHashSet<String>();
    private Collection<String> javascriptLinks = new LinkedHashSet<String>();
    private Collection<Class<? extends Plugin>> pluginClasses = new ArrayList<Class<? extends Plugin>>();
    
    private int updatePeriod = 1000;
    
    private int pageType = PAGE_TYPE_UNKNOWN;
    
    private Collection<String> resourceRoots = new ArrayList<String>();   
    
    private boolean canBeCached = true;
    private static final Map<String, Long> totalResponseTimes = new HashMap<String, Long>();
    private static final Map<String, Integer> totalResponses = new HashMap<String, Integer>();
    private static final Semaphore concurrencyLimiter = new Semaphore(SLOW_RESPONSE_CONCURRENCY_LIMIT, true);

    public static final int PAGE_TYPE_UNKNOWN = 0;
    public static final int PAGE_TYPE_DESKTOP = 1;
    public static final int PAGE_TYPE_MOBILE = 2;
    public static final int PAGE_TYPE_AUDIO = 3;

    private static final String DEFAULT_TEMPLATE_ROOT = "/templates/default/";
    private static final String DEFAULT_MOBILE_TEMPLATE_ROOT = "/templates/mobile_default/";
    private static final String DEFAULT_TEMPLATE_PATH = "template.html";

    private static final String DEFAULT_CSS_FILENAME = "screen.css";
    
    private static final String JQUERY_PATH_CDN = "http://code.jquery.com/jquery-latest.js";
    private static final String JQUERY_PATH_LOCAL = "/lib/jquery-1.2.3.js";
    
    private static final boolean EMBED_EXTERNALS_FOR_BASIC_BROWSER = false;
}

