/*
 * Copyright (C) 2014-present 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 ro.pippo.groovy;

import groovy.text.markup.BaseTemplate;
import groovy.text.markup.MarkupTemplateEngine;
import groovy.text.markup.TemplateConfiguration;

import java.io.IOException;
import java.net.URL;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import org.ocpsoft.prettytime.PrettyTime;

import ro.pippo.core.Languages;
import ro.pippo.core.Messages;
import ro.pippo.core.PippoConstants;
import ro.pippo.core.PippoRuntimeException;
import ro.pippo.core.route.ClasspathResourceHandler;
import ro.pippo.core.route.PublicResourceHandler;
import ro.pippo.core.route.Router;
import ro.pippo.core.route.WebjarsResourceHandler;
import ro.pippo.core.util.StringUtils;

/**
 * Base class for Pippo Groovy Templates.
 *
 * @author James Moger
 */
public abstract class PippoGroovyTemplate extends BaseTemplate {

    private final Map<String, String> modelTypes;

    private final MarkupTemplateEngine engine;

    Router router;

    String language;

    Locale locale;

    Languages languages;

    Messages messages;

    PrettyTime prettyTime;

    AtomicReference<String> webjarsPatternRef;

    AtomicReference<String> publicPatternRef;

    public PippoGroovyTemplate(final MarkupTemplateEngine templateEngine, final Map model,
            final Map<String, String> modelTypes, final TemplateConfiguration configuration) {
        super(templateEngine, model, modelTypes, configuration);

        this.modelTypes = modelTypes;
        this.engine = templateEngine;
        this.webjarsPatternRef = new AtomicReference<>();
        this.publicPatternRef = new AtomicReference<>();
    }

    @SuppressWarnings("unchecked")
    public void setup(Languages languages, Messages messages, Router router) {
        this.languages = languages;
        this.messages = messages;
        this.router = router;

        // set global template variables
        getModel().put("contextPath",  router.getContextPath());
        getModel().put("appPath",  router.getApplicationPath());

        String language = (String) getModel().get(PippoConstants.REQUEST_PARAMETER_LANG);
        if (StringUtils.isNullOrEmpty(language)) {
            language = languages.getLanguageOrDefault(language);
        }
        this.language = language;

        // prepare the locale
        Locale locale = (Locale) getModel().get(PippoConstants.REQUEST_PARAMETER_LOCALE);
        if (locale == null) {
            locale = languages.getLocaleOrDefault(language);
        }
        this.locale = locale;
    }

    public void ln() throws IOException {
        newLine();
    }

    public String webjarsAt(String path) {
        return classpathResourceAt(path, webjarsPatternRef, WebjarsResourceHandler.class);
    }

    public String publicAt(String path) {
        return classpathResourceAt(path, publicPatternRef, PublicResourceHandler.class);
    }

    private String classpathResourceAt(String path, AtomicReference<String> patternRef,
                                       Class<? extends ClasspathResourceHandler> resourceHandlerClass) {

        if (patternRef.get() == null) {
            String pattern = router.uriPatternFor(resourceHandlerClass);
            if (pattern == null) {
                throw new PippoRuntimeException("You must register a route for {}",
                        resourceHandlerClass.getSimpleName());
            }

            patternRef.set(pattern);
        }

        Map<String, Object> parameters = new HashMap<>();
        parameters.put(ClasspathResourceHandler.PATH_PARAMETER, path);

        return router.uriFor(patternRef.get(), parameters);
    }

    public String i18n(String messageKey) throws IOException {
        return messages.get(messageKey, language);
    }

    public String i18n(String messageKey, Object... args) throws IOException {
        return messages.get(messageKey, language, args);
    }

    public String formatTime(Object input, String styleOrPattern) {
        Date date = getDateObject(input);

        DateFormat df;
        int type = parseStyle(styleOrPattern);
        if (type == -1) {
            df = new SimpleDateFormat(styleOrPattern, locale);
        } else {
            df = DateFormat.getDateTimeInstance(type, type, locale);
        }

        return df.format(date);
    }

    private Date getDateObject(Object value) {

        if (value instanceof Date) {
            return (Date) value;
        } else if (value instanceof Calendar) {
            return ((Calendar) value).getTime();
        } else if (value instanceof Long) {
            return new Date((Long) value);
        } else {
            throw new PippoRuntimeException("Failed to get a date object from {}!", value);
        }
    }

    protected Integer parseStyle(String style) {
        if ("full".equals(style)) {
            return DateFormat.FULL;
        } else if ("long".equals(style)) {
            return DateFormat.LONG;
        } else if ("short".equals(style)) {
            return DateFormat.SHORT;
        } else if ("medium".equals(style)) {
            return DateFormat.MEDIUM;
        } else {
            return -1;
        }
    }

    public String prettyTime(Object input) {
        if (prettyTime == null) {
            this.prettyTime = new PrettyTime(locale);
        }
        Date date = getDateObject(input);

        return prettyTime.format(date);
    }

    public String ng(String content) throws IOException {
        return "{{ " + content + " }}";
    }

    /**
     * Imports a template and renders it using the specified model, allowing
     * fine grained composition of templates and layouting. This works
     * similarily to a template include but allows a distinct model to be used.
     * If the layout inherits from the parent model, a new model is created,
     * with the values from the parent model, eventually overriden with those
     * provided specifically for this layout.
     *
     * @param model
     *            model to be passed to the template
     * @param templateName
     *            the name of the template to be used as a layout
     * @param inheritModel
     *            a boolean indicating if we should inherit the parent model
     * @return this template instance
     * @throws IOException
     * @throws ClassNotFoundException
     */
    @Override
    public Object layout(Map model, String templateName, boolean inheritModel) throws IOException,
            ClassNotFoundException {
        Map submodel = inheritModel ? forkModel(model) : model;
        URL resource = engine.resolveTemplate(templateName);
        PippoGroovyTemplate template = (PippoGroovyTemplate) engine
                .createTypeCheckedModelTemplate(resource, modelTypes).make(submodel);
        template.setup(languages, messages, router);
        template.writeTo(getOut());

        return this;
    }

    @SuppressWarnings("unchecked")
    private Map forkModel(Map m) {
        Map result = new HashMap();
        result.putAll(getModel());
        result.putAll(m);

        return result;
    }

}
