package com.xfltr.hapax;

import com.xfltr.hapax.parser.CTemplateParser;
import com.xfltr.hapax.parser.TemplateParser;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.util.HashMap;
import java.util.Map;

/**
 * TemplateCache constructs {@link Template} objects based on files stored on
 * disk. TemplateCache implements an in-memory unbounded cache of parsed
 * Templates. Each stored Template is automatically invalidated if the template
 * file on disk changes.
 *
 * See examples/TemplateCacheExample.java for an example of how to use
 * TemplateCache.
 *
 * It is recommended that you place a reference to your TemplateCache instance
 * in some globally accessible location so that you use a single cache across
 * multiple requests.
 *
 * @author dcoker
 */
public class TemplateCache implements TemplateLoader {

    private final Map<String, Template> templates_ =
            new HashMap<String, Template>();
    private final Map<String, Long> lastUpdated_ =
            new HashMap<String, Long>();
    private final String basePath_;
    private final TemplateParser parser_;

    /**
     * Creates a TemplateLoader using the CTemplateParser.
     */
    public static TemplateLoader create(String base_path) {
        return new TemplateCache(CTemplateParser.create(), base_path);
    }

    /**
     * Creates a TemplateLoader using the given TemplateParser.
     */
    public static TemplateLoader createForParser(String base_path,
            TemplateParser parser) {
        return new TemplateCache(parser, base_path);
    }

    /**
     * Parses and fetches a template from disk.
     *
     * @param filename The path to the template, relative to the
     * templateDirectory passed to the ctor of TemplateCache.
     */
    public Template getTemplate(String filename) throws TemplateException {
        filename = PathUtil.join(basePath_, filename);

        // We check the last modified timestamp on the template once per render.
        // If the template has changed, we reload and reparse it.  Otherwise, we
        // return the copy from memory.
        File file = new File(filename);
        long last_modified = file.lastModified();

        if (inCache(filename, last_modified)) {
            return templates_.get(filename);
        }

        FileReader reader;
        try {
            reader = new FileReader(filename);
        } catch (FileNotFoundException e) {
            throw new TemplateException(e);
        }

        String contents;
        try {
            contents = readToString(reader);
        } catch (IOException e) {
            throw new TemplateException(e);
        }

        Template results;
        results = Template.parse(parser_, contents);
        results.setLoaderContext(new TemplateLoaderContext(this, file.getParent()));
        updateCache(filename, results, last_modified);
        return results;
    }

    /**
     * Parses and fetches a template from a subdirectory of the configured
     * basePath. This is useful when fetching templates with paths relative to
     * other templates (such as in includes).
     */
    public Template getTemplate(String filename, String templateDirectory)
            throws TemplateException {
        assert templateDirectory.startsWith(basePath_);
        String directory_relative_to_template_directory =
                PathUtil.makeRelative(basePath_, "");
        // Construct the filename that we use in the cache
        filename =
                PathUtil.join(directory_relative_to_template_directory, filename);
        return getTemplate(filename);
    }

    /**
     * Private constructor.
     */
    private TemplateCache(TemplateParser parser, String templateDirectory) {
        this.basePath_ = templateDirectory;
        this.parser_ = parser;
    }

    private String readToString(Reader in) throws IOException {
        StringBuilder buf = new StringBuilder();
        try {
            for (int c = in.read(); -1 != c; c = in.read()) {
                buf.append((char) c);
            }
            return buf.toString();
        } finally {
            in.close();
        }
    }

    private boolean inCache(String filename, long last_modified) {
        return lastUpdated_.containsKey(filename)
                && lastUpdated_.get(filename) >= last_modified;
    }

    private void updateCache(String filename, Template results,
            long last_modified) {
        templates_.put(filename, results);
        lastUpdated_.put(filename, last_modified);
    }
}
