package org.apache.slide.projector.processor.table;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.commons.contract.Context;
import org.apache.commons.contract.Result;
import org.apache.commons.contract.Store;
import org.apache.commons.contract.constraints.ArrayConstraints;
import org.apache.commons.contract.constraints.MapConstraints;
import org.apache.commons.contract.constraints.NumberConstraints;
import org.apache.commons.contract.constraints.StringConstraints;
import org.apache.commons.contract.descriptor.ParameterDescriptor;
import org.apache.commons.contract.i18n.ParameterMessage;
import org.apache.commons.i18n.LocalizedError;
import org.apache.slide.projector.Projector;
import org.apache.slide.projector.processor.ConfigurationException;
import org.apache.slide.projector.processor.ProcessException;
import org.apache.slide.projector.processor.SimpleProcessor;
import org.apache.slide.projector.processor.TemplateMapRenderer;
import org.apache.slide.projector.value.Streamable;
import org.apache.slide.projector.value.Text;

public class TableMapRenderer extends TemplateMapRenderer {
    final protected static String HEADER_FRAGMENT = "header";
    final protected static String FOOTER_FRAGMENT = "footer";
    final protected static String FIRST_FRAGMENT = "first";
    final protected static String LAST_FRAGMENT = "last";
    final protected static String EVEN_FRAGMENT = "even";
    final protected static String DEFAULT_FRAGMENT = "default";
    final protected static String EMPTY_FRAGMENT = "empty";

    final static String OFFSET = "offset";
    final static String ITEMS_PER_PAGE = "itemsPerPage";

    final static String LENGTH = "length";

    protected Template headerTemplate;
    protected Template footerTemplate;
    protected Template firstTemplate;
    protected Template lastTemplate;
    protected Template evenTemplate;
    protected Template defaultTemplate;
    protected Template emptyTemplate;

    private ParameterDescriptor []parameterDescriptors;
    private String[] repeatedFragments;

    public TableMapRenderer() {
        setRequiredFragments(new String[] { DEFAULT_FRAGMENT });
        setOptionalFragments(new String[] { EMPTY_FRAGMENT, HEADER_FRAGMENT, FOOTER_FRAGMENT, FIRST_FRAGMENT, LAST_FRAGMENT, EVEN_FRAGMENT });
        setRepeatedFragments(new String[] { FIRST_FRAGMENT, LAST_FRAGMENT, DEFAULT_FRAGMENT, EVEN_FRAGMENT });
    }

    public void configure(Streamable config) throws ConfigurationException {
        super.configure(config);
        ParameterDescriptor []parentParameterDescriptors = super.getParameterDescriptors();
        // remove fragment parameter
        List parameterDescriptorList = new ArrayList();
        for ( int i = 0; i < parentParameterDescriptors.length; i++ ) {
            ParameterDescriptor descriptor = parentParameterDescriptors[i];
            if ( descriptor.getName() != FRAGMENT ) {
            	if ( descriptor.getName().equals(SimpleProcessor.INPUT) ) {
            		// check if entry descriptor is caused by repeated fragments
            		List entryDescriptors = ((MapConstraints)descriptor.getConstraints()).getEntryDescriptors(); 
            		for ( Iterator j = entryDescriptors.iterator(); j.hasNext(); ) {
            			ParameterDescriptor entryDescriptor = (ParameterDescriptor)j.next();
                		if ( !repeatedParameterDescriptor(entryDescriptor) ) {
                			parameterDescriptorList.add(entryDescriptor);
                			j.remove();
                        }
            		}
            		parameterDescriptorList.add(new ParameterDescriptor(SimpleProcessor.INPUT, new ParameterMessage("tableMapRenderer/input"), new ArrayConstraints(parentParameterDescriptors[i].getConstraints())));
            	} else {
            		parameterDescriptorList.add(descriptor);
            	}
            }
        }
        parameterDescriptorList.add(new ParameterDescriptor(OFFSET, new ParameterMessage("tableRenderer/offset"), new NumberConstraints(), new Integer(0)));
        parameterDescriptorList.add(new ParameterDescriptor(ITEMS_PER_PAGE, new ParameterMessage("tableRenderer/length"), new NumberConstraints(), new Integer(-10)));
        parameterDescriptorList.add(new ParameterDescriptor(TableHandler.ID, new ParameterMessage("tableRenderer/id"), new StringConstraints(), new String("table")));
        parameterDescriptorList.add(new ParameterDescriptor(TableHandler.STORE, new ParameterMessage("tableRenderer/store"), new StringConstraints(Projector.STORES), new String(Projector.SESSION_STORE)));
        parameterDescriptorList.add(new ParameterDescriptor(TableHandler.SORTED_BY, new ParameterMessage("tableHandler/sortedBy"), new StringConstraints(), null));
        parameterDescriptorList.add(new ParameterDescriptor(TableHandler.ORDER, new ParameterMessage("tableHandler/order"), new StringConstraints(new String[] {TableHandler.ASCENDING, TableHandler.DESCENDING}), null));
        parameterDescriptors = (ParameterDescriptor[] )parameterDescriptorList.toArray(new ParameterDescriptor[parameterDescriptorList.size()]);
        
        headerTemplate = getOptionalFragment(HEADER_FRAGMENT);
        footerTemplate = getOptionalFragment(FOOTER_FRAGMENT);
        firstTemplate = getOptionalFragment(FIRST_FRAGMENT);
        lastTemplate = getOptionalFragment(LAST_FRAGMENT);
        evenTemplate = getOptionalFragment(EVEN_FRAGMENT);
        emptyTemplate = getOptionalFragment(EMPTY_FRAGMENT);
        try {
            defaultTemplate = getRequiredFragment(DEFAULT_FRAGMENT);
        } catch ( ProcessException exception ) {
            throw new ConfigurationException(new LocalizedError("tableRenderer/fragmentsMissing"), exception);
        }
    }

    public Result process(Map parameter, Context context) throws Exception {
        StringBuffer buffer = new StringBuffer(1024);
        Object []input = (Object [])parameter.get(SimpleProcessor.INPUT);
        Number offsetResource = (Number)parameter.get(OFFSET);
        Number itemsPerPageResource = (Number)parameter.get(ITEMS_PER_PAGE);
        String sortedBy = (String)parameter.get(TableHandler.SORTED_BY);
        String order = (String)parameter.get(TableHandler.ORDER);
        String id = parameter.get(TableHandler.ID).toString();
        Store store = context.getStore(parameter.get(TableHandler.STORE).toString());
        Map tableMap = (Map)store.get(id, context);
        if ( tableMap == null) {
            tableMap = new HashMap();
            store.put(id, tableMap, context);
        } else {
            Number offset = (Number)tableMap.get(TableHandler.CURRENT_POSITION);
            if ( offset != null ) offsetResource = offset;
            Number itemsPerPage = (Number)tableMap.get(ITEMS_PER_PAGE);
            if ( itemsPerPage != null ) itemsPerPageResource = itemsPerPage;
            String sortedByState = (String)tableMap.get(TableHandler.SORTED_BY);
            if ( sortedByState != null ) sortedBy = sortedByState;
            String orderState = (String)tableMap.get(TableHandler.ORDER);
            if ( orderState != null ) order = orderState;
        }
    	// sort table
        if ( sortedBy != null && order != null ) {
        	Comparator comparator = new RowComparator(sortedBy, order); 
        	Arrays.sort(input, comparator);
        }
        tableMap.put(TableHandler.CURRENT_POSITION, offsetResource);
        tableMap.put(ITEMS_PER_PAGE, itemsPerPageResource);
        int offset = offsetResource.intValue();
        int length = itemsPerPageResource.intValue();
        parameter.remove(OFFSET);
        parameter.remove(ITEMS_PER_PAGE);
        int maxIndex = input.length;
        tableMap.put(LENGTH, new Integer(maxIndex));
        offset = Math.min(offset, maxIndex);
        if ( length > 0 ) {
            length = Math.min(length, maxIndex-offset);
        } else {
            length = maxIndex-offset;
        }
        if ( maxIndex == 0 ) {
        	if ( emptyTemplate != null ) emptyTemplate.evaluate(buffer, parameter);
        } else {
            if ( headerTemplate != null ) headerTemplate.evaluate(buffer, parameter);
            for ( int i = 0; i < length; i++ ) {
            	// Enable the use of input parameters to provide default values for optional row parameters
            	Map rowParameter = new HashMap();
                rowParameter.putAll(parameter);
            	Map repeatedParameter = ((Map)input[i+offset]);
                for ( Iterator j = repeatedParameter.entrySet().iterator(); j.hasNext(); ) {
                	Map.Entry entry = (Map.Entry)j.next();
                	if ( entry.getValue() != null ) {
                		rowParameter.put(entry.getKey(), entry.getValue());
                	}
                }
                if ( i == 0 && firstTemplate != null ) {
                    firstTemplate.evaluate(buffer, rowParameter);
                } else if ( i == length -1 && lastTemplate != null ) {
                    lastTemplate.evaluate(buffer, rowParameter);
                } else if ( evenTemplate != null && i%2 == 0 ) {
                    evenTemplate.evaluate(buffer, rowParameter);
                } else {
                    defaultTemplate.evaluate(buffer, rowParameter);
                }
            }
            if ( footerTemplate != null ) footerTemplate.evaluate(buffer, parameter);
        }
        return new Result(OK, OUTPUT, new Text(buffer.toString(), defaultTemplate.getContentType(), false ));
    }

    public ParameterDescriptor[] getParameterDescriptors() {
        return parameterDescriptors;
    }

    public void setRepeatedFragments(String[] repeatedFragments) {
        this.repeatedFragments = repeatedFragments;
    }

    protected boolean repeatedParameterDescriptor(ParameterDescriptor entryDescriptor) {
        List parameterDescriptors = getTemplateParameterDescriptor(repeatedFragments);
        if ( parameterDescriptors.contains(entryDescriptor) ) {
            return true;
        }
        return false;
    }

    protected class RowComparator implements Comparator {
    	private String sortBy, order;
    	
		public RowComparator(String sortBy, String order) {
			this.sortBy = sortBy;
			this.order = order;
		}

		public int compare(Object o1, Object o2) {
			Map map1 = (Map)o1;
			Map map2 = (Map)o2;
			Object v1 = map1.get(sortBy);
			Object v2 = map2.get(sortBy);
			int comparison = 0;
			if ( v1 instanceof Comparable ) {
				comparison = ((Comparable)v1).compareTo(v2);
				if ( order.equals(TableHandler.ASCENDING) ) {
					comparison = -comparison;
				}
			}
			return comparison;
		}
    }
}