package org.wikiwebserver.core;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.HashMap;

import org.wikiwebserver.core.interfaces.CSVStyler;


class LogStore {    
       
    private LogStore() {
        headings = new HashMap<String, List<String>>(); 
        records = new HashMap<String, List<List<String>>>(); 
        locks = new HashMap<String, Object>();
    }
    
    void flush() {
        
        Set<String> pending;
        synchronized (records) {
            pending = new HashSet<String>(records.keySet());
        }
        
        Iterator<String> i = pending.iterator();
        while (i.hasNext()) {
            try {
                String logName = i.next();
                //WareHouse.logInfo("Saving Log: " + logName);
                flushLogData(logName);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    public List<String> getLogHeaders(String logName) throws IOException {

        flushLogData(logName);
        
        StructuredDataReader reader = null;
        try {
            File logFile = getActiveLogFile(logName);
            if (!logFile.exists()) {
                throw new IOException("Log file not found");
            }
            reader = new StructuredDataReader(new BufferedReader(new FileReader(logFile)));
            List<String> headers = reader.readColumns();
            if (headers == null) {
                throw new IOException("Log file empty");
            }
            return headers;
        }
        catch (IOException ex) {
            IOException ioe =  new IOException("Failed to process log file");
            ioe.initCause(ex);
            throw ioe;
        }
        finally {
            if (reader != null) {
                try { reader.close(); } catch (IOException ex) {}
            }
        }
    }
    
    void tailLog(String logName, CSVStyler styler, Writer writer) throws IOException {    
    	
    	Object lock = null;
    	synchronized (locks) {
	    	lock = locks.get(logName);
	    	if (lock == null) {
	    		lock = new Object();
	    		locks.put(logName, lock);
	    	}
    	}
    	
    	StructuredDataWriter csvWriter = null;
    	if (styler == null) {
    		csvWriter = new StructuredDataWriter(writer);
    	}    	
    
        
        try {
        	
            List<String> storedHeaders = null;
            synchronized (headings) {
                storedHeaders = headings.get(logName);
            }
            
            // Wait for initial log entry
            if (storedHeaders == null) {
		    	synchronized (lock) {
		    		lock.wait();
		    	}	
	            storedHeaders = null;
	            synchronized (headings) {
	                storedHeaders = headings.get(logName);
	            }		    	
            }
            
			int next = 0;
			while (true) {

			
				int size = Integer.MAX_VALUE;
				List<String> record = null;
				while (size > next) {
			        synchronized (records) {
			            List<List<String>> liveRecords = records.get(logName);
			            size = liveRecords.size();
			            // Finished for now
			            if (next == size) break;
			            // Live records flushed, reset position
			            else if (next > size) {
			            	next = 0;
			            	break;
			            }
			            record = liveRecords.get(next);
			        }
	
			        if (record != null) {
			        	if (styler != null) {
			        		writer.write(styler.styleRecord(storedHeaders, record));
			        	}
			        	else csvWriter.writeColumns(record);
			               
				        writer.flush();
			        }
			        
			        next++;
				}

		        // Wait for more data
			    synchronized (lock) {				        
		    		lock.wait();
		    	}			        
			}
		} 
		catch (InterruptedException ex) {
			ex.printStackTrace();
		}        
        catch (IOException ex) {
			// ex.printStackTrace();
		}	  
        finally {
        	try { writer.close(); } catch (Exception ex) {}
        }
    }
    
    void searchLogAndOutputTable(String logName,
                                        File logFile, 
                                        String[] searchCriteria, 
                                        int[] searchType, 
                                        boolean[] displayColumns, 
                                        int limit, 
                                        OutputStream out) throws IOException {
        
        if (logFile.equals(getActiveLogFile(logName))) {
            flushLogData(logName);
        }
        
        int rows = 0;  
        StructuredDataReader reader = null;
        OutputStreamWriter writer = null;
        try {          
            writer = new OutputStreamWriter(out, "utf8");    
            writer.write("<p><b>Searching log file:</b> " + logFile + "...</p>");              
            if (!logFile.exists()) {
                writer.write("<p>No log file found.</p>");
                return;
            }
            reader = new StructuredDataReader(new BufferedReader(new FileReader(logFile)));
            List<String> headers = reader.readColumns();
            if (headers == null) {
                throw new IOException("Log file empty");
            }
            
            StringBuilder tableHeader = new StringBuilder();
            tableHeader.append("<table class='logOutputTable' cellpadding='0' cellspacing='0'>");
            tableHeader.append("<tr>");
            for (int c=0; c<headers.size(); c++) {
                if (displayColumns == null || displayColumns[c]) {
                    tableHeader.append("<th>" + headers.get(c) + "</th>");
                }
            }
            tableHeader.append("</tr>");
            
            String alt;
            List<String> columns = reader.readColumns();
            while (columns != null) {
            	
                StringBuilder row = new StringBuilder();
                for (int c=0; c<columns.size(); c++) {
                	String header = headers.get(c);
                	String value = columns.get(c);
                	
                    if (searchType == null) {
                        // process as normal
                    } else if (searchType[c] == WareHouse.SEARCH_CONTAINS) {
                        // column does not contain string
                        if (!value.toLowerCase().contains(searchCriteria[c].toLowerCase())) {
                            row = null;
                            break;
                        }
                    } else if (searchType[c] == WareHouse.SEARCH_EQUALS) {
                        // column does not equal string
                        if (!value.equals(searchCriteria[c])) {
                            row = null;
                            break;                            
                        }
                    }
                    
                    if (displayColumns == null || displayColumns[c]) {
                    	if (value == null) {
                    		 row.append("<td>-</td>");
                    	}
                    	else if (header.equals("URI") || headers.get(c).equals("Referer")) {
                            row.append("<td>" + linkifyURI(value) + "</td>");
                        }
                        else if (header.equals("Browser ID")) {
                            row.append("<td>" + linkifyBrowserID(value) + "</td>");
                        }          
                        else if (header.equals("User ID")) {
                            row.append("<td>" + linkifyUserID(value) + "</td>");
                        }                      
                        else row.append("<td>" + value + "</td>");
                    }
                }
                
                if (row != null) {                  
                    if (rows == 0) writer.write(tableHeader.toString());
                    alt = (rows % 2 == 0) ? "rowA" : "rowB";
                    writer.write("<tr class='" + alt + "'>");    
                    writer.write(row.toString());
                    writer.write("</tr>");
                    rows++;                    
                }
                columns = reader.readColumns();
                if (rows == limit) break;
            }
            if (rows == 0) {
                writer.write("<p>No log entries found.</p>");
            }
            else writer.write("</table>");
        } 
        catch (IOException ex) {
            ex.printStackTrace();
            IOException ioe = new IOException("Failed to process log file");
            ioe.initCause(ex);
            throw ioe;
        }
        finally {
            if (writer != null) {
                try { writer.flush(); } catch (IOException ex) {}
            }            
            if (reader != null) {
                try { reader.close(); } catch (IOException ex) {}
            }
        }
        
        writer.write("<p><b>Finished searching log file:</b> " + logFile + " (" + rows + " entries found)</p>");
    } 
    
    private String linkifyUserID(String userID) {
    	String url = WareHouse.getUrlPathForClass(page.tools.stats.BrowserInfo.class);    	
        return "<a href='" + url + "?userID=" + userID + "'>" + userID + "</a>";
    }     
    
    private String linkifyBrowserID(String browserID) {
    	String url = WareHouse.getUrlPathForClass(page.tools.stats.BrowserInfo.class);
        return "<a href='" + url + "?browserID=" + browserID + "'>" + browserID + "</a>";
    }    
    
    private String linkifyURI(String uri) {
        return "<a href='" + uri + "'>" + uri + "</a>";
    }
    
    private File getActiveLogFile(String logName) {
        return getLogFile(logName, System.currentTimeMillis());
    }
    
    public static File getLogFile(String logName, long logTime) {
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
        formatter.setTimeZone(TimeZone.getTimeZone("GMT"));
        String day = formatter.format(logTime);
        File logFile = new File(WareHouse.LOG_ROOT, day + "." + logName + ".log.csv");
        logFile.getParentFile().mkdirs();
        return logFile;
    }    
    
    private boolean equals(List<String> s1, List<String> s2) {
        if (s1 == null && s2 != null) return false;
        if (s2 == null && s1 != null) return false;
        if (s2.size() != s1.size()) return false;
        for (int i=0; i<s1.size(); i++) {
            if (s1.get(i) == null && s2.get(i) != null) return false;
            if (s2.get(i) == null && s1.get(i) != null) return false;
            if (!s1.get(i).equals(s2.get(i))) return false;
        }
        return true;
    }
    
    void logData(String logName, List<String> newHeadings, List<String> record) {
        
    	// Store headings (if required)
    	List<String> storedHeadings = null;
        synchronized (headings) {
            storedHeadings = headings.get(logName);
        }
        if (storedHeadings == null) {
            storedHeadings = newHeadings;
            synchronized (headings) {
                headings.put(logName, storedHeadings);
            }
        } else {
            if (!equals(storedHeadings, newHeadings)) {
                throw new SecurityException("Incorrect data sent to log " + logName);
            }
        }
        
        // Add log entry
        List<List<String>> cachedRecords = null;
        synchronized (records) {
            cachedRecords = records.get(logName);
        }
        if (cachedRecords == null) {
            cachedRecords = new LinkedList<List<String>>();
            synchronized (records) {
                records.put(logName, cachedRecords);
            }
        }
        synchronized (cachedRecords) {
            cachedRecords.add(record);   
        }
        
        // Notify tail locks
    	Object lock = null;
    	synchronized (locks) {
	    	lock = locks.get(logName);
	    	if (lock == null) {
	    		lock = new Object();
	    		locks.put(logName, lock);
	    	}
    	}
    	
    	synchronized (lock) {
			lock.notifyAll();
		}
    }
    
    private void flushLogData(String logName) throws IOException {
        
        List<List<String>> cachedRecords = null;
        synchronized (records) {
            List<List<String>> liveRecords = records.get(logName);
            if (liveRecords != null) {
                cachedRecords = new LinkedList<List<String>>(liveRecords);
            }
        }
        
        // No new data
        if (cachedRecords == null || cachedRecords.size() == 0) {
            return;
        }
        
        List<String> storedHeaders = null;
        synchronized (headings) {
            storedHeaders = headings.get(logName);
        }      
        
        // No matching headings
        if (storedHeaders == null || storedHeaders.size() == 0) {
            throw new SecurityException("No headers found for log " + logName);
        }
        
        StructuredDataWriter writer = null;
        try {
        	/* LOGGING DISABLED
            File logFile = getActiveLogFile(logName);
            writer = new StructuredDataWriter(new BufferedWriter(new FileWriter(logFile, true)));
            if (!logFile.exists() || logFile.length() == 0) {
                writer.writeColumns(storedHeaders);
            }

            // flush
            for (List<String> record : cachedRecords) {
                writer.writeColumns(record);
            }
            */
        	
            // remove from cache
            synchronized (records) {
                records.remove(logName);
            }            

        } catch (Exception ex) {
            //throw ex;
        } finally {
            //try { writer.close(); } catch (Exception ex) { /* ignore */ }
        }
    }

    
    static LogStore getInstance() {
        return singleton;
    }
    
    public Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException(); 
    }    
    
    private static LogStore singleton = new LogStore();
    
    // Mapping between log names and CSV headings
    private Map<String, List<String>> headings; 
    
    // Mapping between log names and cached records
    private Map<String, List<List<String>>> records;     
    
    // Mapping between log names and tail locks
    private Map<String, Object> locks;      
}

