package org.wikiwebserver.core;
import java.io.IOException;
import java.io.Writer;
import java.util.Iterator;
import java.util.List;
import java.util.Map;


/**
 * a StructuredDataWriter is used to write structured data to a character stream.
 * 
 * Supports writing a collection of key-value pairs from a Map and writing 
 * a CSV from a 2 dimensional list of strings.
 * 
 * @author Michael Gardiner
 * @version 2008-10-20 
 */
public class StructuredDataWriter {   
	
    /**
     * This character is used to surround a field that requires encapsulation.
     * Encapsulation ensures that the field data is held together and not
     * misinterpreted as separate data. When a field needs to include this 
     * character as part of its data, it must be paired with itself.
     */
    public static final char FIELD_ENCAPSULATE = '\"';   	

    private Writer writer;
    
    
    /**
     * Constructs a new StructuredDataWriter for writing structured data to a writer.
     * 
     * @param writer The writer where the structured data should be written.
     */
    public StructuredDataWriter(Writer writer) {
        this.writer = writer;
    }   
    
    public void writeRows(List<List<String>> rows) throws IOException {
    	writeRows(rows, ',', '\n');
    }    
    
    /**
     * Writes a 2 dimensional list of Strings (a table) to the the writer.
     * 
     * @param table A table to convert to CSV and write.
     * @param colSeparator The character that should mark the end of a column
     * @param rowSeparator The character that should mark the end of a row    
     * 
     * @throws IOException a problem writing the data to the writer 
     */   
    public void writeRows(List<List<String>> table, char colSeperator, char rowSeparator) 
    	throws IOException {
        
    	Iterator<List<String>> i = table.iterator();
    	while (i.hasNext()) {
    		List<String> columns = i.next();
            writeColumns(columns, colSeperator, rowSeparator);
        }
    	
    	// Final blank row indicates end of rows for multiStructure
    	write(rowSeparator);
    }    
    
    public void writeColumns(List<String> columns) throws IOException {
    	writeColumns(columns, ',', '\n');
    }
    
    
    /**
     * Writes a 1 dimensional list of Strings (a row) to the writer.
     * 
     * @param columns A row, made of one or more fields, to convert and write.
     * @param colSeparator A character to mark the end of a column
     * @param rowSeparator A character to mark the end of a row  
     *  
     * @throws IOException a problem writing the fields to the writer 
     */
    public void writeColumns(List<String> columns, char colSeperator, char rowSeparator) 
    	throws IOException {

    	Iterator<String> i = columns.iterator();
        while (i.hasNext()) {
        	Object column = i.next();
            writeField(column, colSeperator, rowSeparator, FIELD_ENCAPSULATE, '\r', '\n');
            if (i.hasNext()) {
            	write(colSeperator);
            }
        }
        write(rowSeparator);
    }
    
    /**
     * Writes a Map of name-value pairs to the writer
     * 
     * @param pairs A Map of name-value pairs to write.
     * @param keyValueSeparator A character to join keys to values
     * @param entrySeparator A character to signify the end of a key-value pair
     * 
     * @throws IOException a problem writing the data to the writer 
     */   
    public void writePairs(Map<String, String> pairs, char keyValueSeparator, char entrySeparator) 
    	throws IOException {
        
    	Iterator<Map.Entry<String, String>> i = pairs.entrySet().iterator();
    	while (i.hasNext()) {
    		Map.Entry<String, String> entry = i.next();
    		writeField(entry.getKey(), keyValueSeparator, entrySeparator, FIELD_ENCAPSULATE);
    		write(keyValueSeparator);
    		writeField(entry.getValue(), keyValueSeparator, entrySeparator, FIELD_ENCAPSULATE);
            if (i.hasNext()) {
            	write(entrySeparator);
            }
        }
    	
    	// Final blank entry indicates end of pairs
    	writer.write(entrySeparator);    	
    }      
    
    public void flush() throws IOException {
        writer.flush();
    }    
    
    public void close() throws IOException {
        writer.close();
    }
    
    protected void write(String str) throws IOException {
    	writer.write(str);
    }
    
    protected void write(int c) throws IOException {
    	writer.write(c);
    }     
    
    protected void writeField(Object fieldObj, char... requireEncapsulation) throws IOException {
    	
    	if (fieldObj == null) return;
    	
    	String field = fieldObj.toString();

        // Does the field require encapsulating?
        if (field.length() == 0 || contains(field, requireEncapsulation)) {
            
            // Escape the encapsulate character if it occurs within the field
        	String unsafe = String.valueOf(FIELD_ENCAPSULATE);
        	String safe = FIELD_ENCAPSULATE + unsafe;
            field = field.replace(unsafe, safe);
            
            // Encapsulate the field
            field = FIELD_ENCAPSULATE + field + FIELD_ENCAPSULATE;
        }
        write(field);

    }
    
    private boolean contains(String text, char... tests) {
    	for (char test : tests) {
    		if (text.contains(String.valueOf(test))) {
    			return true;
    		}
    	}
    	return false;
    }
}
