/*
 *  Copyright 2006-2008 Mikhail Kryshen
 *
 *  This file is part of Tema.
 *
 *  Tema 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 3 of the
 *  License, or (at your option) any later version.
 *
 *  Tema 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 Tema.  
 *  If not, see <http://www.gnu.org/licenses/>.
 *
 *  $Id: TemplateParser.java,v 1.58 2008/02/19 00:20:48 mikhail Exp $
 */

package kryshen.tema;

import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.io.Writer;
import kryshen.tema.io.TemplateReader;

/**
 * Parser for Tema templates.
 *
 * @author Mikhail Kryshen
 */
public class TemplateParser {
    /* Brackets. */
    public static final String[] BRACKET_OPEN = {"<%", "[%"};
    public static final String[] BRACKET_CLOSE = {"%>", "%]"};
    
    public static final String ESCAPE_NEWLINE = "\\";
    public static final String ESCAPE_WHITESPACE = "\\\\";
    
    /* Separators. */
    public static final char[] REC_DATA_SEPARATORS = {':'};
    public static final char[] VERBATIM_DATA_SEPARATORS = {'\\', '`'};
    public static final char[] NOCALL_DATA_SEPARATORS = {'#'};
    public static final char[] LIST_SEPARATORS = {' ', '\t', '\r', '\n'};
    
    /**
     * Specifies how to parse function arguments and data.
     */
    static enum DataFormat {
        RECURSIVE(true, false),
        VERBATIM(false, false),
        NOCALL(false, false),
        SUBFUNCTION(true, true),
        SUBFUNCTION_NOCALL(false, true);
        
        public final boolean call;
        public final boolean subfunction;
        
        DataFormat(boolean call, boolean subfunction) {
            this.call = call;
            this.subfunction = subfunction;
        }
        
        /**
         * Returns no-call equivalent to this format.
         */
        public DataFormat noCall() {
            if (this == RECURSIVE)
                return NOCALL;
            
            if (this == SUBFUNCTION)
                return SUBFUNCTION_NOCALL;
            
            return this;
        }
    };
    
    /**
     * Specifies how the template instruction was terminated.
     */
    static enum Terminator {
        EOF, BRACKET, SEPARATOR;
    };
    
    /**
     * The result of evaluating template instruction.
     */
    static class Result {
        Terminator terminator;
        int retCode;
        
        /** No text had been parsed. */
        boolean empty;
        
        Result(Terminator terminator, int retCode, boolean empty) {
            this.terminator = terminator;
            this.retCode = retCode;
            this.empty = empty;
        }
        
        Result() {
            this(null, -1, true);
        }
    };
    
    protected final Context context;
    
    private int termBracket = -1;
    
    public TemplateParser() {
        this.context = new Context(new GlobalContext());
    }
    
    public TemplateParser(File baseDir) {
        this.context = new Context(new GlobalContext(), baseDir);
    }
    
    public TemplateParser(TemplateParser superParser) {
        this.context = new Context(superParser.context);
    }
    
    public TemplateParser(TemplateParser superParser, File baseDir) {
        this.context = new Context(superParser.context, baseDir);
    }
    
    public TemplateParser(Context context) {
        this.context = context;
    }
    
    /**
     * Parse template
     *
     * @param in TemplateReader to read template data.
     * @param out Writer for the processed data.
     * @throws IOException if case of I/O error.
     * @throws TemplateException in case of parsing error (syntax
     * error or invalid arguments in template instructions).
     */
    public int parse(TemplateReader in, Writer out)
    throws IOException, TemplateException {
        return parse(in, out, DataFormat.RECURSIVE, null).retCode;
    }
    
    Result parse(TemplateReader in, Writer out, DataFormat format)
    throws IOException, TemplateException {
        return parse(in, out, format, null);
    }
    
    /**
     * Parse template.
     *
     * @param in TemplateReader to read template data.
     * @param out Writer to write processed data.
     * @param DataFormat specifies parsing mode.
     */
    Result parse(TemplateReader in, Writer out, DataFormat format,
            char[] separators)
            throws IOException, TemplateException {
        
        Result result = new Result();
        
        if (format == DataFormat.SUBFUNCTION ||
                format == DataFormat.SUBFUNCTION_NOCALL) {
            
            result.retCode = parseFunction(in, out, format);
            result.empty = false;
            return result;
        }
        
        while (true) {
            if (format != DataFormat.VERBATIM) {
                int openBracket = matchInput(in, BRACKET_OPEN);
                
                if (openBracket >= 0) {
                    if (result.retCode < 0)
                        result.retCode = 0;
                    
                    if (!format.call) {
                        out.write(BRACKET_OPEN[openBracket]);
                    }
                    
                    int tb = termBracket;
                    termBracket = openBracket;
                    result.retCode += Math.abs(parseFunction(in, out, format));
                    result.empty = false;
                    termBracket = tb;
                    
                    if (!format.call) {
                        out.write(BRACKET_CLOSE[openBracket]);
                    }
                    
                    continue;
                }
            }
            
            if (matchCloseBracket(in)) {
                result.terminator = Terminator.BRACKET;
                skipEscaped(in);
                break;
            }
            
            int c = in.read();
            
            if (c < 0) {
//                if (termBracket >= 0) {
//                    throw new TemplateException("Unexpected end of file", in);
//                }
                
                result.terminator = Terminator.EOF;
                break;
            }
            
            if (separators != null && isSeparator(c, separators)) {
                result.terminator = Terminator.SEPARATOR;
                break;
            }
            
            out.write(c);
            result.empty = false;
        }
        
        return result;
    }
    
    /**
     * Check if the closing bracket sequence follows in the reader.
     */
    boolean checkCloseBracket(TemplateReader in) throws IOException {
        boolean match = matchCloseBracket(in);
        
        if (!match)
            return false;
        
        // Put the matched characters back to the stream
        in.unread(BRACKET_CLOSE[termBracket].toCharArray());
        
        return true;
    }
    
    private boolean matchCloseBracket(TemplateReader in)
    throws IOException {
        
        if (termBracket < 0)
            return false;
        
        return matchInput(in, BRACKET_CLOSE[termBracket]);
    }
    
    private int matchInput(TemplateReader in, String[] s)
    throws IOException {
        for (int i = 0; i < s.length; i++) {
            if (matchInput(in, s[i]))
                return i;
        }
        
        return -1;
    }
    
    private boolean matchInput(TemplateReader in, String s)
    throws IOException {
        int n = s.length();
        for (int k = 0; k < n; k++) {
            int c = in.read();
            
            if (c != s.charAt(k)) {
                if (c >= 0) {
                    in.unread(c);
                }
                
                for (int j = k - 1; j >= 0; j--) {
                    in.unread(s.charAt(j));
                }
                
                return false;
            }
        }
        
        return true;
    }
    
    void skip(TemplateReader in, char[] chars) throws IOException {
        readLoop:
            while (true) {
                int c = in.read();
                
                if (c < 0)
                    break;
                
                for (int i = 0; i < chars.length; i++) {
                    if (c == chars[i])
                        continue readLoop;
                }
                
                in.unread(c);
                break;
            }
    }
    
    private void skipEscaped(TemplateReader in) throws IOException {
        if (matchInput(in, ESCAPE_WHITESPACE)) {
            int c = in.read();
            
            if (c < 0)
                return;
            
            if (!Character.isWhitespace(c)) {
                in.unread(c);
                in.unread(ESCAPE_WHITESPACE.toCharArray());
                return;
            }
            
            do {
                c = in.read();
            } while (Character.isWhitespace(c));
            
            if (c >= 0)
                in.unread(c);
            
            return;
        }
        
        matchInput(in,
                new String[] {
            ESCAPE_NEWLINE + "\r\n",
            ESCAPE_NEWLINE + "\r",
            ESCAPE_NEWLINE + "\n"});
    }
    
    boolean isSeparator(int c, char[] separators) {
        for (char p : separators) {
            if (c == p) return true;
        }
        
        return false;
    }
    
    private int parseFunction(TemplateReader in, Writer out, DataFormat fd)
    throws IOException, TemplateException {
        
        StringBuffer sb = new StringBuffer();
        
        while (true) {
            if (termBracket >= 0 &&
                    matchInput(in, BRACKET_CLOSE[termBracket])) {
                throw new TemplateException(
                        "Unexpected end of instruction", in);
            }
            
            int c = in.read();
            
            if (c < 0)
                throw new TemplateException("Unexpected end of file.", in);
            
            if (!fd.call)
                out.write(c);
            
            if (isSeparator(c, LIST_SEPARATORS)) {
                return invoke(
                        fd.call ? sb.toString() : "true",
                        fd.call ?
                            DataFormat.SUBFUNCTION :
                            DataFormat.SUBFUNCTION_NOCALL,
                        in, out);
            } else if (isSeparator(c, REC_DATA_SEPARATORS)) {
                skipEscaped(in);
                return invoke(
                        fd.call ? sb.toString() : "true",
                        fd.call ?
                            DataFormat.RECURSIVE :
                            DataFormat.NOCALL,
                        in, out);
            } else if (isSeparator(c, VERBATIM_DATA_SEPARATORS)) {
                return invoke(
                        fd.call ? sb.toString() : "true",
                        DataFormat.VERBATIM, in, out);
            } else if (isSeparator(c, NOCALL_DATA_SEPARATORS)) {
                skipEscaped(in);
                return invoke(
                        fd.call ? sb.toString() : "true",
                        DataFormat.NOCALL, in, out);
            } else {
                sb.append((char)c);
            }
        }
    }
    
    public int parseValue(Object value, Writer out) throws IOException {
        if (value == null)
            return 0;
        
        String svalue = value.toString();
        
        if (svalue == null || svalue.length() == 0)
            return 0;
        
        out.write(svalue);
        return 1;
    }
    
    private int invoke(String name, DataFormat fd,
            TemplateReader in, Writer out)
            throws IOException, TemplateException {
        
        FunctionDataParser fdp = new FunctionDataParser(this, fd, in);
                
        int r = invoke(name, fdp, out);                
        
        if (fdp.hasMoreData()) {
            /* Skip remaining function data. */
            fdp.skipData();
        }
        
        return r;
    }
          
    public int invoke(String name, FunctionDataParser fdp, Writer out)
    throws IOException, TemplateException {
        Object value = context.get(name);
        
        if (value instanceof Function) {
            return ((Function) value).invoke(fdp, out);
        }
        
        if (value instanceof Context) {
            String code = fdp.getData();
            
            if (fdp.getLastReturnCode() == 0)
                return 0;
            
            TemplateParser parser = new TemplateParser((Context) value);
            return parser.parse(
                    new TemplateReader(new StringReader(code)), out);
        }
        
        return parseValue(value, out);
    }
    
    public Context getContext() {
        return context;
    }
}