/**
 * Copyright (C) 2009 eXo Platform SAS.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.exoplatform.groovyscript;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.CompilerConfiguration;

import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyCodeSource;

/**
 * @author <a href="mailto:julien.viet@exoplatform.com">Julien Viet</a>
 * @version $Revision$
 */
public class GroovyScriptBuilder {

    /** . */
    private final String templateId;

    /** . */
    private final String templateName;

    /** . */
    private final String templateText;

    /** . */
    private SectionType currentType = null;

    /** . */
    private StringBuilder accumulatedText = new StringBuilder();

    /** . */
    private Script script = new Script();

    public GroovyScriptBuilder(String templateId, String templateName, String templateText) {
        this.templateId = templateId;
        this.templateName = templateName;
        this.templateText = templateText;
    }

    private void begin(SectionType sectionType, Position pos) {
        if (sectionType == null) {
            throw new NullPointerException();
        }
        if (pos == null) {
            throw new NullPointerException();
        }
        if (currentType != null) {
            throw new IllegalStateException();
        }
        this.currentType = sectionType;

        //
        switch (currentType) {
            case STRING:
                break;
            case SCRIPTLET:
                break;
            case EXPR:
                script.appendGroovy(";out.print(\"${");
                break;
        }
    }

    private void append(SectionItem item) {
        if (item instanceof TextItem) {
            TextItem textItem = (TextItem) item;
            String text = textItem.getData();
            switch (currentType) {
                case STRING:
                    accumulatedText.append(text);
                    break;
                case SCRIPTLET:
                    script.appendGroovy(text);
                    script.positionTable.put(script.lineNumber, textItem);
                    break;
                case EXPR:
                    script.appendGroovy(text);
                    script.positionTable.put(script.lineNumber, textItem);
                    break;
            }
        } else if (item instanceof LineBreakItem) {
            switch (currentType) {
                case STRING:
                    accumulatedText.append("\n");
                    break;
                case SCRIPTLET:
                case EXPR:
                    script.appendGroovy("\n");
                    script.lineNumber++;
                    break;
            }
        } else {
            throw new AssertionError();
        }
    }

    private void end() {
        if (currentType == null) {
            throw new IllegalStateException();
        }

        //
        switch (currentType) {
            case STRING:
                if (accumulatedText.length() > 0) {
                    script.appendText(accumulatedText.toString());
                    accumulatedText.setLength(0);
                }
                break;
            case SCRIPTLET:
                // We append a line break because we want that any line comment does not affect the template
                script.appendGroovy("\n");
                script.lineNumber++;
                break;
            case EXPR:
                script.appendGroovy("}\");\n");
                script.lineNumber++;
                break;
        }

        //
        this.currentType = null;
    }

    public GroovyScript build() throws TemplateCompilationException {
        List<TemplateSection> sections = new TemplateParser().parse(templateText);

        //
        for (TemplateSection section : sections) {
            begin(section.getType(), section.getItems().get(0).getPosition());
            for (SectionItem item : section.getItems()) {
                append(item);
            }
            end();
        }

        //
        String groovyText = script.toString();

        //
        CompilerConfiguration config = new CompilerConfiguration();

        //
        byte[] bytes;
        try {
            config.setScriptBaseClass(BaseScript.class.getName());
            bytes = groovyText.getBytes(config.getSourceEncoding());
        } catch (UnsupportedEncodingException e) {
            throw new TemplateCompilationException(e, groovyText);
        }

        //
        InputStream in = new ByteArrayInputStream(bytes);
        GroovyCodeSource gcs = new GroovyCodeSource(in, templateName, "/groovy/shell");
        GroovyClassLoader loader = new GroovyClassLoader(Thread.currentThread().getContextClassLoader(), config);
        Class<?> scriptClass;
        try {
            scriptClass = loader.parseClass(gcs, false);
        } catch (CompilationFailedException e) {
            throw new GroovyCompilationException(e, templateText, groovyText);
        } catch (ClassFormatError e) {
            throw new GroovyCompilationException(e, templateText, groovyText);
        }

        return new GroovyScript(templateId, script.toString(), scriptClass,
                Collections.unmodifiableMap(new HashMap<Integer, TextItem>(script.positionTable)));
    }

    /**
     * Internal representation of a script
     */
    private static class Script {

        /** . */
        private StringBuilder out = new StringBuilder();

        /** . */
        private List<TextContant> textMethods = new ArrayList<TextContant>();

        /** . */
        private int methodCount = 0;

        /** The line number table. */
        private Map<Integer, TextItem> positionTable = new HashMap<Integer, TextItem>();

        /** The current line number. */
        private int lineNumber = 1;

        @Override
        public String toString() {
            StringBuilder builder = new StringBuilder();
            builder.append(out.toString());
            builder.append("\n");
            builder.append("public class Constants\n");
            builder.append("{\n");
            for (TextContant method : textMethods) {
                builder.append(method.getDeclaration()).append("\n");
            }
            builder.append("}\n");
            return builder.toString();
        }

        public void appendText(String text) {
            TextContant m = new TextContant("s" + methodCount++, text);
            out.append("out.print(Constants.").append(m.name).append(");\n");
            textMethods.add(m);
            lineNumber++;
        }

        public void appendGroovy(String s) {
            out.append(s);
        }
    }

    /**
     * This object encapsulate the generation of a method that outputs the specified text.
     */
    private static class TextContant {

        /** . */
        private final String name;

        /** . */
        private final String text;

        private TextContant(String name, String text) {
            this.name = name;
            this.text = text;
        }

        public String getDeclaration() {
            StringBuilder builder = new StringBuilder("");
            for (int i = 0; i < text.length(); i++) {
                char c = text.charAt(i);
                if (c == '\n') {
                    builder.append("\\n");
                } else if (c == '\"') {
                    builder.append("\\\"");
                } else {
                    builder.append(c);
                }
            }
            return "public static final " + GroovyText.class.getName() + " " + name + " = new " + GroovyText.class.getName()
                    + "(\"" + builder + "\");";
        }
    }
}
