package org.postgresql.java.backend;

import org.apache.commons.lang.StringUtils;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
import org.postgresql.java.model.annotations.Hidden;
import org.postgresql.java.model.annotations.Replace;
import org.postgresql.java.model.annotations.Stringable;
import org.postgresql.java.model.annotations.Template;
import org.postgresql.java.model.types.userdefined.UserDefinedType;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


@Component
public class TemplaterEngine {

    private final static Logger logger = LoggerFactory.getLogger(TemplaterEngine.class);


    private static final VelocityEngine ve = new VelocityEngine();

    @PostConstruct
    public void init() {
        ve.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
        ve.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName());
    }

    protected List simpleTypes = Arrays.asList(
            Long.class,
            Boolean.class,
            Integer.class,
            Character.class,
            Double.class,
            Float.class,
            Byte.class,
            String.class
    );

    public String template(Object source) {
        try {

            logger.trace("source = "+source.getClass().getName());

            Map<String, Object> data = convert(source);

            boolean isFile = source.getClass().getAnnotation(Template.class).isFile();
            String value = source.getClass().getAnnotation(Template.class).value();

            if (isFile) {
                return template(value, data);
            } else {
                return evaluate(value, data);
            }

        } catch (Throwable t) {
            // todo: log error
            return "Error happens during templating: " + t.getLocalizedMessage();
        }
    }


    public Map<String, Object> convert(Object source) {
        Map<String, Object> body = new HashMap<>();

        for (Field field: source.getClass().getFields()) {
            Object o = null;
            try {
                o = field.get(source);
            } catch (IllegalAccessException e) {
                // todo: log error
            }
            if (o != null) {
                Object r = replace(source, o, field);
                if (r != null) {
                    body.put(field.getName(), r);
                }
            }
        }

        return body;
    }

    public Object replace(Object source, Object o, Field field) {

        logger.trace("source = "+source.getClass().getName() + "; o = "+o.getClass().getName() + "; field = "+field);

        // check class annotations


        if (o.getClass().isAnnotationPresent(Template.class) && except(source,o)) {

            return template(o);

        }


        if (o.getClass().isAnnotationPresent(Stringable.class)) {


            return o.toString();


        }

        // check field annotations
        if (field.isAnnotationPresent(Hidden.class)) {
            return null;
        }

        if (field.isAnnotationPresent(Stringable.class)) {
            return o.toString();
        }

        if (field.isAnnotationPresent(Replace.class)) {
            return field.getAnnotation(Replace.class).value();
        }

        // check type of object
        if (o instanceof Collection) {
            return ((Collection<Object>) o).stream().map(e -> replace(source, e, field)).collect(Collectors.toList());
        }

        if (simpleTypes.contains(o.getClass()) || o.getClass().isPrimitive() || o.getClass().isEnum()) {
            return o;
        }

        return convert(o);
    }

    /**
     * Use file based template
     * @param fileName path to template in resources
     * @param data
     * @return
     */
    public String template(String fileName, Map<String, Object> data) {

        org.apache.velocity.Template t = ve.getTemplate(fileName);

        StringWriter writer = new StringWriter();
        VelocityContext vc = new VelocityContext(data);
//        vc.put("_nl", "\n");//иногда неоткуда взять перенос строки...
        t.merge(vc, writer);

        return StringUtils.trim(writer.toString());
    }

    /**
     * Use string as a template
     * @param template string, contains template as is
     * @param data
     * @return
     */
    public String evaluate(String template, Map<String, Object> data) {
        StringWriter writer = new StringWriter();
        VelocityContext vc = new VelocityContext(data);

        ve.evaluate(vc, writer, "stub template name", template);
        return StringUtils.trim(writer.toString());
    }


    private boolean except(Object source, Object o){

        if(o instanceof  UserDefinedType){
            return false;
        }


        return true;

    }
}