/*
 * 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.
 */
package com.gitblit.wicket.freemarker;

import java.io.IOException;
import java.io.StringWriter;
import java.util.Map;

import org.apache.wicket.MarkupContainer;
import org.apache.wicket.WicketRuntimeException;
import org.apache.wicket.markup.ComponentTag;
import org.apache.wicket.markup.IMarkupCacheKeyProvider;
import org.apache.wicket.markup.IMarkupResourceStreamProvider;
import org.apache.wicket.markup.MarkupStream;
import org.apache.wicket.markup.html.panel.Panel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.apache.wicket.util.resource.IResourceStream;
import org.apache.wicket.util.resource.StringResourceStream;
import org.apache.wicket.util.string.Strings;

import com.gitblit.utils.StringUtils;

import freemarker.template.Template;
import freemarker.template.TemplateException;

/**
 * This class allows FreeMarker to be used as a Wicket preprocessor or as a
 * snippet injector for something like a CMS.  There are some cases where Wicket
 * is not flexible enough to generate content, especially when you need to generate
 * hybrid HTML/JS content outside the scope of Wicket.
 * 
 * @author James Moger
 * 
 */
@SuppressWarnings("unchecked")
public class FreemarkerPanel extends Panel
		implements
			IMarkupResourceStreamProvider,
			IMarkupCacheKeyProvider
{
	private static final long serialVersionUID = 1L;

	private final String template;
	private boolean parseGeneratedMarkup;
	private boolean escapeHtml;
	private boolean throwFreemarkerExceptions;
	private transient String stackTraceAsString;
	private transient String evaluatedTemplate;

	
	/**
	 * Construct.
	 * 
	 * @param id
	 *            Component id
	 * @param template
	 *            The Freemarker template
	 * @param values
	 *            values map that can be substituted by Freemarker.
	 */
	public FreemarkerPanel(final String id, String template, final Map<String, Object> values)
	{
		this(id, template, Model.ofMap(values));
	}
	
	/**
	 * Construct.
	 * 
	 * @param id
	 *            Component id
	 * @param templateResource
	 *            The Freemarker template as a string resource
	 * @param model
	 *            Model with variables that can be substituted by Freemarker.
	 */
	public FreemarkerPanel(final String id, final String template, final IModel< ? extends Map<String, Object>> model)
	{
		super(id, model);
		this.template = template;
	}

	/**
	 * Gets the Freemarker template.
	 * 
	 * @return the Freemarker template
	 */
	private Template getTemplate()
	{
		if (StringUtils.isEmpty(template))
		{
			throw new IllegalArgumentException("Template not specified!");
		}

		try {
			return Freemarker.getTemplate(template);
		} catch (IOException e) {
			onException(e);
		}

		return null;
	}

	/**
	 * @see org.apache.wicket.markup.html.panel.Panel#onComponentTagBody(org.apache.wicket.markup.
	 *      MarkupStream, org.apache.wicket.markup.ComponentTag)
	 */
	@Override
	protected void onComponentTagBody(MarkupStream markupStream, ComponentTag openTag)
	{
		if (!Strings.isEmpty(stackTraceAsString))
		{
			// TODO: only display the Freemarker error/stacktrace in development
			// mode?
			replaceComponentTagBody(markupStream, openTag, Strings
					.toMultilineMarkup(stackTraceAsString));
		}
		else if (!parseGeneratedMarkup)
		{
			// check that no components have been added in case the generated
			// markup should not be
			// parsed
			if (size() > 0)
			{
				throw new WicketRuntimeException(
						"Components cannot be added if the generated markup should not be parsed.");
			}

			if (evaluatedTemplate == null)
			{
				// initialize evaluatedTemplate
				getMarkupResourceStream(null, null);
			}
			replaceComponentTagBody(markupStream, openTag, evaluatedTemplate);
		}
		else
		{
			super.onComponentTagBody(markupStream, openTag);
		}
	}

	/**
	 * Either print or rethrow the throwable.
	 * 
	 * @param exception
	 *            the cause
	 * @param markupStream
	 *            the markup stream
	 * @param openTag
	 *            the open tag
	 */
	private void onException(final Exception exception)
	{
		if (!throwFreemarkerExceptions)
		{
			// print the exception on the panel
			stackTraceAsString = Strings.toString(exception);
		}
		else
		{
			// rethrow the exception
			throw new WicketRuntimeException(exception);
		}
	}

	/**
	 * Gets whether to escape HTML characters.
	 * 
	 * @return whether to escape HTML characters. The default value is false.
	 */
	public void setEscapeHtml(boolean value)
	{
		this.escapeHtml = value;
	}

	/**
	 * Evaluates the template and returns the result.
	 * 
	 * @param templateReader
	 *            used to read the template
	 * @return the result of evaluating the velocity template
	 */
	private String evaluateFreemarkerTemplate(Template template)
	{
		if (evaluatedTemplate == null)
		{
			// Get model as a map
			final Map<String, Object> map = (Map<String, Object>)getDefaultModelObject();

			// create a writer for capturing the Velocity output
			StringWriter writer = new StringWriter();

			// string to be used as the template name for log messages in case
			// of error
			try
			{
				// execute the Freemarker script and capture the output in writer
				Freemarker.evaluate(template, map, writer);

				// replace the tag's body the Freemarker output
				evaluatedTemplate = writer.toString();

				if (escapeHtml)
				{
					// encode the result in order to get valid html output that
					// does not break the rest of the page
					evaluatedTemplate = Strings.escapeMarkup(evaluatedTemplate).toString();
				}
				return evaluatedTemplate;
			}
			catch (IOException e)
			{
				onException(e);
			}
			catch (TemplateException e)
			{
				onException(e);
			}
			return null;
		}
		return evaluatedTemplate;
	}

	/**
	 * Gets whether to parse the resulting Wicket markup.
	 * 
	 * @return whether to parse the resulting Wicket markup. The default is false.
	 */
	public void setParseGeneratedMarkup(boolean value)
	{
		this.parseGeneratedMarkup = value;
	}

	/**
	 * Whether any Freemarker exception should be trapped and displayed on the panel (false) or thrown
	 * up to be handled by the exception mechanism of Wicket (true). The default is false, which
	 * traps and displays any exception without having consequences for the other components on the
	 * page.
	 * <p>
	 * Trapping these exceptions without disturbing the other components is especially useful in CMS
	 * like applications, where 'normal' users are allowed to do basic scripting. On errors, you
	 * want them to be able to have them correct them while the rest of the application keeps on
	 * working.
	 * </p>
	 * 
	 * @return Whether any Freemarker exceptions should be thrown or trapped. The default is false.
	 */
	public void setThrowFreemarkerExceptions(boolean value)
	{
		this.throwFreemarkerExceptions = value;
	}

	/**
	 * @see org.apache.wicket.markup.IMarkupResourceStreamProvider#getMarkupResourceStream(org.apache
	 *      .wicket.MarkupContainer, java.lang.Class)
	 */
	public final IResourceStream getMarkupResourceStream(MarkupContainer container,
			Class< ? > containerClass)
	{
		Template template = getTemplate();
		if (template == null)
		{
			throw new WicketRuntimeException("could not find Freemarker template for panel: " + this);
		}

		// evaluate the template and return a new StringResourceStream
		StringBuffer sb = new StringBuffer();
		sb.append("<wicket:panel>");
		sb.append(evaluateFreemarkerTemplate(template));
		sb.append("</wicket:panel>");
		return new StringResourceStream(sb.toString());
	}

	/**
	 * @see org.apache.wicket.markup.IMarkupCacheKeyProvider#getCacheKey(org.apache.wicket.
	 *      MarkupContainer, java.lang.Class)
	 */
	public final String getCacheKey(MarkupContainer container, Class< ? > containerClass)
	{
		// don't cache the evaluated template
		return null;
	}

	/**
	 * @see org.apache.wicket.Component#onDetach()
	 */
	@Override
	protected void onDetach()
	{
		super.onDetach();
		stackTraceAsString = null;
		evaluatedTemplate = null;
	}
}
