package org.apache.velocity.runtime.directive;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

import org.apache.velocity.Template;
import org.apache.velocity.context.InternalContextAdapter;
import org.apache.velocity.exception.MethodInvocationException;
import org.apache.velocity.exception.ParseErrorException;
import org.apache.velocity.exception.ResourceNotFoundException;
import org.apache.velocity.exception.TemplateInitException;
import org.apache.velocity.exception.VelocityException;
import org.apache.velocity.runtime.Renderable;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.RuntimeConstants.SpaceGobbling;
import org.apache.velocity.runtime.RuntimeServices;
import org.apache.velocity.runtime.parser.Token;
import org.apache.velocity.runtime.parser.node.ASTDirective;
import org.apache.velocity.runtime.parser.node.Node;
import org.apache.velocity.runtime.parser.node.StandardParserTreeConstants;
import org.apache.velocity.util.StringUtils;

import org.apache.commons.lang3.Validate;

import java.io.IOException;
import java.io.Writer;
import java.util.List;

/**
 * This class acts as a proxy for potential macros.  When the AST is built
 * this class is inserted as a placeholder for the macro (whether or not
 * the macro is actually defined).  At render time we check whether there is
 * a implementation for the macro call. If an implementation cannot be
 * found the literal text is rendered.
 * @since 1.6
 */
public class RuntimeMacro extends Directive
{
    /**
     * Name of the macro
     */
    private String macroName;

    /**
     * Literal text of the macro
     */
    private String literal = null;

    /**
     * Node of the macro call
     */
    private Node node = null;

    /**
     * Indicates if we are running in strict reference mode.
     */
    protected boolean strictRef = false;

    /**
     * badArgsErrorMsg will be non null if the arguments to this macro
     * are deamed bad at init time, see the init method.  If his is non null, then this macro
     * cannot be rendered, and if there is an attempt to render we throw an exception
     * with this as the message.
     */
    private String badArgsErrorMsg = null;

    /**
     * Return name of this Velocimacro.
     *
     * @return The name of this Velocimacro.
     */
    @Override
    public String getName()
    {
        return macroName;
    }

    /**
     * Override to always return "macro".  We don't want to use
     * the macro name here, since when writing VTL that uses the
     * scope, we are within a #macro call.  The macro name will instead
     * be used as the scope name when defining the body of a BlockMacro.
     */
    @Override
    public String getScopeName()
    {
        return "macro";
    }

    /**
     * Velocimacros are always LINE
     * type directives.
     *
     * @return The type of this directive.
     */
    @Override
    public int getType()
    {
        return LINE;
    }


    /**
     * Initialize the Runtime macro. At the init time no implementation so we
     * just save the values to use at the render time.
     *
     * @param rs runtime services
     * @param name macro name
     * @param context InternalContextAdapter
     * @param node node containing the macro call
     */
    public void init(RuntimeServices rs, String name, InternalContextAdapter context,
                     Node node)
    {
        super.init(rs, context, node);

        macroName = Validate.notNull(name);
        macroName = rsvc.useStringInterning() ? macroName.intern() : macroName;
        this.node = node;

        /*
         * Apply strictRef setting only if this really looks like a macro,
         * so strict mode doesn't balk at things like #E0E0E0 in a template.
         * compare with ")" is a simple #foo() style macro, comparing to
         * "#end" is a block style macro. We use starts with because the token
         * may end with '\n'
         */
        // Tokens can be used here since we are in init() and Tokens have not been dropped yet
        Token t = node.getLastToken();
        if (t.image.startsWith(")") || t.image.startsWith(rsvc.getParserConfiguration().getHashChar() + "end"))
        {
            strictRef = rsvc.getBoolean(RuntimeConstants.RUNTIME_REFERENCES_STRICT, false);
        }

        // Validate that none of the arguments are plain words, (VELOCITY-614)
        // they should be string literals, references, inline maps, or inline lists
        for (int n=0; n < node.jjtGetNumChildren(); n++)
        {
            Node child = node.jjtGetChild(n);
            if (child.getType() == StandardParserTreeConstants.JJTWORD)
            {
                badArgsErrorMsg = "Invalid arg '" + child.getFirstTokenImage()
                + "' in macro #" + macroName + " at " + StringUtils.formatFileString(child);

                if (strictRef)  // If strict, throw now
                {
                    /* indicate col/line assuming it starts at 0
                     * this will be corrected one call up  */
                    throw new TemplateInitException(badArgsErrorMsg,
                    null,
                    rsvc.getLogContext().getStackTrace(),
                    context.getCurrentTemplateName(), 0, 0);
                }
            }
        }
        // TODO: Improve this
        // this is only needed if the macro does not exist during runtime
        // since tokens are eliminated after this init call, we have to create a cached version of the
        // literal which is in 99.9% cases waste. However, for regular macro calls (non Block macros)
        // this doesn't create very long Strings so it's probably acceptable
        getLiteral();
    }

    /**
     * It is probably quite rare that we need to render the macro literal
     * but since we won't keep the tokens in memory, we need to calculate it
     * at parsing time.
     */
    private String getLiteral()
    {
        SpaceGobbling spaceGobbling = rsvc.getSpaceGobbling();
        ASTDirective directive = (ASTDirective)node;

        String morePrefix = directive.getMorePrefix();

        if (literal == null)
        {
            StringBuilder buffer = new StringBuilder();
            Token t = node.getFirstToken();

            /* avoid outputting twice the prefix and the 'MORE' prefix,
             * but still display the prefix in the cases where the ASTDirective would hide it */
            int pos = -1;
            while (t != null && t != node.getLastToken())
            {
                if (pos == -1) pos = t.image.lastIndexOf(rsvc.getParserConfiguration().getHashChar());
                if (pos != -1)
                {
                    buffer.append(t.image.substring(pos));
                    pos = 0;
                }
                else if (morePrefix.length() == 0 && spaceGobbling.compareTo(SpaceGobbling.LINES) >= 0)
                {
                    buffer.append(t.image);
                }
                t = t.next;
            }

            if (t != null)
            {
                if (pos == -1) pos = t.image.lastIndexOf(rsvc.getParserConfiguration().getHashChar());
                if (pos != -1)
                {
                    buffer.append(t.image.substring(pos));
                }
            }

            literal = buffer.toString();
            /* avoid outputting twice the postfix, but still display it in the cases
             * where the ASTDirective would hide it */
            String postfix = directive.getPostfix();
            if ((morePrefix.length() > 0 || spaceGobbling == SpaceGobbling.NONE) && literal.endsWith(postfix))
            {
                literal = literal.substring(0, literal.length() - postfix.length());
            }
        }
        return literal;
    }


    /**
     * Velocimacro implementation is not known at the init time. So look for
     * a implementation in the macro libraries and if finds one renders it. The
     * actual rendering is delegated to the VelocimacroProxy object. When
     * looking for a macro we first loot at the template with has the
     * macro call then we look at the macro libraries in the order they appear
     * in the list. If a macro has many definitions above look up will
     * determine the precedence.
     *
     * @param context
     * @param writer
     * @param node
     * @return true if the rendering is successful
     * @throws IOException
     * @throws ResourceNotFoundException
     * @throws ParseErrorException
     * @throws MethodInvocationException
     */
    @Override
    public boolean render(InternalContextAdapter context, Writer writer,
                          Node node)
            throws IOException, ResourceNotFoundException,
            ParseErrorException, MethodInvocationException
    {
        return render(context, writer, node, null);
    }

    /**
     * This method is used with BlockMacro when we want to render a macro with a body AST.
     *
     * @param context
     * @param writer
     * @param node
     * @param body AST block that was enclosed in the macro body.
     * @return true if the rendering is successful
     * @throws IOException
     * @throws ResourceNotFoundException
     * @throws ParseErrorException
     * @throws MethodInvocationException
     */
    public boolean render(InternalContextAdapter context, Writer writer,
                          Node node, Renderable body)
            throws IOException, ResourceNotFoundException,
            ParseErrorException, MethodInvocationException
    {
        VelocimacroProxy vmProxy = null;
        Template renderingTemplate = (Template)context.getCurrentResource();

        /*
         * first look in the source template
         */
        Object o = rsvc.getVelocimacro(macroName, renderingTemplate, getTemplate());

        if( o != null )
        {
            // getVelocimacro can only return a VelocimacroProxy so we don't need the
            // costly instanceof check
            vmProxy = (VelocimacroProxy)o;
        }

        /*
         * if not found, look in the macro libraries.
         */
        if (vmProxy == null)
        {
            List<Template> macroLibraries = context.getMacroLibraries();
            if (macroLibraries != null)
            {
                for (int i = macroLibraries.size() - 1; i >= 0; i--)
                {
                    o = rsvc.getVelocimacro(macroName, renderingTemplate, macroLibraries.get(i));

                    // get the first matching macro
                    if (o != null)
                    {
                        vmProxy = (VelocimacroProxy) o;
                        break;
                    }
                }
            }
        }

        if (vmProxy != null)
        {
            if (badArgsErrorMsg != null)
            {
                throw new TemplateInitException(badArgsErrorMsg,
                  null, rsvc.getLogContext().getStackTrace(),
                  context.getCurrentTemplateName(), node.getColumn(), node.getLine());
            }

            try
            {
                preRender(context);
                return vmProxy.render(context, writer, node, body);
            }
            catch (StopCommand stop)
            {
                if (!stop.isFor(this))
                {
                    throw stop;
                }
                return true;
            }
            catch (RuntimeException | IOException e)
            {
                /*
                 * We catch, the exception here so that we can record in
                 * the logs the template and line number of the macro call
                 * which generate the exception.  This information is
                 * especially important for multiple macro call levels.
                 * this is also true for the following catch blocks.
                 */
                log.error("Exception in macro #{} called at {}",
                          macroName, StringUtils.formatFileString(node));
                throw e;
            }
            finally
            {
                postRender(context);
            }
        }
        else if (strictRef)
        {
            throw new VelocityException("Macro '#" + macroName + "' is not defined at "
                + StringUtils.formatFileString(node), null, rsvc.getLogContext().getStackTrace());
        }

        /*
         * If we cannot find an implementation write the literal text
         */
        writer.write(getLiteral());
        return true;
    }
}
