/*
 * Copyright 2004-2005 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.codehaus.groovy.grails.web.pages;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.groovy.grails.commons.*;
import org.codehaus.groovy.grails.web.taglib.GrailsTagRegistry;
import org.codehaus.groovy.grails.web.taglib.GroovySyntaxTag;
import org.codehaus.groovy.grails.web.taglib.exceptions.GrailsTagException;

import java.io.*;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * NOTE: Based on work done by the GSP standalone project (https://gsp.dev.java.net/)
 *
 * Parsing implementation for GSP files
 *
 * @author Troy Heninger
 * @author Graeme Rocher
 *
 * Date: Jan 10, 2004
 *
 */
public class Parse implements Tokens {
    public static final Log LOG = LogFactory.getLog(Parse.class);

    private static final Pattern PARA_BREAK = Pattern.compile("/p>\\s*<p[^>]*>", Pattern.CASE_INSENSITIVE);
    private static final Pattern ROW_BREAK = Pattern.compile("((/td>\\s*</tr>\\s*<)?tr[^>]*>\\s*<)?td[^>]*>", Pattern.CASE_INSENSITIVE);
    private static final Pattern PARSE_TAG_FIRST_PASS = Pattern.compile("(\\s*(\\S+)\\s*=\\s*[\"]([^\"]*)[\"][\\s|>]{1}){1}");
    private static final Pattern PARSE_TAG_SECOND_PASS = Pattern.compile("(\\s*(\\S+)\\s*=\\s*[']([^']*)['][\\s|>]{1}){1}");

    private Scan scan;
    private GSPWriter out;
    private String className;
    private boolean finalPass = false;
    private int tagIndex;
    private Map tagContext;
    private List tagMetaStack = new ArrayList();
    private GrailsTagRegistry tagRegistry = GrailsTagRegistry.getInstance();
    private boolean bufferWhiteSpace ;

    private StringBuffer whiteSpaceBuffer = new StringBuffer();
    private int currentOutputLine = 1;
    private String contentType = DEFAULT_CONTENT_TYPE;
    private boolean doNextScan = true;
    private int state;
    private static final String START_MULTILINE_STRING = "'''";
    private static final String END_MULTILINE_STRING = "'''";
    private static final String DEFAULT_CONTENT_TYPE = "text/html;charset=UTF-8";
    private Map constants = new TreeMap();
    private int constantCount = 0;

    private final String pageName;
    private static final String EMPTY_MULTILINE_STRING = "''''''";
    public static final String[] DEFAULT_IMPORTS = new String[] {
        "org.codehaus.groovy.grails.web.pages.GroovyPage",
        "org.codehaus.groovy.grails.web.taglib.*",
        "org.springframework.web.util.*",
        "grails.util.GrailsUtil"
    };
    private static final String CONFIG_PROPERTY_DEFAULT_CODEC = "grails.views.default.codec";
    private static final String CONFIG_PROPERTY_GSP_ENCODING = "grails.views.gsp.encoding";

    private String codecName;
    private static final String IMPORT_DIRECTIVE = "import";
    private static final String CONTENT_TYPE_DIRECTIVE = "contentType";
    private static final String DEFAULT_CODEC_DIRECTIVE = "defaultCodec";
    private String gspEncoding;
    public static final String GROOVY_SOURCE_CHAR_ENCODING = "UTF-8";

    public String getContentType() {
        return this.contentType;
    }

    public int getCurrentOutputLineNumber() {
        return currentOutputLine;
    }

    class TagMeta  {
        String name;
        String namespace;
        Object instance;
        boolean isDynamic;
        boolean hasAttributes;
        int lineNumber;

        public String toString() {
            return "<"+namespace+":"+name+">";
        }
    }

    public Parse(String name, String filename, InputStream in) throws IOException {
        Map config = ConfigurationHolder.getFlatConfig();

        // Get the GSP file encoding from Config, or fall back to system file.encoding if none set
        Object gspEnc = config.get(CONFIG_PROPERTY_GSP_ENCODING);
        if ((gspEnc != null) && (gspEnc.toString().trim().length() > 0)) {
            gspEncoding = gspEnc.toString();
        } else {
            gspEncoding = System.getProperty("file.encoding", "us-ascii");
        }

        if(LOG.isDebugEnabled()) {
            LOG.debug("GSP file encoding set to: " + gspEncoding);
        }
        
        scan = new Scan(readStream(in));
        this.pageName = filename;
        makeName(name);
        Object o = config.get(CONFIG_PROPERTY_DEFAULT_CODEC);
        lookupCodec(o);

    } // Parse()

    private void lookupCodec(Object o) {
        if(o!=null) {
            String codecName = o.toString();
            GrailsApplication app = ApplicationHolder.getApplication();
            if(app != null) {
                GrailsClass codecClass = app.getArtefactByLogicalPropertyName(CodecArtefactHandler.TYPE, codecName);
                if(codecClass == null) codecClass = app.getArtefactByLogicalPropertyName(CodecArtefactHandler.TYPE, codecName.toUpperCase());
                if(codecClass != null) {
                    this.codecName = codecClass.getFullName();
                }
            }
        }
    }

    public int[] getLineNumberMatrix() {
        return out.getLineNumbers();
    }
    public InputStream parse() {

        StringWriter sw = new StringWriter();
        out = new GSPWriter(sw,this);
        page();
        finalPass = true;
        scan.reset();
        page();

        // This gets bytes in system's default encoding
        InputStream in = null;
        try {
            in = new ByteArrayInputStream(sw.toString().getBytes(GROOVY_SOURCE_CHAR_ENCODING));
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Grails cannot run unless your environment supports UTF-8!");
        }
        //System.out.println("Compiled GSP into Groovy code: " + sw.toString());
        if(LOG.isDebugEnabled()) {
            LOG.debug("Compiled GSP into Groovy code: " + sw.toString());
        }
        scan = null;
        return in;
    } 


    private void declare(boolean gsp) {
        if (finalPass) return;
        if (LOG.isDebugEnabled()) LOG.debug("parse: declare");
        out.println();
        write(scan.getToken().trim(), gsp);
        out.println();
        out.println();
    } // declare()

    private void direct() {
        if (finalPass) return;
        if (LOG.isDebugEnabled()) LOG.debug("parse: direct");
        String text = scan.getToken();
        text = text.trim();
        directPage(text);
    } // direct()

    private void directPage(String text) {
        text = text.trim();
//		LOG.debug("directPage(" + text + ')');
        Pattern pat = Pattern.compile("(\\w+)\\s*=\\s*\"([^\"]*)\"");
        Matcher mat = pat.matcher(text);
        for (int ix = 0;;) {
            if (!mat.find(ix)) return;
            String name = mat.group(1);
            String value = mat.group(2);
            if (name.equals(IMPORT_DIRECTIVE)) pageImport(value);
            if (name.equals(CONTENT_TYPE_DIRECTIVE)) contentType(value);
            if (name.equals(DEFAULT_CODEC_DIRECTIVE)) lookupCodec(value);
            ix = mat.end();
        }
    } // directPage()

    private void contentType(String value) {
        this.contentType = value;
    }

    private void scriptletExpr() {
        if (!finalPass) return;
        if (LOG.isDebugEnabled()) LOG.debug("parse: expr");

        String text = scan.getToken().trim();
        out.printlnToResponse(text);
    }

    private void expr() {
        if (!finalPass) return;
        if (LOG.isDebugEnabled()) LOG.debug("parse: expr");

        String text = scan.getToken().trim();
        if(codecName != null) {
            out.printlnToResponse("Codec.encode("+text+")");            
        }
        else {
            out.printlnToResponse(text);
        }
    } // expr()


    private void html() {
        if (!finalPass) return;
        if (LOG.isDebugEnabled()) LOG.debug("parse: html");
        String text = scan.getToken();
        if(Pattern.compile("\\S").matcher(text).find())
            bufferWhiteSpace = false;
        
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        String[] lines = text.split("\\n");

        if(lines.length == 1 && !StringUtils.isBlank(lines[0])) {
            out.printlnToResponse('\'' + escapeGroovy(lines[0]) + '\'');
        }
        else {

            pw.print(START_MULTILINE_STRING);
            boolean hasContent = false;
            boolean firstLine = true;
            for (int i = 0; i < lines.length; i++) {
                String line = lines[i];
                final String content = escapeGroovy(line);
                if(!StringUtils.isEmpty(content)) {
                    if(!hasContent) {
                        hasContent = true;
                        break;
                    }
                }
            }
            if(hasContent) {
                for (int i = 0; i < lines.length; i++) {
                    String line = lines[i];
                    final String content = escapeGroovy(line);
                    if(firstLine) {
                        pw.print(content);
                        firstLine = false;
                    }
                    else {
                        pw.println();
                        pw.print(content);
                    }

                }
            }
            pw.print(END_MULTILINE_STRING);
            pw.println();
            
            if(hasContent && !bufferWhiteSpace) {
                final String constantValue = sw.toString();
                final String constantName = "STATIC_HTML_CONTENT_" + constantCount++;
                constants.put(constantName, constantValue);
                out.printlnToResponse(constantName);
            }
        }

    } // html()

    private void makeName(String uri) {
        String name;
        int slash = uri.lastIndexOf('/');
        if (slash > -1) {
            name = uri.substring(slash + 1);
            uri = uri.substring(0,(uri.length() - 1) - name.length());
            while(uri.endsWith("/")) {
                uri = uri.substring(0,uri.length() -1);
            }
            slash = uri.lastIndexOf('/');
            if(slash > -1) {
                    name = uri.substring(slash + 1) + '_' + name;
            }
        }
        else {
            name = uri;
        }
        StringBuffer buf = new StringBuffer(name.length());
        for (int ix = 0, ixz = name.length(); ix < ixz; ix++) {
            char c = name.charAt(ix);
            if (c < '0' || (c > '9' && c < '@') || (c > 'Z' && c < '_') || (c > '_' && c < 'a') || c > 'z') c = '_';
            else if (ix == 0 && c >= '0' && c <= '9') c = '_';
            buf.append(c);
        }
        className = buf.toString();
    } // makeName()

    private static boolean match(CharSequence pat, CharSequence text, int start) {
        int ix = start, ixz = text.length(), ixy = start + pat.length();
        if (ixz > ixy) ixz = ixy;
        if (pat.length() > ixz - start) return false;
        for (; ix < ixz; ix++) {
            if (Character.toLowerCase(text.charAt(ix)) != Character.toLowerCase(pat.charAt(ix - start))) {
                return false;
            }
        }
        return true;
    } // match()

    private static int match(Pattern pat, CharSequence text, int start) {
        Matcher mat = pat.matcher(text);
        if (mat.find(start) && mat.start() == start) {
            return mat.end();
        }
        return 0;
    } // match()

    private void page() {
        if (LOG.isDebugEnabled()) LOG.debug("parse: page");
        if (finalPass) {
            out.println();
            out.print("class ");
            out.print(className);
            out.println(" extends GroovyPage {");
            out.println("public Object run() {");            

        }

        loop: for (;;) {
            if(doNextScan)
                state = scan.nextToken();
            else
                doNextScan = true;

            switch (state) {
                case EOF: break loop;
                case HTML: html(); break;
                case JEXPR: scriptletExpr(); break;
                case JSCRIPT: script(false); break;
                case JDIRECT: direct(); break;
                case JDECLAR: declare(false); break;
                case GEXPR: expr(); break;
                case GSCRIPT: script(true); break;
                case GDIRECT: direct(); break;
                case GDECLAR: declare(true); break;
                case GSTART_TAG: startTag(); break;
                case GEND_TAG: endTag(); break;
            }
        }

        if (finalPass) {
            if(!tagMetaStack.isEmpty()) {
                TagMeta tag = (TagMeta)tagMetaStack.iterator().next();
                throw new GrailsTagException("Grails tags were not closed! ["+tagMetaStack+"] in GSP "+pageName+"", pageName, tag.lineNumber);
            }

            out.println("}");
            for (Iterator i = constants.keySet().iterator(); i.hasNext();) {
                String name = (String) i.next();
                out.println("static final " + name + " = " + constants.get(name));
            }
            out.println("}");
        }
        else {
            for (int i = 0; i < DEFAULT_IMPORTS.length; i++) {
                out.print("import ");
                out.println(DEFAULT_IMPORTS[i]);

            }
            if(codecName != null) {
                out.print("import ");
                out.print(codecName);
                out.println(" as Codec");
            }            
        }
    } // page()


    private void endTag() {
        if (!finalPass) return;

       String tagName = scan.getToken().trim();
       String ns = scan.getNamespace();
       
       if(tagMetaStack.isEmpty())
             throw new GrailsTagException("Found closing Grails tag with no opening ["+tagName+"]");

       TagMeta tm = (TagMeta)tagMetaStack.remove(this.tagMetaStack.size() - 1);
       String lastInStack = tm.name;
       String lastNamespaceInStack = tm.namespace;

       // if the tag name is blank then it has been closed by the start tag ie <tag />
       if(StringUtils.isBlank(tagName))
               tagName = lastInStack;

       if(!lastInStack.equals(tagName) ||
    	  !lastNamespaceInStack.equals(ns)) {
           throw new GrailsTagException("Grails tag ["+lastNamespaceInStack+":"+lastInStack+"] was not closed");
       }

       if(GroovyPage.DEFAULT_NAMESPACE.equals(ns) && tagRegistry.isSyntaxTag(tagName)) {
           if(tm.instance instanceof GroovySyntaxTag) {
               GroovySyntaxTag tag = (GroovySyntaxTag)tm.instance;
               if(tag.isBufferWhiteSpace())
                    bufferWhiteSpace = true;
               tag.doEndTag();
           }
           else {
              throw new GrailsTagException("Grails tag ["+tagName+"] was not closed");
           }
       }
       else {
          out.println("}");
          if(tm.hasAttributes) {
               out.println("invokeTag('"+tagName+"','"+ns+"',attrs"+tagIndex+",body"+tagIndex+")");
          }
          else {
               out.println("invokeTag('"+tagName+"','"+ns+"',[:],body"+tagIndex+")");
          }
       }
       tagIndex--;
    }

    private void startTag() {
        if (!finalPass) return;
        tagIndex++;

        String text;
        StringBuffer buf = new StringBuffer( scan.getToken().trim() );
        String ns = scan.getNamespace();
        
        state = scan.nextToken();
        while(state != HTML && state != GEND_TAG && state != EOF) {
            if(state == GTAG_EXPR) {
                buf.append("${");
                buf.append(scan.getToken().trim());
                buf.append("}");
            }
            else {
                buf.append(scan.getToken().trim());
            }
            state = scan.nextToken();
        }

        doNextScan = false;

        text = buf.toString();

        String tagName;
        Map attrs = new TreeMap();
        text = text.replaceAll("[\r\n]", " ");  // this line added TODO query this
        
        if(text.indexOf(' ') > -1) { // ignores carriage returns and new lines
               int i = text.indexOf(' ');
               tagName = text.substring(0,i);
               String attrTokens = text.substring(i,text.length());
               attrTokens += '>'; // closing bracket marker

               // do first pass parse which retrieves double quoted attributes
                Matcher m = PARSE_TAG_FIRST_PASS.matcher(attrTokens);
                populateAttributesFromMatcher(m,attrs);

               // do second pass parse which retrieves single quoted attributes
               m = PARSE_TAG_SECOND_PASS.matcher(attrTokens);
               populateAttributesFromMatcher(m,attrs);
        }
        else {
            tagName = text;
        }

        if(state == EOF){
            throw new GrailsTagException("Unexpected end of file encountered parsing Tag [" + tagName + "] for " + className + ". Are you missing a closing brace '}'?");
        }

        TagMeta tm = new TagMeta();
        tm.name = tagName;
        tm.namespace = ns;
        tm.hasAttributes = !attrs.isEmpty();
        tm.lineNumber = getCurrentOutputLineNumber();
        tagMetaStack.add(tm);

        if (GroovyPage.DEFAULT_NAMESPACE.equals(ns) && tagRegistry.isSyntaxTag(tagName)) {
            if(this.tagContext == null) {
                this.tagContext = new HashMap();
                this.tagContext.put(GroovyPage.OUT,out);
            }
            GroovySyntaxTag tag = (GroovySyntaxTag)tagRegistry.newTag(tagName);
            tag.init(tagContext);
            tag.setAttributes(attrs);
            if(!tag.hasPrecedingContent() && !bufferWhiteSpace) {
                throw new GrailsTagException("Tag ["+tag.getName()+"] cannot have non-whitespace characters directly preceding it.");
            }
            else if(!tag.hasPrecedingContent() && bufferWhiteSpace) {
                whiteSpaceBuffer.delete(0,whiteSpaceBuffer.length());
                bufferWhiteSpace = false;
            } else {
                if(whiteSpaceBuffer.length() > 0) {
                    out.printlnToResponse(whiteSpaceBuffer.toString());
                    whiteSpaceBuffer.delete(0,whiteSpaceBuffer.length());
                }
                bufferWhiteSpace = false;
            }
            tag.doStartTag();
            tm.instance = tag;
        }
        else {
            if(attrs.size() > 0) {
                out.print("attrs"+tagIndex+" = [");
                for (Iterator i = attrs.keySet().iterator(); i.hasNext();) {
                    String name = (String) i.next();
                    out.print(name);
                    out.print(':');
                    out.print(attrs.get(name));
                    if(i.hasNext())
                        out.print(',');
                    else
                        out.println(']');
                }
            }
            out.println("body"+tagIndex+" = new GroovyPageTagBody(this,binding.webRequest) {" );
        }
    }

    private void populateAttributesFromMatcher(Matcher m, Map attrs) {
        while(m.find()) {
            String name = m.group(2);
            String val = m.group(3);
            name = '\"' + name + '\"';
            if(val.startsWith("${") && val.endsWith("}")) {
                val = val.substring(2,val.length() -1);
            }
            else if(!(val.startsWith("[") && val.endsWith("]"))) {
                val = '\"' + val + '\"';
            }
            attrs.put(name,val);
        }
    }

    private void pageImport(String value) {
//		LOG.debug("pageImport(" + value + ')');
        String[] imports = Pattern.compile(";").split(value.subSequence(0, value.length()));
        for (int ix = 0; ix < imports.length; ix++) {
            out.print("import ");
            out.print(imports[ix]);
            out.println();
        }
    } // pageImport()


    private String escapeGroovy(CharSequence text) {
        StringBuffer buf = new StringBuffer();
        for (int ix = 0, ixz = text.length(); ix < ixz; ix++) {
            char c = text.charAt(ix);
            String rep = null;
            if (c == '\n') {
                incrementLineNumber();
                rep = "\\n";
            }
            else if (c == '\r') rep = "\\r";
            else if (c == '\t') rep = "\\t";
            else if (c == '\'') rep = "\\'";
            else if (c == '\\') rep = "\\\\";
            if (rep != null) buf.append(rep);
            else buf.append(c);
        }
        return buf.toString();
    }

    private String readStream(InputStream in) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try {
            byte[] buf = new byte[8192];
            for (;;) {
                int read = in.read(buf);
                if (read <= 0) break;
                out.write(buf, 0, read);
            }
            return out.toString( gspEncoding);
        } finally {
            out.close();
            in.close();
        }
    } // readStream()

    private void script(boolean gsp) {
        if (!finalPass) return;
        if (LOG.isDebugEnabled()) LOG.debug("parse: script");
        out.println();
        write(scan.getToken().trim(), gsp);
        out.println();
        out.println();
    } // script()

    private void write(CharSequence text, boolean gsp) {
        if (!gsp) {
            out.print(text);
            return;
        }
        for (int ix = 0, ixz = text.length(); ix < ixz; ix++) {
            char c = text.charAt(ix);
            String rep = null;
            if (Character.isWhitespace(c)) {
                for (ix++; ix < ixz; ix++) {
                    if (Character.isWhitespace(text.charAt(ix))) continue;
                    ix--;
                    rep = " ";
                    break;
                }
            } else if (c == '&') {
                if (match("&semi;", text, ix)) {
                    rep = ";";
                    ix += 5;
                } else if (match("&amp;", text, ix)) {
                    rep = "&";
                    ix += 4;
                } else if (match("&lt;", text, ix)) {
                    rep = "<";
                    ix += 3;
                } else if (match("&gt;", text, ix)) {
                    rep = ">";
                    ix += 3;
                }
            } else if (c == '<') {
                if (match("<br>", text, ix) || match("<hr>", text, ix)) {
                    rep = "\n";
                    incrementLineNumber();
                    ix += 3;
                } else {
                    int end = match(PARA_BREAK, text, ix);
                    if (end <= 0) end = match(ROW_BREAK, text, ix);
                    if (end > 0) {
                        rep = "\n";
                        incrementLineNumber();
                        ix = end;
                    }
                }
            }
            if (rep != null) out.print(rep);
            else out.print(c);
        }
    } // write()

    private void incrementLineNumber() {
        currentOutputLine++;
    }
} // Parse
