package org.apache.slide.projector.processor;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.contract.Context;
import org.apache.commons.contract.Result;
import org.apache.commons.contract.constraints.StringConstraints;
import org.apache.commons.contract.descriptor.ParameterDescriptor;
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.slide.projector.ContentType;
import org.apache.slide.projector.constraints.AnyConstraints;
import org.apache.slide.projector.constraints.TextConstraints;
import org.apache.slide.projector.repository.RepositoryException;
import org.apache.slide.projector.util.StreamHelper;
import org.apache.slide.projector.value.Streamable;
import org.apache.slide.projector.value.Text;

public class TemplateRenderer implements ConfigurableProcessor {
    private static Logger logger = Logger.getLogger(TemplateRenderer.class.getName());

    public final static String OK = "ok";
    public final static String OUTPUT = "output";
    public final static String FRAGMENT = "fragment";

    protected final static String FRAGMENT_START = "<!--*** Start of '";
    protected final static String FRAGMENT_END = "<!--*** End of '";
    protected final static String FRAGMENT_CLOSE = "' ***-->";

    protected final static String IGNORE_START = "<!--*** Start ignore ***-->";
    protected final static String IGNORE_END = "<!-- *** End ignore *** -->";

    protected final static String TAG_OPEN = "<%";
    protected final static String TAG_CLOSE = "%>";
    protected final static String CONDITION_OPEN = "<?";
    protected final static String CONDITION_CLOSE = "?>";
    protected final static char SEPARATOR = ';';
    protected final static String OPTIONAL = "optional";
    protected final static String REQUIRED = "required";

    protected final static String DEFAULT_FRAGMENT = "-default-fragment-";

    protected final static ResultDescriptor[] resultDescriptors = new ResultDescriptor[] { new ResultDescriptor(
            StateDescriptor.OK_DESCRIPTOR,
            new ResultEntryDescriptor[]{
                    new ResultEntryDescriptor(OUTPUT, new LocalizedMessage("templateRenderer/result/output"), 
                            TextConstraints.UNCONSTRAINED)
            })};
    
    private ParameterDescriptor[] parameterDescriptors;

    protected List parameterDescriptions;
    protected boolean fragments;

    private Map templates;
    private String[] requiredFragments, optionalFragments;
    private boolean ignoreUndefinedFragments = true;
    protected List optionalParameters = new ArrayList(); // all variables used in conditions reflect optional parameters

    // FIXME: Nested fragments
    public void configure(Streamable config) throws ConfigurationException {
        templates = new HashMap();
        parameterDescriptions = new ArrayList();
        try {
            String template = StreamHelper.streamToString(config);
            // ignore sections marked as ignored
            int currentPosition = 0;
            int lastPosition = 0;
            StringBuffer strippedTemplate = new StringBuffer(template.length());
            while ((currentPosition = template.indexOf(IGNORE_START, currentPosition)) >= 0) {
                strippedTemplate.append(template.substring(lastPosition, currentPosition));
                currentPosition = template.indexOf(IGNORE_END, currentPosition)+IGNORE_END.length();
                lastPosition = currentPosition;
            }
            strippedTemplate.append(template.substring(lastPosition));
            template = strippedTemplate.toString();
            
            // split template into different fragments if fragment identifiers are found
            currentPosition = 0;
            boolean fragments = false;
            while ((currentPosition = template.indexOf(FRAGMENT_START, currentPosition)) >= 0) {
                fragments = true;
                currentPosition += FRAGMENT_START.length();
                int startTagClose = template.indexOf(FRAGMENT_CLOSE, currentPosition);
                String fragmentName = template.substring(currentPosition, startTagClose);
                currentPosition = startTagClose+FRAGMENT_CLOSE.length();
                int fragmentEnd = template.indexOf(FRAGMENT_END, currentPosition);
                if ( !ignoreFragment(fragmentName) ) {
                    Template fragment = new Template(template.substring(currentPosition, fragmentEnd));
                    templates.put(fragmentName, fragment);
                }
                currentPosition = fragmentEnd+FRAGMENT_CLOSE.length();
            }
            // check if all required templates are defined
            if (requiredFragments != null ) {
                for ( int i = 0; i < requiredFragments.length; i++ ) {
                    boolean requiredFragmentDefined = false;
                    for ( Iterator j = templates.keySet().iterator(); j.hasNext(); ) {
                        String definedFragment = (String)j.next();
                        if ( definedFragment.startsWith(requiredFragments[i]) ) {
                            requiredFragmentDefined = true;
                            break;
                        }
                    }
                    if ( !requiredFragmentDefined ) {
                        throw new ConfigurationException(new LocalizedError("templateRenderer/requiredFragmentMissing", new String[] { requiredFragments[i] }));
                    }
                }
            }
            parameterDescriptions.add(new ParameterDescriptor(FRAGMENT, new ParameterMessage("templateRenderer/fragment"), new StringConstraints((String [])templates.keySet().toArray(new String[0])), new String(DEFAULT_FRAGMENT)));
            if ( fragments ) {
                if ( requiredFragments != null ) {
                    templates.put(DEFAULT_FRAGMENT, templates.get(requiredFragments[0]));
                } else if ( optionalFragments != null ) {
                    templates.put(DEFAULT_FRAGMENT, templates.get(optionalFragments[0]));
                }
            } else {
                templates.put(DEFAULT_FRAGMENT, new Template(template));
            }
            parameterDescriptors = (ParameterDescriptor [])parameterDescriptions.toArray(new ParameterDescriptor[parameterDescriptions.size()]);
        } catch (RepositoryException exception) {
            logger.log(Level.SEVERE, "Could not load configuration resource!", exception);
        }
    }

    public Result process(Map parameter, Context context) throws Exception {
        String fragment = (String)parameter.get(FRAGMENT);
        Template template = getRequiredFragment(fragment);
        StringBuffer buffer = new StringBuffer(template.getLength());
        template.evaluate(buffer, parameter);
        return new Result(OK, OUTPUT, new Text(buffer.toString(), template.getContentType(), template.isDocument() ));
    }

    public void setRequiredFragments(String[] requiredFragments) {
        this.requiredFragments = requiredFragments;
    }

    public void setOptionalFragments(String[] optionalFragments) {
        this.optionalFragments = optionalFragments;
    }
    
	public void ignoreUndefinedFragments(boolean ignoreUndefinedFragments) {
		this.ignoreUndefinedFragments = ignoreUndefinedFragments; 
	}

    public String renderFragment(String fragment, Map parameter)  throws ProcessException {
        Template template = getRequiredFragment(fragment);
        return renderFragment(template, parameter);
    }

    public String renderFragment(Template template, Map parameter)  throws ProcessException {
        StringBuffer buffer = new StringBuffer(template.getLength());
        template.evaluate(buffer, parameter);
        return buffer.toString();
    }

    public void renderFragment(StringBuffer buffer, String fragment, Map parameter) throws ProcessException {
        Template template = getRequiredFragment(fragment);
        template.evaluate(buffer, parameter);
    }

    protected Template getRequiredFragment(String fragment) throws ProcessException {
        Template template = (Template)templates.get(fragment);
        if ( template == null ) throw new ProcessException(new LocalizedError("templateArrayRenderer/fragmentNotFound", new String[] { fragment }));
        return template;
    }

    protected Template getOptionalFragment(String fragment) {
        return (Template)templates.get(fragment);
    }

    protected Template getOptionalFragment(String fragment, Template defaultTemplate) {
        Template optionalFragment = (Template)templates.get(fragment);
        if ( optionalFragment == null ) return defaultTemplate;
        return optionalFragment;
    }

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

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

    protected String[] getDefinedFragments() {
        return (String [])templates.keySet().toArray(new String[templates.size()]);
    }

    protected List getTemplateParameterDescriptor(String[] fragments) {
        List templateParameterDescriptors = new ArrayList();
        for ( int i = 0; i < fragments.length; i++ ) {
            Template template = (Template)templates.get(fragments[i]);
            if ( template != null ) {
                templateParameterDescriptors.addAll(template.getTemplateParameterDescriptors().values());
            }
        }
        return templateParameterDescriptors;
    }

    protected boolean ignoreFragment(String fragmentName) {
        if ( requiredFragments == null && optionalFragments == null ) return false;
        if ( requiredFragments != null ) {
            for ( int i = 0; i < requiredFragments.length; i++ ) {
                if ( fragmentName.startsWith(requiredFragments[i])) return false;
            }
        }
        if ( optionalFragments != null ) {
            for ( int i = 0; i < optionalFragments.length; i++ ) {
                if ( fragmentName.startsWith(optionalFragments[i])) return false;
            }
        }
        return ignoreUndefinedFragments;
    }

    protected class Template {
        private boolean document;
        private int length;
        private String contentType;
        private EnclosingTemplateFragment compiledTemplate = new EnclosingTemplateFragment();
        private Map templateParameterDescriptors = new HashMap();

        public Template(String template) {
            length = template.length();
            compileTemplate(template, 0, template.length(), compiledTemplate, false);
            document = ContentType.determineIsDocument(template);
            contentType = ContentType.determineContentType(template);
        }

        public int getLength() {
            return length;
        }

        private int compileTemplate(String template, int currentPosition, int length, EnclosingTemplateFragment compiledTemplate, boolean condition) {
            boolean tagsLeft = true;
            do {
                int nextOpeningVariable = template.indexOf(TAG_OPEN, currentPosition);
                int nextOpeningCondition = template.indexOf(CONDITION_OPEN, currentPosition);
                int nextClosingCondition = template.indexOf(CONDITION_CLOSE, currentPosition);
                if ( nextClosingCondition != -1 && ( nextClosingCondition < nextOpeningCondition || nextOpeningCondition == -1 ) && ( nextClosingCondition < nextOpeningVariable || nextOpeningVariable == -1 ) ) {
                    compiledTemplate.addNestedTemplateFragment(new StaticTemplateFragment(template.substring(currentPosition, nextClosingCondition)));
                    currentPosition = nextClosingCondition+CONDITION_CLOSE.length();
                    return currentPosition;
                } else if ( nextOpeningCondition != -1 && ( nextOpeningCondition < nextOpeningVariable || nextOpeningVariable == -1 ) ) {
                    compiledTemplate.addNestedTemplateFragment(new StaticTemplateFragment(template.substring(currentPosition, nextOpeningCondition)));
                    currentPosition = nextOpeningCondition+CONDITION_OPEN.length();
                    String variable = template.substring(currentPosition, template.indexOf(' ', currentPosition));
                    currentPosition += variable.length()+1;
                    ConditionalTemplateFragment conditionalTemplateFragment = new ConditionalTemplateFragment(variable);
                    compiledTemplate.addNestedTemplateFragment(conditionalTemplateFragment);
                    currentPosition = compileTemplate(template, currentPosition, length, conditionalTemplateFragment, true);
                } else if ( nextOpeningVariable != -1 ) {
                    compiledTemplate.addNestedTemplateFragment(new StaticTemplateFragment(template.substring(currentPosition, nextOpeningVariable)));
                    currentPosition = nextOpeningVariable+TAG_OPEN.length();
                    int close = template.indexOf(TAG_CLOSE, currentPosition);
                    String variableName = template.substring(currentPosition, close);
                    String allowedContentTypes = null;
                    boolean required = !condition;
                    int modeSeparator = variableName.indexOf(SEPARATOR);
                    if ( modeSeparator > 0 ) {
                        String mode = variableName.substring(modeSeparator+1);
                        variableName = variableName.substring(0, modeSeparator);
                        int contentTypeSeparator = mode.indexOf(SEPARATOR);
                        if ( contentTypeSeparator > 0 ) {
                            allowedContentTypes = mode.substring(contentTypeSeparator+1);
                            mode = mode.substring(0, contentTypeSeparator);
                        }
                        if ( mode.equals(OPTIONAL) ) {
                            required = false;
                        } else if ( mode.equals(REQUIRED) ) {
                            required = true;
                        } else {
                            logger.log(Level.SEVERE, "Mode '"+mode+"' not allowed. Only optional or required are valid modes");
                        }
                    }
                    AnyConstraints resourceValueDescriptor = new AnyConstraints();
                    if ( !required || optionalParameters.contains(variableName)) {
//                        resourceValueDescriptor.addAllowedContentType(NullResource.CONTENT_TYPE);
                    }
                    if ( allowedContentTypes != null ) {
                        StringTokenizer tokenizer = new StringTokenizer(allowedContentTypes, ",");
                        while (tokenizer.hasMoreElements()) {
                            String contentTypeToken = tokenizer.nextToken();
                            resourceValueDescriptor.addAllowedContentType(contentTypeToken);
                        }
                        compiledTemplate.addNestedTemplateFragment(new Variable(variableName, resourceValueDescriptor.getAllowedContentTypes(), required));
                    } else {
//                        resourceValueDescriptor.addAllowedContentType(ContentType.ANY_TEXT);
                        compiledTemplate.addNestedTemplateFragment(new Variable(variableName, required));
                    }
                    ParameterDescriptor parameterDescriptor;
                    if ( required ) {
                        parameterDescriptor = new ParameterDescriptor(variableName, new ParameterMessage("templateVariable", new String[] { variableName }), resourceValueDescriptor);
                    } else {
                        parameterDescriptor = new ParameterDescriptor(variableName, new ParameterMessage("templateVariable", new String[] { variableName }), resourceValueDescriptor, null);
                    }
                    if ( !parameterDescriptions.contains(parameterDescriptor) ) {
                    	parameterDescriptions.add(parameterDescriptor);
                    }
                    templateParameterDescriptors.put(variableName, parameterDescriptor);
                    currentPosition = close+TAG_CLOSE.length();
                } else {
                    tagsLeft = false;
                }
            } while ( tagsLeft );
            compiledTemplate.addNestedTemplateFragment(new StaticTemplateFragment(template.substring(currentPosition)));
            return currentPosition;
        }

        public Map getTemplateParameterDescriptors() {
            return templateParameterDescriptors;
        }

        public boolean isDocument() {
            return document;
        }

        public String getContentType() {
            return contentType;
        }

        public void evaluate(StringBuffer buffer, Map parameter) throws ProcessException {
            evaluate(buffer, parameter,  0);
        }

        public void evaluate(StringBuffer buffer, Map parameter, int index) throws ProcessException {
            compiledTemplate.render(buffer, parameter, index);
        }
    }

    interface TemplateFragment {
        public void render(StringBuffer buffer, Map parameter, int index) throws ProcessException;
    }

    class StaticTemplateFragment implements TemplateFragment {
        private String content;

        public StaticTemplateFragment(String content) {
            this.content = content;
        }

        public void render(StringBuffer buffer, Map parameter, int index) throws ProcessException {
            buffer.append(content);
        }
    }

    class EnclosingTemplateFragment implements TemplateFragment {
        private List nestedTemplateFragments = new ArrayList();

        public void render(StringBuffer buffer, Map parameter, int index) throws ProcessException {
            for ( Iterator i = nestedTemplateFragments.iterator(); i.hasNext(); ) {
                TemplateFragment fragment = (TemplateFragment)i.next();
                fragment.render(buffer, parameter, index);
            }
        }

        public void addNestedTemplateFragment(Object fragment) {
            nestedTemplateFragments.add(fragment);
        }
    }

    class ConditionalTemplateFragment extends EnclosingTemplateFragment {
        private String variable;

        public ConditionalTemplateFragment(String variable) {
            this.variable = variable;
            optionalParameters.add(variable);
            if ( parameterDescriptions.contains(variable) ) {
                ((AnyConstraints)((ParameterDescriptor)parameterDescriptions.get(parameterDescriptions.indexOf(variable))).getConstraints()).addAllowedContentType(ContentType.NULL);
            }
        }

        public String getVariable() {
            return variable;
        }

        public void render(StringBuffer buffer, Map parameter, int index) throws ProcessException {
            Object variableValue = parameter.get(variable);
            if ( variableValue != null ) {
                 super.render(buffer, parameter, index);
            }
        }
    }

    class Variable implements TemplateFragment {
        private String name;
        private String[] allowedContentTypes;
        private boolean required;

        public Variable(String name, boolean required) {
            this.name = name;
            this.required = required;
        }

        public Variable(String name, String []allowedContentType, boolean required) {
            this.name = name;
            this.allowedContentTypes = allowedContentType;
            this.required = required;
        }

        public void render(StringBuffer buffer, Map parameter, int index) throws ProcessException {
            Object variableValue = parameter.get(name);
            if ( variableValue instanceof Object[] ) {
                Object []array = (Object [])variableValue;
                if ( index > array.length-1 ) {
                    variableValue = array[index % array.length];
                } else {
                    variableValue = array[index];
                }
            }
            if ( required && (variableValue == null ) ) {
                throw new ProcessException(new LocalizedError("templateRenderer/unsetValueNotAllowed", new Object[] { name }));
            }    
            if ( !(!required && variableValue == null) && allowedContentTypes != null && !ContentType.matches(allowedContentTypes, ContentType.getContentType(variableValue))) {
                throw new ProcessException(new LocalizedError("templateRenderer/contentTypeMismatch", new Object[] { name, ContentType.getContentTypesAsString(allowedContentTypes), ContentType.getContentType(variableValue) }));
            }
            if ( variableValue != null ) {
                buffer.append(variableValue);
            }
        }
    }
}