/*
 * Copyright 2014 astamuse company,Ltd.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package com.astamuse.asta4d.web.util.message;

import static com.astamuse.asta4d.render.SpecialRenderer.Clear;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.astamuse.asta4d.Context;
import com.astamuse.asta4d.data.ContextBindData;
import com.astamuse.asta4d.render.ElementNotFoundHandler;
import com.astamuse.asta4d.render.ElementSetter;
import com.astamuse.asta4d.render.Renderable;
import com.astamuse.asta4d.render.Renderer;
import com.astamuse.asta4d.template.ClasspathTemplateResolver;
import com.astamuse.asta4d.template.Template;
import com.astamuse.asta4d.template.TemplateException;
import com.astamuse.asta4d.template.TemplateNotFoundException;
import com.astamuse.asta4d.template.TemplateResolver;
import com.astamuse.asta4d.util.SelectorUtil;
import com.astamuse.asta4d.web.WebApplicationConfiguration;
import com.astamuse.asta4d.web.WebApplicationContext;
import com.astamuse.asta4d.web.dispatch.RedirectInterceptor;
import com.astamuse.asta4d.web.dispatch.RedirectUtil;

public class DefaultMessageRenderingHelper implements MessageRenderingHelper {

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

    private final static String FLASH_MSG_LIST_KEY = "FLASH_MSG_LIST_KEY#" + DefaultMessageRenderingHelper.class;

    protected final static class MessageHolder {
        MessageRenderingSelector selector;
        MessageRenderingSelector alternativeSelector;
        String message;

        public MessageHolder(MessageRenderingSelector selector, MessageRenderingSelector alternativeSelector, String message) {
            super();
            this.selector = selector;
            this.alternativeSelector = alternativeSelector;
            this.message = message;
        }

    }

    public final static class MessageRenderingSelector {

        private String duplicator;

        private String valueTarget;

        public MessageRenderingSelector() {

        }

        public MessageRenderingSelector(String duplicator, String valueTarget) {
            super();
            this.duplicator = duplicator;
            this.valueTarget = valueTarget;
        }

        public String getDuplicator() {
            return duplicator;
        }

        public void setDuplicator(String duplicator) {
            this.duplicator = duplicator;
        }

        public String getValueTarget() {
            return valueTarget;
        }

        public void setValueTarget(String valueTarget) {
            this.valueTarget = valueTarget;
        }

        @Override
        public int hashCode() {
            return ((duplicator == null) ? 0 : duplicator.hashCode()) ^ ((valueTarget == null) ? 0 : valueTarget.hashCode());
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            MessageRenderingSelector other = (MessageRenderingSelector) obj;
            if (duplicator == null) {
                if (other.duplicator != null)
                    return false;
            } else if (!duplicator.equals(other.duplicator))
                return false;
            if (valueTarget == null) {
                if (other.valueTarget != null)
                    return false;
            } else if (!valueTarget.equals(other.valueTarget))
                return false;
            return true;
        }

    }

    private TemplateResolver fallbackMessageContainerResolver = new ClasspathTemplateResolver();

    private String messageGlobalContainerParentSelector = "body";

    private String messageGlobalContainerSelector = "#global-msg-container";

    private String messageGlobalContainerSnippetFilePath = "/com/astamuse/asta4d/web/util/message/DefaultMessageContainerSnippet.html";

    private MessageRenderingSelector messageGlobalInfoSelector = new MessageRenderingSelector("#info-msg li", ":root");

    private MessageRenderingSelector messageGlobalWarnSelector = new MessageRenderingSelector("#warn-msg li", ":root");

    private MessageRenderingSelector messageGlobalErrSelector = new MessageRenderingSelector("#err-msg li", ":root");

    private String messageDuplicatorIndicatorAttrName = "afd:message-duplicator";

    private Elements cachedSnippet = null;

    private ContextBindData<List<MessageHolder>> messageList = new ContextBindData<List<MessageHolder>>(true) {
        @Override
        protected List<MessageHolder> buildData() {
            return new LinkedList<>();
        }
    };

    public DefaultMessageRenderingHelper() {
        super();
    }

    public static DefaultMessageRenderingHelper getConfiguredInstance() {
        return (DefaultMessageRenderingHelper) WebApplicationConfiguration.getWebApplicationConfiguration().getMessageRenderingHelper();
    }

    public String getMessageGlobalContainerParentSelector() {
        return messageGlobalContainerParentSelector;
    }

    public void setMessageGlobalContainerParentSelector(String messageGlobalContainerParentSelector) {
        this.messageGlobalContainerParentSelector = messageGlobalContainerParentSelector;
    }

    public String getMessageGlobalContainerSelector() {
        return messageGlobalContainerSelector;
    }

    public void setMessageGlobalContainerSelector(String messageGlobalContainerSelector) {
        this.messageGlobalContainerSelector = messageGlobalContainerSelector;
    }

    public String getMessageGlobalContainerSnippetFilePath() {
        return messageGlobalContainerSnippetFilePath;
    }

    public void setMessageGlobalContainerSnippetFilePath(String messageGlobalContainerSnippetFilePath) {
        this.messageGlobalContainerSnippetFilePath = messageGlobalContainerSnippetFilePath;
    }

    public MessageRenderingSelector getMessageGlobalInfoSelector() {
        return messageGlobalInfoSelector;
    }

    public void setMessageGlobalInfoSelector(MessageRenderingSelector messageGlobalInfoSelector) {
        this.messageGlobalInfoSelector = messageGlobalInfoSelector;
    }

    public MessageRenderingSelector getMessageGlobalWarnSelector() {
        return messageGlobalWarnSelector;
    }

    public void setMessageGlobalWarnSelector(MessageRenderingSelector messageGlobalWarnSelector) {
        this.messageGlobalWarnSelector = messageGlobalWarnSelector;
    }

    public MessageRenderingSelector getMessageGlobalErrSelector() {
        return messageGlobalErrSelector;
    }

    public void setMessageGlobalErrSelector(MessageRenderingSelector messageGlobalErrSelector) {
        this.messageGlobalErrSelector = messageGlobalErrSelector;
    }

    public String getMessageDuplicatorIndicatorAttrName() {
        return messageDuplicatorIndicatorAttrName;
    }

    public void setMessageDuplicatorIndicatorAttrName(String messageDuplicatorIndicatorAttrName) {
        this.messageDuplicatorIndicatorAttrName = messageDuplicatorIndicatorAttrName;
    }

    public Renderer createMessageRenderer() {
        Renderer renderer = Renderer.create();

        Renderer message = renderMesssages();
        if (message != null) {
            renderer.add(message);
        }
        renderer.add(postMessageRendering());
        return renderer;
    }

    protected Renderer postMessageRendering() {
        // remove all the remaining message duplicators which may not be referenced in message outputting, which is why they are remaining
        Renderer render = Renderer.create();
        render.disableMissingSelectorWarning();
        render.add(SelectorUtil.attr(messageDuplicatorIndicatorAttrName), Clear);
        render.enableMissingSelectorWarning();
        return render;
    }

    /**
     * 
     * @return Pair.left: whether the alternative message container is necessary <br>
     *         Pair.right: the actual renderer
     */
    protected Renderer renderMesssages() {
        List<MessageHolder> allMsgList = new LinkedList<>();

        allMsgList.addAll(messageList.get());

        if (allMsgList.isEmpty()) {
            return null;
        }
        Renderer renderer = Renderer.create();

        final Map<MessageRenderingSelector, List<MessageHolder>> msgMap = new HashMap<>();
        final Map<MessageRenderingSelector, List<String>> alternativeMsgMap = new HashMap<>();
        List<MessageHolder> tmpList;
        for (MessageHolder mh : allMsgList) {
            tmpList = msgMap.get(mh.selector);
            if (tmpList == null) {
                tmpList = new LinkedList<>();
                msgMap.put(mh.selector, tmpList);
            }
            tmpList.add(mh);
        }

        renderer.disableMissingSelectorWarning();

        for (final Entry<MessageRenderingSelector, List<MessageHolder>> item : msgMap.entrySet()) {
            if (item.getKey() == null) {
                List<String> list;
                for (MessageHolder mh : item.getValue()) {
                    list = alternativeMsgMap.get(mh.alternativeSelector);
                    if (list == null) {
                        list = new LinkedList<>();
                        alternativeMsgMap.put(mh.alternativeSelector, list);
                    }
                    list.add(mh.message);
                }
            } else {
                final MessageRenderingSelector selector = item.getKey();
                renderer.add(selector.duplicator, item.getValue(), (MessageHolder obj) -> {
                    Renderer render = Renderer.create(selector.valueTarget, obj.message);
                    render.add(":root", messageDuplicatorIndicatorAttrName, Clear);
                    return render;
                });
                renderer.add(new ElementNotFoundHandler(selector.duplicator) {
                    @Override
                    public Renderer alternativeRenderer() {
                        List<String> list;
                        for (MessageHolder mh : item.getValue()) {
                            list = alternativeMsgMap.get(mh.alternativeSelector);
                            if (list == null) {
                                list = new LinkedList<>();
                                alternativeMsgMap.put(mh.alternativeSelector, list);
                            }
                            list.add(mh.message);
                        }
                        return Renderer.create();
                    }
                });
            }
        } // end for loop

        renderer.enableMissingSelectorWarning();

        renderer.add(messageGlobalContainerParentSelector, new Renderable() {
            @Override
            public Renderer render() {
                Renderer renderer = Renderer.create();
                if (!alternativeMsgMap.isEmpty()) {
                    renderer.add(new ElementNotFoundHandler(messageGlobalContainerSelector) {
                        @Override
                        public Renderer alternativeRenderer() {
                            // add global message container if not exists
                            return Renderer.create(":root", new ElementSetter() {
                                @Override
                                public void set(Element elem) {
                                    List<Element> elems = new ArrayList<>(retrieveCachedContainerSnippet());
                                    Collections.reverse(elems);
                                    for (Element child : elems) {
                                        elem.prependChild(child.clone());
                                    }
                                }
                            });
                        }// alternativeRenderer
                    });// ElementNotFoundHandler

                    renderer.add(messageGlobalContainerSelector, new Renderable() {
                        @Override
                        public Renderer render() {
                            Renderer alternativeMsgRenderer = Renderer.create();
                            for (final Entry<MessageRenderingSelector, List<String>> item : alternativeMsgMap.entrySet()) {
                                final MessageRenderingSelector selector = item.getKey();
                                alternativeMsgRenderer.add(selector.duplicator, item.getValue(), (String msg) -> {
                                    Renderer render = Renderer.create(selector.valueTarget, msg);
                                    render.add(":root", messageDuplicatorIndicatorAttrName, Clear);
                                    return render;
                                });
                            }
                            return alternativeMsgRenderer;
                        }
                    });// messageGlobalContainerSelector
                }
                return renderer;
            }
        });

        return renderer;

    }

    protected Elements retrieveCachedContainerSnippet() {
        if (WebApplicationConfiguration.getWebApplicationConfiguration().isCacheEnable()) {
            if (cachedSnippet == null) {
                cachedSnippet = retrieveContainerSnippet();
            }
            return cachedSnippet;
        } else {
            return retrieveContainerSnippet();
        }

    }

    protected Elements retrieveContainerSnippet() {
        WebApplicationConfiguration conf = WebApplicationConfiguration.getWebApplicationConfiguration();
        Template template;
        try {
            // at first, we treat the configured snippet file as a template file
            try {
                template = conf.getTemplateResolver().findTemplate(messageGlobalContainerSnippetFilePath);
            } catch (TemplateNotFoundException e) {
                // then treat it as classpath resource
                template = fallbackMessageContainerResolver.findTemplate(messageGlobalContainerSnippetFilePath);
            }
            return template.getDocumentClone().body().children();
        } catch (TemplateException | TemplateNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    public void outputMessage(final MessageRenderingSelector selector, final MessageRenderingSelector alternativeSelector,
            final String msg) {
        messageList.get().add(new MessageHolder(selector, alternativeSelector, msg));
        RedirectUtil.registerRedirectInterceptor(this.getClass().getName() + "#outputMessage", new RedirectInterceptor() {
            @Override
            public void beforeRedirect() {
                List<MessageHolder> list = new ArrayList<>(messageList.get());
                if (!list.isEmpty()) {
                    RedirectUtil.addFlashScopeData(FLASH_MSG_LIST_KEY, list);
                }
            }

            @Override
            public void afterRedirectDataRestore() {
                List<MessageHolder> flashedList = Context.getCurrentThreadContext().getData(WebApplicationContext.SCOPE_FLASH,
                        FLASH_MSG_LIST_KEY);
                if (flashedList != null) {
                    messageList.get().addAll(flashedList);
                }
            }
        });
    }

    private void outputMessage(String duplicator, String msgTargetSelector, MessageRenderingSelector alternativeSelector, String msg) {
        MessageRenderingSelector selector = null;
        if (duplicator == null) {
            // do nothing
        } else {
            if (msgTargetSelector == null) {
                msgTargetSelector = ":root";
            }
            selector = new MessageRenderingSelector(duplicator, msgTargetSelector);
        }
        outputMessage(selector, alternativeSelector, msg);

    }

    public void info(String msg) {
        info(null, null, msg);
    }

    public void info(String selector, String msg) {
        info(selector, null, msg);
    }

    public void info(String duplicator, String msgTargetSelector, String msg) {
        outputMessage(duplicator, msgTargetSelector, messageGlobalInfoSelector, msg);
    }

    public void warn(String msg) {
        warn(null, null, msg);
    }

    public void warn(String selector, String msg) {
        warn(selector, null, msg);
    }

    public void warn(String duplicator, String msgTargetSelector, String msg) {
        outputMessage(duplicator, msgTargetSelector, messageGlobalWarnSelector, msg);
    }

    public void err(String msg) {
        err(null, null, msg);
    }

    public void err(String selector, String msg) {
        err(selector, null, msg);
    }

    public void err(String duplicator, String msgTargetSelector, String msg) {
        outputMessage(duplicator, msgTargetSelector, messageGlobalErrSelector, msg);
    }

}
