package page.distribute;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.StringTokenizer;

import org.wikiwebserver.core.WareHouse;
import org.wikiwebserver.distribute.interfaces.Task;
import org.wikiwebserver.distribute.interfaces.WorkerNode;
import org.wikiwebserver.distribute.se.worker.task.DirectoryListingTask;
import org.wikiwebserver.distribute.se.worker.task.ImageListingTask;
import org.wikiwebserver.distribute.server.NodeOfflineException;
import org.wikiwebserver.distribute.server.TaskStub;
import org.wikiwebserver.distribute.server.WorkerNodeManager;
import org.wikiwebserver.distribute.util.FileDetails;
import org.wikiwebserver.distribute.util.PersistableVector;
import org.wikiwebserver.handler.http.HTTPException;
import org.wikiwebserver.handler.http.HTTPHandler;
import org.wikiwebserver.handler.http.interfaces.HTTPResponder;

import page.config.SiteTemplatedPage;
import page.tools.entity.NodeData;
import page.tools.stats.NodeInfo;

import static org.wikiwebserver.html.HTMLHelper.*;


public class FileBrowser extends SiteTemplatedPage implements HTTPResponder {
	
	private static final long DEFAULT_EXPIRE = 48 * 60 * 60 * 1000;
	private static final long TIME_DELAY_WARNING_THRESHOLD = 2 * 1000;
	// The amount of time a retry occurs before the connection time of the node.
	private static final int ESTIMATED_CONNECTION_LATENCY = 500;
	
	public static final int THUMBNAIL_SIZE = ImageListingTask.THUMBNAIL_SIZE;
    public static final int NUM_THUMB_COLS = ImageListingTask.NUM_THUMB_COLS;
    public static final int NUM_THUMB_ROWS = ImageListingTask.NUM_THUMB_ROWS;
    public static final int THUMB_HORIZ_PADDING = ImageListingTask.THUMB_HORIZ_PADDING;
    public static final int THUMB_VERT_PADDING = ImageListingTask.THUMB_VERT_PADDING;	
	
	private final WorkerNode node;
	private final NodeData nodeData;
	private final String path;
	
	private String mode;
	private String downloadFileName;
	private int offset;
	
	public FileBrowser(WorkerNode node, String mode, String path) {
		this.node = node;
		this.nodeData = NodeData.getNodeDataById(node.getNodeId());   
		this.mode = mode;
		this.path = path;		
	}
	
	public void init(HTTPHandler conn) throws HTTPException {
		super.init(conn);
        if (getFormData() != null) {             
        	downloadFileName = getFormData().getFirst("downloadFileName");        
            String offsetString = getFormData().getFirst("offset");
            if (offsetString != null) {
                offset = Integer.parseInt(offsetString);
            }
        }
	}
	
    public void generate() throws HTTPException {
        
        setTitle("Browse node - WikiWebServer.org");
        
    	addCSSLink("/templates/uk/co/mydisk/mydisk.css");  
    	
        if (node == null || nodeData == null) {
            getHandler().getResponse().setCode(404);
            getHandler().getResponse().setInfo("Node not found");            
            getHandler().getResponse().getHeaders().set("Cache-Control", "no-cache");   
            append(h(1, "Node not found"));
            append(p("The node specified could not be found. Remember node names are case sensitive."));
            return;
        }
        
    	
        
        if (nodeData.isBlocked()) {
            getHandler().getResponse().setCode(404);
            getHandler().getResponse().setInfo("Node blocked");
            getHandler().getResponse().getHeaders().set("Cache-Control", "no-cache");               
            append(h(1, "Node blocked"));
            append(p("The node has been actively blocked and can not be accessed."));  
            return;            
        }
        
        
    	
    	String password = null;
    	
    	// Is a new node password specified?
    	if (getFormData() != null) {    
    	    password = getFormData().getFirst("password");
    	    if (password != null) {
    	        getBrowser().put("nodeTaskPassword", password);
    	        String url = getServiceAddress() + getUri();
    	        throw new HTTPException(302, "Password accepted", url);
    	    }
    	}
    	
    	if (mode == null) {
    	    mode = "file";
    	}
    	
    	// use a previously used password
    	if (password == null && getBrowser() != null) {
    	    password = (String) getBrowser().get("nodeTaskPassword");
    	}

        long waitTime = 0;
        try {
        	waitTime = node.getWaitTime();
        	
        } catch (NodeOfflineException ex) {
            getHandler().getResponse().setCode(504);
            getHandler().getResponse().setInfo("Node offline");            
        	getHandler().getResponse().getHeaders().set("Cache-Control", "no-cache");   
        	append(h(1, "Communication problem"));
            append(p("The node appears to be offline."));
            append(getGoogleAdsenseBlock("pub-7253309958196609", "5421269628", 728, 90));
            
/*
            append(getWaitAdvert("Want to access your files when your computer is switched off?", 
                    "You might like to consider hosting your files on a server. Not only will" +
                    " your files be available when your computer is switched turned off, they will" +
                    " also transfer faster and be backed up too!"));    
*/            
            
            return;
        }
        
        String rootPath = getUrl().substring(0, getUrl().indexOf("/", 7)+1);
        String profile = WareHouse.getUrlPathForClass(NodeInfo.class) + "?nodeID=" + nodeData.getId();
        String nodeName = "Node name: " + a(profile, nodeData.getName());
        String trail = "Path: " + generatePathLinks(rootPath, path);        
        
        // Display a visual waiting page if we know a connection from the node
        // is going to take some time.
        if (waitTime > TIME_DELAY_WARNING_THRESHOLD && getPageType() == PAGE_TYPE_DESKTOP) {	
        	
        	String href = getServiceAddress() + getUri();
        	
        	// Sneaky javascript so we dont display the timer and redirect when back is pressed.
        	String pageSequence = getRequest().getHeaders().getRequestCookies().get("sequenceID");
        	if (pageSequence == null) pageSequence = "0";
        	int pageSequenceId = Integer.parseInt(pageSequence);
        	
        	long retryDelay = waitTime-ESTIMATED_CONNECTION_LATENCY;
        	
        	String directionAwareJavascript =
	        	"var pageSequenceId = " + pageSequenceId + ";    " + LF +
	        	"var clientSquenceId = getCookie('sequenceID');  " + LF +
	        	"if (clientSquenceId > pageSequenceId) {         " + LF +
	        	"    // back button has been pressed             " + LF +
	        	"    history.back();                             " + LF +	
	        	"} else {                                        " + LF + 
	        	"    setTimeout(\"window.location.href='" + href + "'; " +
	        			"setCookie('sequenceID', pageSequenceId + 1);\", " + (retryDelay) + ");" + LF +
	        	"}";

        	appendToHead(javaScript(directionAwareJavascript));
        	//appendToHead(javaScript("setTimeout(\"window.location.href='" + href + "';\", " + (retryDelay) + ");"));
            
            append(h(1, "Transferring data..."));
            append(p(nodeName));
            append(p(trail));
            append(getGoogleAdsenseBlock("pub-7253309958196609", "5421269628", 728, 90));
            append(p("Please wait while a connection is established."));
            append(div("outer_progress", div("progress")));
            append(javaScript("$('#progress').animate({'width': '+=600px'}, " + retryDelay + ");"));
            int waitSeconds = (int) (waitTime / 1000);
            append(p("This will take approximately " + waitSeconds + " seconds. Subsequent requests will be faster."));
            
            /*
            append(getWaitAdvert("Tired of waiting for a connection?", 
                        "You might like to consider hosting your files on a server. Not only will" +
                        " the files be instantly available, but they will transfer much faster" +
                        " and you will no longer need to keep your computer turned on!"));
            */
            // Prepare the node for subsequent tasks
            node.reduceWaitTime();
            
            // Don't cache wait page
            getHandler().getResponse().getHeaders().set("Cache-Control", "no-cache");      
            
            return;
        }
        
        
		// This is a request to download a file
		if (downloadFileName != null) {
			String href = getServiceAddress() + getUrl() + WareHouse.urlEncode(downloadFileName);
			throw new HTTPException(301, "Direct access", href);
		}        

        
		boolean showFilesLink = false;
        boolean showGalleryLink = false;
        boolean playAudioLink = false;		
        
        if (mode.equals("audio")) {
            append(h(1, "Audio Player"));
            append(p(nodeName));
            append(p(trail));
            
            String playerUrl = "/templates/default/xspf/xspf_player.swf";           
            String playlistUrl = getServiceAddress() + getUrl() + "?mode=playlist";
            String params = "&autoplay=true";             
            
            String xspfPlayer = "<object type='application/x-shockwave-flash' width='400' height='200'" +
                                " data='" + playerUrl + "?playlist_url=" + playlistUrl + params + "'>" +
                                "<param name='movie' value='" + playerUrl + "?playlist_url=" + playlistUrl + params + "' />" +
                                "</object>";

            
            append(div("xspfPlayer", xspfPlayer));
            
            append(p(a(playlistUrl, "XSPF Playlist")));
            
            showFilesLink = true;
        }     
        
        else if (mode.equals("preview")) {
            
            append(h(1, "Image Preview"));
            append(p(nodeName));
            append(p(trail));
            
            String imageUrl = "?mode=image&offset=" + offset;
            String prevImages = "?mode=preview&offset=" + (offset-1);
            String nextImages = "?mode=preview&offset=" + (offset+1);

            append(div("gallery", image(imageUrl, "Image preview")));
            
            append(div("nextGallery", a(nextImages, "Next")));  
            if (offset > 0)  append(div("prevGallery", a(prevImages, "Previous")));               
            
            showFilesLink = true;
            showGalleryLink = true;
        }
		
        else if (mode.equals("gallery")) {
		    
            append(h(1, "Image Listing"));
            append(p(nodeName));
            append(p(trail));
            
            String imageUrl = "?mode=image_index&offset=" + offset;
            String prevImages = "?mode=gallery&offset=" + (offset-9);
            String nextImages = "?mode=gallery&offset=" + (offset+9);

		    append(div("gallery", image(imageUrl, "Images in this location", "usemap='#image_map'")));
		    
		    StringBuilder imageMap = new StringBuilder();
		    
		    imageMap.append("<map name='image_map'>");
		    int imageOffset = offset;
	        for (int r=0; r<NUM_THUMB_ROWS; r++) {
	            for (int c=0; c<NUM_THUMB_COLS; c++) {
	                int x1 = THUMB_HORIZ_PADDING + c * (THUMBNAIL_SIZE + THUMB_HORIZ_PADDING*2);
	                int x2 = x1 + THUMBNAIL_SIZE;
	                int y1 = THUMB_VERT_PADDING + r * (THUMBNAIL_SIZE + THUMB_VERT_PADDING*2);	
	                int y2 = y1 + THUMBNAIL_SIZE;
	                String coords = x1 + "," + y1 + "," + x2 + "," + y2;
	                String href = "?mode=preview&offset=" + imageOffset;
	                imageMap.append("<area shape='rect' coords='" + coords + "' href='" + href + "'>");
	                imageOffset++;
	            }
	        }
	                
		    imageMap.append("</map>");
		       
            append(imageMap.toString());
            
            
            append(div("nextGallery", a(nextImages, "Next")));	
            if (offset > 0)  append(div("prevGallery", a(prevImages, "Previous")));               
		    
		    showFilesLink = true;
		}
		
        
		else if (mode.equals("file")) {
    		// This is a request for a directory listing
        	try {
                String decodedPath = decodePath(path);
                
        		Task task = new DirectoryListingTask();
        		task.setTaskInputMeta(decodedPath);
        		task.setTaskPassword(password);
        		
    			List<FileDetails> directories = new LinkedList<FileDetails>();
    			List<FileDetails> files = new LinkedList<FileDetails>();    		
        		
    			TaskStub stub = node.addNewTaskAndWait(task);
    
        		PersistableVector fileDetails = (PersistableVector) stub.getOutput();
    			
    			if (fileDetails == null) {
    				throw new Exception("Node did not return a directory listing");
    			}
    			
    			for (Object obj : fileDetails) {
    				FileDetails item = (FileDetails) obj;
    				if (item.isDirectory()) directories.add(item);
    				else files.add(item);
    			}
    
    	        append(h(1, "File Listing"));
    	        append(p(nodeName));
    	        append(getGoogleAdsenseBlock("pub-7253309958196609", "5421269628", 728, 90));
    	        append(p(trail));
    	        
    			
    			append("<table id='listing' class='tablesorter'>");
    			append("<thead><tr>");
    			append("<th>Filename</th>");
    			if (getPageType() == PAGE_TYPE_DESKTOP) {
    			    append("<th width='120'>Last modified</th><th width='80'>Size</th>");
    			}
    			append("</tr></thead><tbody>");
    
    			for (FileDetails item : directories) {
    				String name = item.getName();
    				if (name.endsWith("/")) name = name.substring(0, name.length()-1);
    				String resourceUrl = WareHouse.urlEncode(name) + "/";
    				
    				append("<tr>");
    				append("<td>" + strong(a(resourceUrl, item.getName())) + "</td>");	
    				if (getPageType() == PAGE_TYPE_DESKTOP) {
        				String lastModString = WareHouse.formatStandardDate(item.getLastModified());
        				append("<td>" + lastModString + "</td><td></td>");	
    				}
    				append("</tr>");		
    			}	
    			
    			for (FileDetails item : files) {
    			    String type = WareHouse.getContentType(item.getName());
    			    String encName = WareHouse.urlEncode(item.getName());
    				String resourceUrl = "?downloadFileName=" + encName;
    
    				append("<tr>");
    				append("<td>");
    				
    				if (item.getName().toLowerCase().endsWith(".mp3")) {
    				    playAudioLink = true;
    				}
    
    				if (type.startsWith("image/")) {
    				    
    				    showGalleryLink = true;
    
                        // Link to resized image for mobile devices				    
    				    if (getPageType() == PAGE_TYPE_MOBILE) {
    				        append(a(encName+"?max_dim=320", item.getName()));    
    				    }
    				    // Preview option for other devices
    				    else {		        
    				        append(a(resourceUrl, item.getName(), "class='preview'"));
    				    }
    				} else {
    	                append(a(resourceUrl, item.getName()));				    
    				}
    				append("</td>");	
    				
    				if (getPageType() == PAGE_TYPE_DESKTOP) {
        				String lastModString = WareHouse.formatStandardDate(item.getLastModified());
        				append("<td>" + lastModString + "</td>");	
        				String prefixForSorting = "<span style='display: none;'>" + pad(item.getLength(), 10) + "</span>" ;
        				append("<td>" + prefixForSorting + WareHouse.formatSize(item.getLength()) + "</td>");	
    				}
    				append("</tr>");
    			}	
    			if (directories.size() + files.size() == 0) {
    				append("<tr><td colspan='3'>No files found.</td></tr>");	
    			}
    			append("</tbody>");
    			append("</table>");
    			
    
    			
    			if (directories.size() + files.size() > 0) {
    			    
    			    if (getPageType() == PAGE_TYPE_DESKTOP) {
    			        
    			        addJavascriptLink("/lib/jquery.tablesorter.js");
    			        addJavascriptLink("/templates/default/tooltip/tooltip.js");         
    			        addCSSLink("/templates/default/table/style.css");  
    			        addCSSLink("/templates/default/tooltip/tooltip.css");   
    			        
    			        append(javaScript("$(document).ready(function() { $(\"#listing\").tablesorter(); });"));		        
    			    }
    			}
    			
    		}
        	catch (SecurityException ex) {
                getHandler().getResponse().getHeaders().set("Cache-Control", "no-cache");     	    
                append(h(1, "Security error"));
                if (password == null) {
                    append(p("This node requires a password."));	
                }
                else {
                    append(p("The password specified to access this node is incorrect, please try again."));    
                }            
                append(form(p(passwordfield("password")) +
                            p(submitbutton("action", "Submit"))));
    			
    		} 
    		catch (Exception ex) {
                
                String msg = "Failed to browse directory";
                throw new HTTPException(500, msg, ex);
            }
		}
		
        if (getPageType() == PAGE_TYPE_DESKTOP) {
            
            // Add helper links
            if (showFilesLink || showGalleryLink || playAudioLink) {
                
                append(cleardiv());                
                
                List<String> helperLinks = new LinkedList<String>();
                
                if (showFilesLink) {
                    helperLinks.add(a("?mode=file", "List files"));
                }                
                if (showGalleryLink) {
                    helperLinks.add(a("?mode=gallery&offset=" + offset, "View images"));
                }
                if (playAudioLink) {
                    helperLinks.add(a("?mode=audio", "Stream audio"));
                }       
                
                append("<p>Options: ");
                Iterator<String> i = helperLinks.iterator();
                while (i.hasNext()) {
                    append(i.next());
                    if (i.hasNext()) append(" | ");
                }
                append("</p>");
            }                   
        }		
		
		if (node != WorkerNodeManager.EXAMPLE_NODE && nodeData != null) {
		    
            String host = getRequest().getHeaders().getFirst("Host");
            if (host == null) host = getHandler().getServiceHost();

            String source = (String) nodeData.get("lastRequestAddress");
            String forwardedFor = (String) nodeData.get("lastRequestAddressForwardedFor");
            StringBuilder info = new StringBuilder();
            info.append("These files are stored on a node connected to the Internet via" + 
                        " the IP address " + strong(source) + ".");
            
            if (forwardedFor != null) {
                info.append(" This IP address is known to be a proxy server which forwarded the " +
                            " nodes connection from " + strong(forwardedFor) + ".");
            }
            
            info.append(" Please be aware that " + strong(host) + " does not store these files" +
                        " and only facilitates access to them.");
            
            info.append(" List generated: " + WareHouse.formatStandardDate(System.currentTimeMillis()) + ".");              
            
            append(div("fileLocation", p(info.toString())));
        }		
    }
    
    private String getWaitAdvert(String heading, String text) {
        
        String boxNetScriptUrl = "http://www.jdoqocy.com/placeholder-3234307?target=_top&mouseover=N";
        return div("waitBoxA", incorporateJavaScript(boxNetScriptUrl, false)) +
               div("waitBoxB", h(2, heading) + p(text) +
                    p(a("http://www.anrdoezrs.net/click-3060789-10489880", 
                        "Store, share, and access your files anywhere with Box.net.  Free Trial",
                        "target='_top'") + 
                      image("http://www.awltovhc.com/image-3060789-10489880", "CJ Stats",
                            "width='1' height='1' border='0'")
                    )
               ) + cleardiv();
    }

    public static String decodePath(String url) {
        
        if (url == null) return null;
        
        StringBuilder fileBuilder = new StringBuilder();
        
        StringTokenizer t = new StringTokenizer(url, "/");
        while (t.hasMoreTokens()) {
            fileBuilder.append(WareHouse.urlDecode(t.nextToken()));
            if (t.hasMoreTokens()) {
                fileBuilder.append("/");
            }
        }        
        
        return fileBuilder.toString();
    }    
    
    public static String generatePathLinks(String root, String path) {
        
        StringBuilder linkBuilder = new StringBuilder();
        
        StringTokenizer t = new StringTokenizer(path, "/");
        linkBuilder.append(a(root, "Shared files") + " / ");
        String parent = "";
        while (t.hasMoreTokens()) {
        	String name = t.nextToken();
        	parent += name + "/";
        	linkBuilder.append(a(root + parent, WareHouse.urlDecode(name)) + " / ");
        }        
        
        return linkBuilder.toString();
    }     
  
    public String getCacheKey() {
        
    	// Don't cache failed node lookups
    	if (node == null) return null;
    	if (nodeData == null) return null;
    	// Don't cache password protected content
    	if (nodeData.requiresPassword()) return null;
    	
    	StringBuilder key = new StringBuilder();
    	
    	key.append(node.getNodeId());
    	key.append(path);
    	key.append(node.getConfigurationId());
    	
    	if (downloadFileName != null) {
    	    key.append(downloadFileName);
    	}
        if (mode != null) {
            key.append(mode);
        }
        if (offset > 0) {
            key.append(String.valueOf(offset));
        }        
        
    	return key.toString();
    }
    
    public long getExpireTime() {
        return System.currentTimeMillis() + DEFAULT_EXPIRE;
    }
    
    private String pad(long number, int amount) {
        String s = String.valueOf(number);
        while (s.length() < amount) {
            s = "0" + s;
        }
        return s;
    }
}
