package dasniko.ozark.react;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import javax.annotation.Priority;
import javax.inject.Inject;
import javax.mvc.Models;
import javax.mvc.engine.Priorities;
import javax.mvc.engine.ViewEngine;
import javax.mvc.engine.ViewEngineContext;
import javax.mvc.engine.ViewEngineException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author Niko Köbler, http://www.n-k.de, @dasniko
 */
@Priority(Priorities.FRAMEWORK)
public class ReactViewEngine implements ViewEngine {

    private static final String viewPrefix = "react:";

    @Inject
    React react;
    @Inject
    ServletContext servletContext;

    private ObjectMapper mapper = new ObjectMapper();

    @Override
    public boolean supports(String view) {
        return view.startsWith(viewPrefix);
    }

    @Override
    public void processView(ViewEngineContext context) throws ViewEngineException {
        // parse view and extract the actual template and the react.js function to call
        String view = context.getView();
        String[] viewParts = view.substring(viewPrefix.length()).split("\\?");
        if (viewParts.length < 2) {
            throw new ViewEngineException("You have to specify at least a view and a function (e.g. react:view.jsp?function=renderOnServer)!");
        }

        String template = viewParts[0];

        Map<String, String> params = parseQueryString(viewParts[1]);
        String function = params.get("function");

        String dataKey = params.getOrDefault("data", "data");
        String contentKey = params.getOrDefault("content", "content");

        // get "data" from model
        Models models = context.getModels();
        Object data = models.get(dataKey);

        // call given js function on data
        String content = react.render(function, data);

        // and put results as string in model
        models.put(contentKey, content);
        try {
            models.put(dataKey, mapper.writeValueAsString(data));
        } catch (JsonProcessingException e) {
            throw new ViewEngineException(e);
        }

        try {
            processRequest(context, models, template);
        } catch (ServletException | IOException e) {
            throw new ViewEngineException(e);
        }
    }

    private Map<String, String> parseQueryString(final String query) {
        return Arrays.asList(query.split("&")).stream().map(p -> p.split("=")).collect(Collectors.toMap(s -> s[0], s -> s[1]));
    }

    private void processRequest(final ViewEngineContext context, final Models models, final String view) throws ServletException, IOException {
        RequestDispatcher requestDispatcher = null;
        HttpServletRequest request = context.getRequest();

        for (String name : models) {
            request.setAttribute(name, models.get(name));
        }

        final String viewExtension = getViewExtension(view);
        for (Map.Entry<String, ? extends ServletRegistration> e : servletContext.getServletRegistrations().entrySet()) {
            final Collection<String> mappings = e.getValue().getMappings();
            if (mappings.contains(viewExtension)) {
                requestDispatcher = servletContext.getNamedDispatcher(e.getKey());
                request = new HttpServletRequestWrapper(context.getRequest()) {
                    @Override
                    public String getRequestURI() {
                        return resolveView(context, view);
                    }

                    @Override
                    public String getServletPath() {
                        return getRequestURI();
                    }

                    @Override
                    public String getPathInfo() {
                        return null;
                    }

                    @Override
                    public StringBuffer getRequestURL() {
                        return new StringBuffer(getRequestURI());
                    }
                };
            }
        }

        if (requestDispatcher == null) {
            requestDispatcher = servletContext.getRequestDispatcher(resolveView(context, view));
        }

        requestDispatcher.forward(request, context.getResponse());
    }

    private String getViewExtension(final String view) {
        return "*" + view.substring(view.lastIndexOf("."));
    }

    private String resolveView(final ViewEngineContext context, final String view) {
        if (!view.startsWith("/")) {
            String viewFolder = (String) context.getConfiguration().getProperty(VIEW_FOLDER);
            viewFolder = viewFolder == null ? DEFAULT_VIEW_FOLDER : viewFolder;
            viewFolder += !viewFolder.endsWith("/") ? "/" : "";
            return viewFolder + view;
        }
        return view;
    }
}