package com.slyak.spring.jpa;

import freemarker.cache.StringTemplateLoader;
import freemarker.template.Configuration;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.util.ClassUtils;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.metamodel.EntityType;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.StringWriter;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * TODO use exist configuration.
 * <p/>
 *
 * @author <a href="mailto:[email protected]">stormning</a>
 * @version V1.0, 2015/8/10.
 */
public class FreemarkerSqlTemplates implements ResourceLoaderAware, InitializingBean {

    private static Configuration cfg = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);

    private static StringTemplateLoader sqlTemplateLoader = new StringTemplateLoader();

    static {
        cfg.setTemplateLoader(sqlTemplateLoader);
    }

    private final Log logger = LogFactory.getLog(getClass());

    private String encoding = "UTF-8";

    @PersistenceContext
    private EntityManager em;

    private Map<String, Long> lastModifiedCache = new ConcurrentHashMap<>();

    private Map<String, List<Resource>> sqlResources = new ConcurrentHashMap<>();

    private String templateLocation = "classpath:/sqls";

    private String templateBasePackage = "**";

    private ResourceLoader resourceLoader;

    private String suffix = ".xml";

    private Map<String, NamedTemplateResolver> suffixResolvers = new HashMap<>();

    {
        suffixResolvers.put(".sftl", new SftlNamedTemplateResolver());
    }

    public String process(String entityName, String methodName, Map<String, Object> model) {
        reloadIfPossible(entityName);
        try {
            StringWriter writer = new StringWriter();
            cfg.getTemplate(getTemplateKey(entityName, methodName), encoding).process(model, writer);
            return writer.toString();
        } catch (Exception e) {
            logger.error("process template error. Entity name: " + entityName + " methodName:" + methodName, e);
            return StringUtils.EMPTY;
        }
    }

    private String getTemplateKey(String entityName, String methodName) {
        return entityName + ":" + methodName;
    }

    private void reloadIfPossible(final String entityName) {
        try {
            Long lastModified = lastModifiedCache.get(entityName);
            List<Resource> resourceList = sqlResources.get(entityName);

            long newLastModified = 0;
            for (Resource resource : resourceList) {
                if (newLastModified == 0) {
                    newLastModified = resource.lastModified();
                } else {
                    //get the last modified.
                    newLastModified = newLastModified > resource.lastModified() ?
                            newLastModified : resource.lastModified();
                }
            }

            //check modified for cache.
            if (lastModified == null || newLastModified > lastModified) {
                lastModifiedCache.put(entityName, newLastModified);

                //process template.
                for (Resource resource : resourceList) {
                    Iterator<Void> iterator = suffixResolvers.get(suffix)
                            .doInTemplateResource(resource, (templateName, content) -> {
                                String key = getTemplateKey(entityName, templateName);
                                Object src = sqlTemplateLoader.findTemplateSource(key);
                                if (src != null) {
                                    logger.warn("found duplicate template key, will replace the value, key:" + key);
                                }
                                sqlTemplateLoader
                                        .putTemplate(getTemplateKey(entityName, templateName), content);
                            });
                    while (iterator.hasNext()) {
                        iterator.next();
                    }
                }
            }
        } catch (Exception e) {
            logger.error(e);
        }
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
        XmlNamedTemplateResolver xmlNamedTemplateResolver = new XmlNamedTemplateResolver(resourceLoader);
        xmlNamedTemplateResolver.setEncoding(encoding);
        this.suffixResolvers.put(".xml", xmlNamedTemplateResolver);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        Set<String> names = new HashSet<>();
        Set<EntityType<?>> entities = em.getMetamodel().getEntities();
        for (EntityType<?> entity : entities) {
            names.add(entity.getName());
        }

        String suffixPattern = "/**/*" + suffix;

        if (!names.isEmpty()) {
            String pattern;
            if (StringUtils.isNotBlank(templateBasePackage)) {
                pattern = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
                        ClassUtils.convertClassNameToResourcePath(templateBasePackage) + suffixPattern;

                loadPatternResource(names, pattern);
            }
            if (StringUtils.isNotBlank(templateLocation)) {
                pattern = templateLocation.contains(suffix) ? templateLocation : templateLocation + suffixPattern;
                try {
                    loadPatternResource(names, pattern);
                } catch (FileNotFoundException e) {
                    if ("classpath:/sqls".equals(templateLocation)) {
                        //warn: default value
                        logger.warn("templateLocation[" + templateLocation + "] not exist!");
                        logger.warn(e.getMessage());
                    } else {
                        //throw: custom value.
                        throw e;
                    }
                }
            }
        }
    }

    private void loadPatternResource(Set<String> names, String pattern) throws IOException {
        PathMatchingResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(
                resourceLoader);
        Resource[] resources = resourcePatternResolver.getResources(pattern);
        for (Resource resource : resources) {
            String resourceName = resource.getFilename().replace(suffix, "");
            if (names.contains(resourceName)) {
                //allow multi resource.
                List<Resource> resourceList;
                if (sqlResources.containsKey(resourceName)) {
                    resourceList = sqlResources.get(resourceName);
                } else {
                    resourceList = new LinkedList<>();
                    sqlResources.put(resourceName, resourceList);
                }
                resourceList.add(resource);
            }
        }
    }

    public void setTemplateLocation(String templateLocation) {
        this.templateLocation = templateLocation;
    }

    public void setTemplateBasePackage(String templateBasePackage) {
        this.templateBasePackage = templateBasePackage;
    }

    public void setEncoding(String encoding) {
        this.encoding = encoding;
    }

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