package org.bndtools.templating.engine.mustache;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;

import org.bndtools.templating.Resource;
import org.bndtools.templating.ResourceMap;
import org.bndtools.templating.ResourceType;
import org.bndtools.templating.StringResource;
import org.bndtools.templating.TemplateEngine;
import org.eclipse.core.runtime.IProgressMonitor;
import org.osgi.service.component.annotations.Component;

import com.github.mustachejava.DefaultMustacheFactory;

import aQute.bnd.osgi.Constants;
import aQute.bnd.osgi.Instructions;
import aQute.lib.io.IO;

@Component(name = "org.bndtools.templating.engine.mustache", property = {
	"name=mustache", "version=0.8.18"
})
public class MustacheTemplateEngine implements TemplateEngine {

	private static final String	TEMPLATE_PROPERTIES	= "_template.properties";
	private static final String	DEFAULT_PROPERTIES	= "_defaults.properties";

	private static final String	DEFAULT_LEFT_DELIM	= "{{";
	private static final String	DEFAULT_RIGHT_DELIM	= "}}";

	private static class TemplateSettings {
		String			leftDelim		= DEFAULT_LEFT_DELIM;
		String			rightDelim		= DEFAULT_RIGHT_DELIM;
		Instructions	preprocessMatch	= new Instructions("*");
		Instructions	ignore			= null;

		private TemplateSettings() {}

		static TemplateSettings readFrom(Properties props) {
			TemplateSettings settings = new TemplateSettings();
			if (props != null) {
				settings.leftDelim = props.getProperty("leftDelim", DEFAULT_LEFT_DELIM);
				settings.rightDelim = props.getProperty("rightDelim", DEFAULT_RIGHT_DELIM);
				String process = props.getProperty("process", Constants.DEFAULT_PREPROCESSS_MATCHERS);
				String processBefore = props.getProperty("process.before", null);
				if (processBefore != null)
					process = processBefore + ", " + process;
				String processAfter = props.getProperty("process.after", null);
				if (processAfter != null)
					process = process + ", " + processAfter;
				settings.preprocessMatch = new Instructions(process);

				String ignore = props.getProperty("ignore", null);
				settings.ignore = ignore != null ? new Instructions(ignore) : null;
			}
			return settings;
		}
	}

	@Override
	public Map<String, String> getTemplateParameters(ResourceMap inputs, IProgressMonitor monitor) throws Exception {
		final Map<String, String> params = new HashMap<>();
		final Properties defaults = readDefaults(inputs);

		TemplateSettings settings = readSettings(inputs);
		DefaultMustacheFactory factory = new DefaultMustacheFactory();
		AccumulateNamesObjectHandler namesAccumulator = new AccumulateNamesObjectHandler(factory.getObjectHandler());
		factory.setObjectHandler(namesAccumulator);

		int counter = 0;
		for (Entry<String, Resource> entry : inputs.entries()) {
			String inputPath = entry.getKey();
			factory.compile(new StringReader(inputPath), "mapping", settings.leftDelim, settings.rightDelim)
				.execute(new StringWriter(), Collections.emptyMap());
			Resource source = entry.getValue();
			if (settings.ignore == null || !settings.ignore.matches(inputPath)) {
				if (source.getType() == ResourceType.File && settings.preprocessMatch.matches(inputPath)) {
					try (Reader reader = IO.reader(source.getContent(), source.getTextEncoding())) {
						factory.compile(reader, "temp" + (counter++), settings.leftDelim, settings.rightDelim)
							.execute(new StringWriter(), Collections.emptyMap())
							.toString();
					}
				}
			}
		}

		for (String param : namesAccumulator.getNames()) {
			params.put(param, defaults.getProperty(param));
		}

		return params;
	}

	@Override
	public ResourceMap generateOutputs(ResourceMap inputs, Map<String, List<Object>> parameters,
		IProgressMonitor monitor) throws Exception {
		TemplateSettings settings = readSettings(inputs);
		Properties defaults = readDefaults(inputs);

		ResourceMap outputs = new ResourceMap();
		final Map<String, Object> flattenedParams = flattenParameters(parameters);
		applyDefaults(defaults, flattenedParams);

		DefaultMustacheFactory mustacheFactory = new DefaultMustacheFactory();
		mustacheFactory.setObjectHandler(new CheckMissingObjectHandler(mustacheFactory.getObjectHandler()));

		for (Entry<String, Resource> entry : inputs.entries()) {
			String inputPath = entry.getKey();
			Resource source = entry.getValue();

			StringWriter writer = new StringWriter();
			mustacheFactory.compile(new StringReader(inputPath), "mapping", settings.leftDelim, settings.rightDelim)
				.execute(writer, flattenedParams);
			String outputPath = writer.toString();

			if (settings.ignore == null || !settings.ignore.matches(inputPath)) {
				Resource output;
				switch (source.getType()) {
					case Folder :
						output = source;
						break;
					case File :
						if (settings.preprocessMatch.matches(inputPath)) {
							// This file should be processed with the template
							// engine
							try (Reader reader = IO.reader(source.getContent(), source.getTextEncoding())) {
								StringWriter rendered = new StringWriter();
								mustacheFactory.compile(reader, outputPath, settings.leftDelim, settings.rightDelim)
									.execute(rendered, flattenedParams);
								output = new StringResource(rendered.toString());
							}
						} else {
							// This file should be directly copied
							output = source;
						}
						break;
					default :
						throw new IllegalArgumentException("Unknown resource type " + source.getType());
				}
				outputs.put(outputPath, output);
			}
		}

		return outputs;
	}

	private static void applyDefaults(Properties defaults, Map<String, Object> params) {
		for (Enumeration<?> defaultsEnum = defaults.propertyNames(); defaultsEnum.hasMoreElements();) {
			String name = (String) defaultsEnum.nextElement();
			String value = defaults.getProperty(name, null);
			if (!params.containsKey(name))
				params.put(name, value);
		}
	}

	private static TemplateSettings readSettings(ResourceMap inputs) throws IOException, UnsupportedEncodingException {
		Properties settingsProp = new Properties();
		Resource settingsResource = inputs.remove(TEMPLATE_PROPERTIES);
		if (settingsResource != null) {
			if (settingsResource.getType() != ResourceType.File)
				throw new IllegalArgumentException(
					String.format("Template settings resource %s must be a file; found resource type %s.",
						TEMPLATE_PROPERTIES, settingsResource.getType()));
			try (Reader reader = new InputStreamReader(settingsResource.getContent(),
				settingsResource.getTextEncoding())) {
				settingsProp.load(reader);
			}
		}
		TemplateSettings settings = TemplateSettings.readFrom(settingsProp);
		return settings;
	}

	private static Properties readDefaults(ResourceMap inputs) throws IOException {
		Properties props = new Properties();
		Resource defaultsResource = inputs.remove(DEFAULT_PROPERTIES);
		if (defaultsResource != null) {
			if (defaultsResource.getType() != ResourceType.File)
				throw new IllegalArgumentException(
					String.format("Default properties resource %s must be a file; found resource type %s",
						DEFAULT_PROPERTIES, defaultsResource.getType()));
			try (Reader reader = new InputStreamReader(defaultsResource.getContent(),
				defaultsResource.getTextEncoding())) {
				props.load(reader);
			}
		}
		return props;
	}

	private static Map<String, Object> flattenParameters(Map<String, List<Object>> parameters) {
		Map<String, Object> flattened = new HashMap<>(parameters.size());
		for (Entry<String, List<Object>> entry : parameters.entrySet()) {
			List<Object> list = entry.getValue();
			if (list != null && !list.isEmpty()) {
				if (list.size() == 1) {
					flattened.put(entry.getKey(), list.get(0));
				} else {
					flattened.put(entry.getKey(), list);
				}
			}
		}
		return flattened;
	}
}
