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

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.apache.commons.contract.Context;
import org.apache.commons.contract.EnvironmentConsumer;
import org.apache.commons.contract.Information;
import org.apache.commons.contract.Processor;
import org.apache.commons.contract.Result;
import org.apache.commons.contract.constraints.ArrayConstraints;
import org.apache.commons.contract.constraints.BooleanConstraints;
import org.apache.commons.contract.constraints.LocaleConstraints;
import org.apache.commons.contract.constraints.MapConstraints;
import org.apache.commons.contract.constraints.StringConstraints;
import org.apache.commons.contract.constraints.ValidationException;
import org.apache.commons.contract.descriptor.ParameterDescriptor;
import org.apache.commons.contract.descriptor.RequiredEnvironmentDescriptor;
import org.apache.commons.contract.descriptor.ResultDescriptor;
import org.apache.commons.contract.descriptor.ResultEntryDescriptor;
import org.apache.commons.contract.descriptor.StateDescriptor;
import org.apache.commons.contract.i18n.ParameterMessage;
import org.apache.commons.i18n.LocalizedError;
import org.apache.commons.i18n.LocalizedMessage;
import org.apache.commons.i18n.MessageNotFoundException;
import org.apache.slide.projector.Projector;
import org.apache.slide.projector.constraints.URIConstraints;
import org.apache.slide.projector.engine.ProcessorManager;
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.TemplateRenderer;
import org.apache.slide.projector.processor.process.Process;
import org.apache.slide.projector.store.FormStore;
import org.apache.slide.projector.value.Streamable;
import org.apache.slide.projector.value.URI;
import org.apache.slide.projector.value.URIValue;

/**
 * The ControlComposer class
 * 
 */
public class ControlComposer extends TemplateRenderer implements EnvironmentConsumer {
	//	 Parameters for the control composer
    public final static String VALIDATE = "validate";
    public final static String LOCALE = "locale";
    public final static String ACTION = "action";
    public final static String ERRORS_PROCESSOR = "errorsProcessor";
    public final static String CONTROL_DESCRIPTIONS = "controlDescriptions";
    public final static String TRIGGER_DESCRIPTIONS = "triggerDescriptions";

    // Parameters for each control 
    public final static String CONTROL = "control";
    public final static String CONTROL_NAME = "controlName";
    public final static String CONTROL_CONTAINER = "controlContainer";

    public final static String TRIGGER = "trigger";
    public final static String TRIGGER_NAME = "triggerName";
    public final static String TRIGGER_CONTAINER = "triggerContainer";
    
    // Parameters for the control container
    protected final static String TITLE = "title";
    protected final static String TEXT = "text";
    protected final static String PROMPT = "prompt";
    protected final static String ERRORS = "errors";
    protected final static String ERRORS_TITLE = "errors-title";
    
    public final static String ERROR_NUMBER = "error-number";
    public final static String ERROR_TITLE = "error-title";
    public final static String ERROR_TEXT = "error-text";
    public final static String ERROR_DETAILS = "error-details";
    public final static String ERROR_SUMMARY = "error-summary";

    // Result
    public final static String GENERATED_CONTROLS = "renderedControls";
    public final static String GENERATED_TRIGGERS = "renderedTriggers";
    public final static String RENDERED_ERRORS = "renderedErrors";
    public final static String DEFAULT_STATE ="default";
    public final static String VALID_STATE ="valid";
    public final static String INVALID_STATE ="invalid";

    // Parameters for inheriting classes
    public final static String HANDLER = "handler";
	public final static String METHOD = "method";
	
    protected final static String DEFAULT_FORM = "default form";
    protected final static String INVALID_FORM = "invalid form";
    protected final static String VALID_FORM = "valid form";

    protected final static String GET = "GET";
    protected final static String POST = "POST";
    protected final static String[] methods = new String[] { GET, POST }; 

    protected Template defaultTemplate;
    protected Template validTemplate;
    protected Template invalidTemplate;

    protected LocalizedError NO_ERROR_MESSAGE_AVAILABLE = new LocalizedError("errorMessageNotFound");
    protected ParameterMessage NO_PARAMETER_MESSAGE_AVAILABLE = new ParameterMessage("parameterMessageNotFound");

    private ParameterDescriptor[] parameterDescriptors;

    public ControlComposer() {
        setRequiredFragments(new String[] { DEFAULT_FORM });
        setOptionalFragments(new String[] { VALID_FORM, INVALID_FORM });
    }

    private final static ResultDescriptor[] resultDescriptors = new ResultDescriptor[] {
            new ResultDescriptor(new StateDescriptor(DEFAULT_STATE, new LocalizedMessage("controlComposer/state/default")),
                    new ResultEntryDescriptor[]{
                    new ResultEntryDescriptor(GENERATED_CONTROLS, new LocalizedMessage("controlComposer/generatedControls"), ArrayConstraints.UNCONSTRAINED),
                    new ResultEntryDescriptor(GENERATED_TRIGGERS, new LocalizedMessage("controlComposer/generatedTriggers"), ArrayConstraints.UNCONSTRAINED),
            }),
            new ResultDescriptor(new StateDescriptor(VALID_STATE, new LocalizedMessage("controlComposer/state/valid")),
                    new ResultEntryDescriptor[]{
                    new ResultEntryDescriptor(GENERATED_CONTROLS, new LocalizedMessage("controlComposer/generatedControls"), ArrayConstraints.UNCONSTRAINED),
                    new ResultEntryDescriptor(GENERATED_TRIGGERS, new LocalizedMessage("controlComposer/generatedTriggers"), ArrayConstraints.UNCONSTRAINED),
            }),
            new ResultDescriptor(new StateDescriptor(INVALID_STATE, new LocalizedMessage("controlComposer/state/invalid")),
                    new ResultEntryDescriptor[]{
                    new ResultEntryDescriptor(GENERATED_CONTROLS, new LocalizedMessage("controlComposer/generatedControls"), ArrayConstraints.UNCONSTRAINED),
                    new ResultEntryDescriptor(GENERATED_TRIGGERS, new LocalizedMessage("controlComposer/generatedTriggers"), ArrayConstraints.UNCONSTRAINED),
                    new ResultEntryDescriptor(RENDERED_ERRORS, new LocalizedMessage("controlComposer/renderedErrors"), ArrayConstraints.UNCONSTRAINED)
            })
            };

    private final static RequiredEnvironmentDescriptor[] requiredEnvironmentDescriptors = new RequiredEnvironmentDescriptor[] {
    		new RequiredEnvironmentDescriptor(LOCALE, Projector.SESSION_STORE, new ParameterMessage("treeLocalized/requiredEnvironment/locale"), new LocaleConstraints(), Locale.getDefault())
    };
    
    public void configure(Streamable config) throws ConfigurationException {
        super.configure(config);
        ParameterDescriptor[] parentParameterDescriptors = super.getParameterDescriptors();
        parameterDescriptors = new ParameterDescriptor[parentParameterDescriptors.length + 4];
        System.arraycopy(parentParameterDescriptors, 0, parameterDescriptors, 0, parentParameterDescriptors.length);
        parameterDescriptors[parentParameterDescriptors.length] =
        	new ParameterDescriptor(CONTROL_DESCRIPTIONS, new ParameterMessage("controlComposer/controlDescriptions"), new ArrayConstraints(
        			new MapConstraints(new ParameterDescriptor[] {
        					new ParameterDescriptor(Control.ACTION, new ParameterMessage("controlComposer/controlDescriptions/action"), new URIConstraints(), null),
        					new ParameterDescriptor(CONTROL, new ParameterMessage("controlComposer/controlDescriptions/control"), new URIConstraints()),
        					new ParameterDescriptor(CONTROL_CONTAINER, new ParameterMessage("controlComposer/controlDescriptions/controlContainer"), new URIConstraints(), null),
        			})));
        parameterDescriptors[parentParameterDescriptors.length+1] =
        	new ParameterDescriptor(TRIGGER_DESCRIPTIONS, new ParameterMessage("controlComposer/triggerDescriptions"), new ArrayConstraints(
        			new MapConstraints(new ParameterDescriptor[] {
        					new ParameterDescriptor(Trigger.ACTION, new ParameterMessage("controlComposer/triggerDescriptions/action"), new URIConstraints(), null),
        					new ParameterDescriptor(Trigger.VALIDATE, new ParameterMessage("controlComposer/triggerDescriptions/validate"), new BooleanConstraints(), Boolean.TRUE),
							new ParameterDescriptor(Trigger.INVOLVED_PARAMETERS, new ParameterMessage("trigger/involvedParameters"), new ArrayConstraints(new StringConstraints()), null),
        					new ParameterDescriptor(Process.STEP, new ParameterMessage("controlComposer/triggerDescriptions/step"), new StringConstraints()),
        					new ParameterDescriptor(TRIGGER, new ParameterMessage("controlComposer/triggerDescriptions/trigger"), new URIConstraints()),
        					new ParameterDescriptor(TRIGGER_CONTAINER, new ParameterMessage("controlComposer/triggerDescriptions/triggerContainer"), new URIConstraints(), null)
        			})));
        parameterDescriptors[parentParameterDescriptors.length+2] =
        	new ParameterDescriptor(ACTION, new ParameterMessage("controlComposer/action"), new URIConstraints());
        parameterDescriptors[parentParameterDescriptors.length+3] =
        	new ParameterDescriptor(ERRORS_PROCESSOR, new ParameterMessage("controlComposer/errorsProcessor"), new URIConstraints(), null);

        try {
            defaultTemplate = getRequiredFragment(DEFAULT_FORM);
        } catch ( ProcessException exception ) {
            throw new ConfigurationException(new LocalizedError("form/defaultFragmentMissing"));
        }
        validTemplate = getOptionalFragment(VALID_FORM);
        invalidTemplate = getOptionalFragment(INVALID_FORM);
    }

    public Result process(Map parameter, Context context) throws Exception {
    	Object[] controlDescriptions = (Object [])parameter.get(CONTROL_DESCRIPTIONS);
    	Object[] triggerDescriptions = (Object [])parameter.get(TRIGGER_DESCRIPTIONS);
    	URI actionUri = (URIValue)parameter.get(ACTION);
    	Object errorsProcessorUri = parameter.get(ERRORS_PROCESSOR);
        Locale locale = (Locale)context.getStore(Projector.SESSION_STORE).get(LOCALE, context);
        String state = DEFAULT_STATE;
        List informations = context.getInformations();
		Map mapResource = (Map)((FormStore)context.getStore(Projector.FORM_STORE)).getDomain();
		List generatedControls = new ArrayList();
		List involvedParameters = new ArrayList();
		for (int i = 0; i < controlDescriptions.length; i++ ) {
        	Map controlParameters = (Map)controlDescriptions[i];
        	String controlName = controlParameters.get(CONTROL_NAME).toString();
        	URI controlUri = (URI)controlParameters.get(CONTROL); 
        	Control control = (Control)ProcessorManager.getInstance().getProcessor(controlUri);
        	Object controlActionUri = controlParameters.get(Control.ACTION);
        	if ( controlActionUri == null ) controlActionUri = actionUri; 
       		controlParameters.put(Control.ACTION, controlActionUri);
        	try {
        		ProcessorManager.prepareValues(control.getParameterDescriptors(), controlParameters, context);
        	} catch ( ValidationException exception ) {
        		throw new ValidationException(new LocalizedError("controlComposer/controlParameterInvalid", new Object[] { controlUri }), exception);
        	}
            Object controlContainerUri = controlParameters.get(CONTROL_CONTAINER); 
            ParameterDescriptor parameterDescriptor = Control.getParameterDescriptor(controlParameters, context); 
            String parameterName = parameterDescriptor.getName();
            involvedParameters.add(new String(parameterName));
            ParameterMessage description = (ParameterMessage)parameterDescriptor.getDescription();
            boolean required = parameterDescriptor.isRequired();
            String controlState = Control.OPTIONAL_CONTROL;
            if ( required ) {
            	controlState = Control.REQUIRED_CONTROL;
            }
            Object controlValue = null;
            boolean validate = false;
            if ( mapResource != null ) {
            	controlValue = mapResource.get(parameterName);
            	validate = false;
            	Boolean validateResource = (Boolean)mapResource.get(VALIDATE);
            	if ( validateResource != null ) validate = validateResource.booleanValue();
            }
            if ( validate ) {
            	try {
            		controlValue = ProcessorManager.prepareValue(parameterDescriptor, controlValue, context);
            	} catch ( ValidationException exception ) {
            		controlValue = StringConstraints.UNCONSTRAINED.cast(controlValue, context).toString();
            		context.addInformation(new Information(Information.ERROR, exception.getErrorMessage(), new String[] { parameterName }));
            	}
            	controlParameters.put(Control.VALUE, controlValue);
            	if ( hasErrors(informations, parameterName) ) {
            		if ( required ) {
            			controlState = Control.REQUIRED_INVALID_CONTROL;
            		} else {
            			controlState = Control.OPTIONAL_INVALID_CONTROL;
            		}
            		explodeInformations(controlParameters, informations, parameterName, locale);
            	} else {
            		if ( required ) {
            			controlState = Control.REQUIRED_VALID_CONTROL;
            		} else {
            			controlState = Control.OPTIONAL_VALID_CONTROL;
            		}
            	}
            }
            controlParameters.put(Control.STATE, controlState);
            controlParameters.put(Control.VALUE, controlValue);
            Result controlResult = control.process(controlParameters, context);
            if ( controlContainerUri != null ) {
            	Processor controlContainer = ProcessorManager.getInstance().getProcessor((URI)controlContainerUri);
            	Map controlContainerParameters = new HashMap();
            	controlContainerParameters.putAll(parameter);
            	if ( hasErrors(informations, parameterName) ) {
            		explodeInformations(controlContainerParameters, informations, parameterName, locale);
            	} 
            	controlContainerParameters.put(Control.STATE, controlState);
            	controlContainerParameters.put(CONTROL, controlResult.getResultEntries().get(OUTPUT));
            	controlContainerParameters.put(TITLE, description.getTitle(locale, parameterName ));
            	try {
            		controlContainerParameters.put(TEXT, description.getText(locale));
            		controlContainerParameters.put(PROMPT, description.getPrompt(locale));
            	} catch ( MessageNotFoundException exception ) {
            		controlContainerParameters.put(TEXT, NO_PARAMETER_MESSAGE_AVAILABLE.getText(locale, "&nbsp;"));
            		controlContainerParameters.put(PROMPT, NO_PARAMETER_MESSAGE_AVAILABLE.getPrompt(locale, "&nbsp;"));
            	}
            	try {
            		ProcessorManager.prepareValues(controlContainer.getParameterDescriptors(), controlContainerParameters, context);
            	} catch ( ValidationException exception ) {
            		throw new ValidationException(new LocalizedError("controlComposer/controlContainerParameterInvalid", new Object[] { controlContainerUri }), exception);
            	}
            	Result controlContainerResult = controlContainer.process(controlContainerParameters, context);
            	Map controlMap = new HashMap();
                controlMap.put(controlName, controlContainerResult.getResultEntries().get(OUTPUT));
                generatedControls.add(controlMap);
            } else {
                Map controlMap = new HashMap();
                controlMap.put(controlName, controlResult.getResultEntries().get(OUTPUT));
            	generatedControls.add(controlMap);
            }
            if ( controlState == Control.OPTIONAL_INVALID_CONTROL || controlState == Control.REQUIRED_INVALID_CONTROL  ) {
            	state = INVALID_STATE;
            } else if ( state == DEFAULT_STATE && ( controlState == Control.OPTIONAL_VALID_CONTROL || controlState == Control.REQUIRED_VALID_CONTROL ) ) {
            	state = VALID_STATE;
            }
        }
        Result composerResult = new Result(state, GENERATED_CONTROLS, generatedControls.toArray()); 
		List generatedTriggers = new ArrayList();
		for (int i = 0; i < triggerDescriptions.length; i++ ) {
        	Map triggerParameters = (Map)triggerDescriptions[i];
        	String triggerName = triggerParameters.get(TRIGGER_NAME).toString();
        	Object involvedTriggerParameters = triggerParameters.get(Trigger.INVOLVED_PARAMETERS);
        	if ( involvedTriggerParameters == null ) {
        		involvedTriggerParameters = ((String[])involvedParameters.toArray(new String[involvedParameters.size()]));
        	}
        	triggerParameters.put(Trigger.INVOLVED_PARAMETERS, involvedTriggerParameters);
        	URI triggerUri = (URI)triggerParameters.get(TRIGGER);
        	Trigger trigger = (Trigger)ProcessorManager.getInstance().getProcessor(triggerUri);
        	Object triggerActionUri = triggerParameters.get(Trigger.ACTION);
        	if ( triggerActionUri == null ) triggerActionUri = actionUri; 
       		triggerParameters.put(Trigger.ACTION, triggerActionUri);
        	try {
        		ProcessorManager.prepareValues(trigger.getParameterDescriptors(), triggerParameters, context);
        	} catch ( ValidationException exception ) {
        		throw new ValidationException(new LocalizedError("controlComposer/triggerParameterInvalid", new Object[] { triggerUri }), exception);
        	}
            Object triggerContainerUri = triggerParameters.get(TRIGGER_CONTAINER); 
            Result triggerResult = trigger.process(triggerParameters, context);
            if ( triggerContainerUri != null ) {
            	Processor triggerContainer = ProcessorManager.getInstance().getProcessor((URI)triggerContainerUri);
            	Map triggerContainerParameters = new HashMap();
            	triggerContainerParameters.putAll(parameter);
            	triggerContainerParameters.put(TRIGGER, triggerResult.getResultEntries().get(OUTPUT));
            	try {
            		ProcessorManager.prepareValues(triggerContainer.getParameterDescriptors(), triggerContainerParameters, context);
            	} catch ( ValidationException exception ) {
            		throw new ValidationException(new LocalizedError("controlComposer/triggerContainerParameterInvalid", new Object[] { triggerContainerUri }), exception);
            	}
            	Result triggerContainerResult = triggerContainer.process(triggerContainerParameters, context);
                Map triggerMap = new HashMap();
                triggerMap.put(triggerName, triggerContainerResult.getResultEntries().get(OUTPUT));
                generatedTriggers.add(triggerMap);
            } else {
                Map triggerMap = new HashMap();
                triggerMap.put(triggerName, triggerResult.getResultEntries().get(OUTPUT));
            	generatedTriggers.add(triggerMap);
            }
        }
        composerResult.addResultEntry(GENERATED_TRIGGERS, generatedTriggers.toArray());
        if ( errorsProcessorUri instanceof URI ) {
        	Processor errorsTable = ProcessorManager.getInstance().getProcessor((URI)errorsProcessorUri);
        	List errorList = new ArrayList();
        	for ( Iterator i = context.getInformations().iterator(); i.hasNext(); ) {
        		Information info = (Information)i.next();
        		Map errors = new HashMap();
        		explodeInformation(errors, info, locale );
        		errorList.add(errors);
        	}
        	Map errorsParameter = new HashMap();
        	Map[] errorsArray = new Map[errorList.size()];
        	errorsParameter.put(SimpleProcessor.INPUT, ((Map[])errorList.toArray(errorsArray)));
        	Result renderedErrors = ProcessorManager.process(errorsTable, errorsParameter, context);
        	composerResult.addResultEntry(RENDERED_ERRORS, renderedErrors.getResultEntries().get(OUTPUT));
        }
        return composerResult;
    }

    protected void explodeInformations(Map parameters, List informations, String parameterName, Locale locale) {
        // FIXME: Use template for error numbers
        List matchingInfos = getErrors(informations, parameterName);
        StringBuffer buffer = new StringBuffer();
        for ( Iterator i = matchingInfos.iterator(); i.hasNext(); ) {
            Information info = (Information)i.next();
        	buffer.append(info.getNumber()).append(") ");
            explodeInformation(parameters, info, locale);
        }
        parameters.put(ERROR_NUMBER, buffer.toString());
    }

    protected void explodeInformation(Map parameters, Information info, Locale locale) {
        parameters.put(ERROR_NUMBER, String.valueOf(info.getNumber()));
        try {
            parameters.put(ERROR_TITLE, info.getLocalizedError().getTitle(locale));
            parameters.put(ERROR_TEXT, info.getLocalizedError().getText(locale));
            parameters.put(ERROR_SUMMARY, info.getLocalizedError().getSummary(locale));
            parameters.put(ERROR_DETAILS, info.getLocalizedError().getDetails(locale));
        } catch ( MessageNotFoundException exception ) {
            parameters.put(ERROR_TITLE, NO_ERROR_MESSAGE_AVAILABLE.getTitle(locale, "no error title available"));
            parameters.put(ERROR_TEXT, NO_ERROR_MESSAGE_AVAILABLE.getText(locale, "no error text available"));
            parameters.put(ERROR_SUMMARY, NO_ERROR_MESSAGE_AVAILABLE.getSummary(locale, "no error summary available"));
            parameters.put(ERROR_DETAILS, NO_ERROR_MESSAGE_AVAILABLE.getDetails(locale, "no error details available"));
        }
    }

   protected boolean hasErrors(List informations, String parameterName) {
       for ( Iterator i = informations.iterator(); i.hasNext(); ) {
       	Information info = (Information)i.next();
           if ( info.isParameterInvolved(parameterName) && info.getSeverity() == Information.ERROR ) {
               return true;
           }
       }
       return false;
   }

    protected List getErrors(List informations, String parameterName) {
        List matchingInfos = new ArrayList();
        for ( Iterator i = informations.iterator(); i.hasNext(); ) {
        	Information info = (Information)i.next();
        	if ( info.isParameterInvolved(parameterName) && info.getSeverity() == Information.ERROR ) {
                matchingInfos.add(info);
            }
        }
        return matchingInfos;
    }

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

    public RequiredEnvironmentDescriptor[] getRequiredEnvironmentDescriptors() {
		return requiredEnvironmentDescriptors;
	}

    public ResultDescriptor[] getResultDescriptors() {
        return resultDescriptors;
    }
}