package enkan.component.freemarker;

import enkan.component.ComponentLifecycle;
import enkan.data.HttpResponse;
import enkan.exception.MisconfigurationException;
import enkan.util.HttpResponseUtils;
import freemarker.cache.ClassTemplateLoader;
import freemarker.cache.MultiTemplateLoader;
import freemarker.cache.TemplateLoader;
import freemarker.core.HTMLOutputFormat;
import freemarker.core.OutputFormat;
import freemarker.ext.beans.BeanModel;
import freemarker.ext.beans.StringModel;
import freemarker.template.*;
import kotowari.component.TemplateEngine;
import kotowari.data.TemplatedHttpResponse;
import kotowari.data.Validatable;
import kotowari.io.LazyRenderInputStream;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.util.List;
import java.util.Locale;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * Template engine component using by Freemarker.
 *
 * @author kawasima
 */
public class FreemarkerTemplateEngine extends TemplateEngine<FreemarkerTemplateEngine> {
    private Configuration config;
    private String prefix = "templates";
    private String suffix = ".ftl";
    private ClassLoader classLoader;
    private String encoding = "UTF-8";
    private TemplateLoader templateLoader;

    private OutputFormat outputFormat = HTMLOutputFormat.INSTANCE;

    /**
     * {@inheritDoc}
     */
    @Override
    public HttpResponse render(String name, Object... keyOrVals) {
        TemplatedHttpResponse response = TemplatedHttpResponse.create(name, keyOrVals);
        response.setBody(new LazyRenderInputStream(() -> {
            try {
                Template template = config.getTemplate(name + suffix, encoding);
                StringWriter writer = new StringWriter();
                template.process(response.getContext(), writer);
                return new ByteArrayInputStream(writer.toString().getBytes(encoding));
            } catch (TemplateException e) {
                throw new MisconfigurationException("freemarker.TEMPLATE", e.getFTLInstructionStack(), e.getMessageWithoutStackTop(), e);
            } catch (IOException e) {
                throw new MisconfigurationException("freemarker.TEMPLATE", e.getMessage(),
                        String.format(Locale.US, "Make a template '%s'.", name), e);
            }

        }));
        HttpResponseUtils.contentType(response, "text/html");
        return response;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected ComponentLifecycle<FreemarkerTemplateEngine> lifecycle() {
        return new ComponentLifecycle<FreemarkerTemplateEngine>() {
            @Override
            public void start(FreemarkerTemplateEngine component) {
                if (classLoader == null) {
                    classLoader = Thread.currentThread().getContextClassLoader();
                }
                config = new Configuration(new Version(2,3,27));
                config.setTemplateLoader(createTemplateLoader());
                config.setOutputFormat(outputFormat);
                config.setObjectWrapper(new DefaultObjectWrapper(new Version(2,3,27)) {
                    @Override
                    protected TemplateModel handleUnknownType(final Object obj) throws TemplateModelException {
                        if (obj instanceof Validatable) {
                            return new ValidatableFormAdapter((Validatable) obj, this);
                        }
                        return super.handleUnknownType(obj);
                    }
                });
            }

            @Override
            public void stop(FreemarkerTemplateEngine component) {
                config = null;
                classLoader = null;
            }
        };
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    @Override
    public Object createFunction(Function<List, Object> func) {
        return (TemplateMethodModelEx) arguments ->
                func.apply((List)arguments.stream().map(arg -> {
                    if (arg instanceof BeanModel) {
                        return ((StringModel) arg).getWrappedObject();
                    } else {
                        return arg;
                    }
                }).collect(Collectors.toList()));
    }

    private TemplateLoader createTemplateLoader() {
        TemplateLoader classTemplateLoader = new ClassTemplateLoader(classLoader, prefix);
        if (templateLoader != null) {
            if (templateLoader instanceof MultiTemplateLoader) {
                MultiTemplateLoader mtl = (MultiTemplateLoader) templateLoader;
                TemplateLoader[] loaders = new TemplateLoader[mtl.getTemplateLoaderCount() + 1];
                for(int i=0; i < mtl.getTemplateLoaderCount(); i++) {
                    loaders[i] = mtl.getTemplateLoader(i);
                }
                loaders[mtl.getTemplateLoaderCount() + 1] = classTemplateLoader;
                return new MultiTemplateLoader(loaders);

            } else {
                return new MultiTemplateLoader(new TemplateLoader[]{templateLoader, classTemplateLoader});
            }
        } else {
            return classTemplateLoader;
        }
    }

    /**
     * Set an output format used by this freemarker.
     *
     * @param outputFormat FreeMarker output format
     */
    public void setOutputFormat(OutputFormat outputFormat) {
        this.outputFormat = outputFormat;
    }

    /**
     * Set an template loader used by this freemarker.
     *
     * @param templateLoader template loader
     */
    public void setTemplateLoader(TemplateLoader templateLoader) {
        this.templateLoader = templateLoader;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    public void setSuffix(String suffix) {
        this.suffix = suffix;
    }
}