/*
 * Copyright 2013 Red Hat, Inc. and/or its affiliates.
 *
 * 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 org.kie.spring.namespace;


import org.kie.spring.factorybeans.EnvironmentDefFactoryBean;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;

import java.util.ArrayList;
import java.util.List;

import static org.kie.spring.namespace.DefinitionParserHelper.emptyAttributeCheck;

public class EnvironmentDefinitionParser extends AbstractBeanDefinitionParser {

    public static final String ATTRIBUTE_ID = "id";
    public static final String ATTRIBUTE_REF = "ref";
    public static final String ATTRIBUTE_SCOPE = "scope";
    public static final String ATTRIBUTE_STRATEGY_ACCEPTOR_REF = "strategy-acceptor-ref";
    public static final String ATTRIBUTE_ENV_REF = "env-ref";

    public static final String ELEMENT_ENTITY_MANAGER_FACTORY = "entity-manager-factory";
    public static final String ELEMENT_TRANSACTION_MANAGER = "transaction-manager";
    public static final String ELEMENT_GLOBALS = "globals";
    public static final String ELEMENT_DATE_FORMATS = "date-formats";
    public static final String ELEMENT_CALENDARS = "calendars";
    public static final String ELEMENT_OBJECT_MARSHALLING_STRATEGIES = "object-marshalling-strategies";
    public static final String ELEMENT_SERIALIZABLE_PLACEHOLDER_RESOLVER_STRATEGY = "serializable-placeholder-resolver-strategy";
    public static final String ELEMENT_IDENTITY_PLACEHOLDER_RESOLVER_STRATEGY = "identity-placeholder-resolver-strategy";
    public static final String ELEMENT_PROCESS_INSTANCE_RESOLVER_STRATEGY = "process-instance-resolver-strategy";
    public static final String ELEMENT_JPA_PLACEHOLDER_RESOLVER_STRATEGY = "jpa-placeholder-resolver-strategy";
    public static final String ELEMENT_SCOPED_ENTITY_MANAGER = "scoped-entity-manager";
    public static final String ELEMENT_CUSTOM_MARSHALLING_STRATEGY = "custom-marshalling-strategy";
    public static final String ELEMENT_BEAN = "bean";
    public static final String ELEMENT_STRATEGY_ACCEPTOR = "strategy-acceptor";

    public static final String PROPERTY_ENTITY_MANAGER_FACTORY = "entityManagerFactory";
    public static final String PROPERTY_TRANSACTION_MANAGER = "transactionManager";
    public static final String PROPERTY_GLOBALS = "globals";
    public static final String PROPERTY_CALENDARS = "calendars";
    public static final String PROPERTY_DATE_FORMATS = "dateFormats";
    public static final String PROPERTY_JPA_PLACE_HOLDER_RESOLVER_STRATEGY_ENV = "jpaPlaceHolderResolverStrategyEnv";
    public static final String PROPERTY_APP_SCOPED_ENTITY_MANAGER = "appScopedEntityManager";
    public static final String PROPERTY_CMD_SCOPED_ENTITY_MANAGER = "cmdScopedEntityManager";
    public static final String PROPERTY_SERIALIZABLE_PLACEHOLDER_RESOLVER_STRATEGY_ACCEPTOR = "serializablePlaceholderResolverStrategyAcceptor";
    public static final String PROPERTY_IDENTITY_PLACEHOLDER_RESOLVER_STRATEGY_ACCEPTOR = "identityPlaceholderResolverStrategyAcceptor";
    public static final String PROPERTY_CUSTOM_MARSHALLING_STRATEGY = "customMarshallingStrategies";
    public static final String PROPERTY_OBJECT_MARSHALLING_ORDER = "objectMarshallersOrder";

    public static final String PROPERTY_NAME = "name";

    protected AbstractBeanDefinition parseInternal(Element element,
                                                   ParserContext parserContext) {

        BeanDefinitionBuilder factory = BeanDefinitionBuilder.rootBeanDefinition(EnvironmentDefFactoryBean.class);
        String id = element.getAttribute(ATTRIBUTE_ID);
        emptyAttributeCheck(element.getLocalName(), ATTRIBUTE_ID, id);
        factory.addPropertyValue(PROPERTY_NAME, id);

        checkRefOrNestedBean(element, ELEMENT_ENTITY_MANAGER_FACTORY, PROPERTY_ENTITY_MANAGER_FACTORY, ATTRIBUTE_REF, factory, parserContext);
        checkRefOrNestedBean(element, ELEMENT_TRANSACTION_MANAGER, PROPERTY_TRANSACTION_MANAGER, ATTRIBUTE_REF, factory, parserContext);
        checkRefOrNestedBean(element, ELEMENT_GLOBALS, PROPERTY_GLOBALS, ATTRIBUTE_REF, factory, parserContext);
        checkRefOrNestedBean(element, ELEMENT_DATE_FORMATS, PROPERTY_DATE_FORMATS, ATTRIBUTE_REF, factory, parserContext);
        checkRefOrNestedBean(element, ELEMENT_CALENDARS, PROPERTY_CALENDARS, ATTRIBUTE_REF, factory, parserContext);

        Element objectMarshallingStrategiesElement = DomUtils.getChildElementByTagName(element, ELEMENT_OBJECT_MARSHALLING_STRATEGIES);
        if (objectMarshallingStrategiesElement != null) {
            List<String> marshallerOrderList = new ArrayList<String>();
            List<Element> children = DomUtils.getChildElements(objectMarshallingStrategiesElement);
            ManagedList managedCustomList = new ManagedList();

            for (Element child : children) {
                String localName = child.getLocalName();
                marshallerOrderList.add(localName);

                if (ELEMENT_SERIALIZABLE_PLACEHOLDER_RESOLVER_STRATEGY.equalsIgnoreCase(localName)) {
                    parsePlaceholderResolverStrategyElement(parserContext, factory, child, PROPERTY_SERIALIZABLE_PLACEHOLDER_RESOLVER_STRATEGY_ACCEPTOR);
                } else if (ELEMENT_IDENTITY_PLACEHOLDER_RESOLVER_STRATEGY.equalsIgnoreCase(localName)) {
                    parsePlaceholderResolverStrategyElement(parserContext, factory, child, PROPERTY_IDENTITY_PLACEHOLDER_RESOLVER_STRATEGY_ACCEPTOR);
                } else if (ELEMENT_PROCESS_INSTANCE_RESOLVER_STRATEGY.equalsIgnoreCase(localName)) {
                    //do nothing, the bean will be created in the EnvDefBeanFactory
                } else if (ELEMENT_JPA_PLACEHOLDER_RESOLVER_STRATEGY.equalsIgnoreCase(localName)) {
                    String envRef = child.getAttribute(ATTRIBUTE_ENV_REF);
                    if (StringUtils.hasText(envRef)) {
                        factory.addPropertyValue(PROPERTY_JPA_PLACE_HOLDER_RESOLVER_STRATEGY_ENV, new RuntimeBeanReference(envRef));
                    }
                } else if (ELEMENT_CUSTOM_MARSHALLING_STRATEGY.equalsIgnoreCase(localName)) {
                    String ref = child.getAttribute(ATTRIBUTE_REF);
                    if (StringUtils.hasText(ref)) {
                        managedCustomList.add(new RuntimeBeanReference(ref));
                    } else {
                        Element nestedBean = DomUtils.getChildElementByTagName(child, ELEMENT_BEAN);
                        if (nestedBean != null) {
                            Object obj = parserContext.getDelegate().parsePropertySubElement(nestedBean, null, null);
                            managedCustomList.add(obj);
                        } else {
                            throw new IllegalArgumentException(ELEMENT_CUSTOM_MARSHALLING_STRATEGY + " must have either a " + ATTRIBUTE_REF + " attribute or an embedded " + ELEMENT_BEAN + " child element!");
                        }
                    }
                }
            }
            factory.addPropertyValue(PROPERTY_CUSTOM_MARSHALLING_STRATEGY, managedCustomList);
            factory.addPropertyValue(PROPERTY_OBJECT_MARSHALLING_ORDER, marshallerOrderList);
        }

        Element scopedEM = DomUtils.getChildElementByTagName(element, ELEMENT_SCOPED_ENTITY_MANAGER);
        if (scopedEM != null) {
            String ref = scopedEM.getAttribute(ATTRIBUTE_REF);
            Object runtimeBean = null;
            if (StringUtils.hasText(ref)) {
                runtimeBean = new RuntimeBeanReference(ref);
            } else {
                Element beanElement = DomUtils.getChildElementByTagName(scopedEM, ELEMENT_BEAN);
                if (beanElement != null) {
                    runtimeBean = parserContext.getDelegate().parsePropertySubElement(beanElement, null, null);
                } else {
                    throw new IllegalArgumentException(ELEMENT_SCOPED_ENTITY_MANAGER + " must have an embedded " + ELEMENT_BEAN + " element, or a '" + ATTRIBUTE_REF + "' attribute");
                }
            }
            String scope = scopedEM.getAttribute(ATTRIBUTE_SCOPE);
            if ("app".equalsIgnoreCase(scope)) {
                factory.addPropertyValue(PROPERTY_APP_SCOPED_ENTITY_MANAGER, runtimeBean);
            } else if ("cmd".equalsIgnoreCase(scope)) {
                factory.addPropertyValue(PROPERTY_CMD_SCOPED_ENTITY_MANAGER, runtimeBean);
            } else {
                throw new IllegalArgumentException(ELEMENT_SCOPED_ENTITY_MANAGER + ": '" + ATTRIBUTE_SCOPE + "' attribute must be either 'app' or 'cmd'");
            }
        }
        return factory.getBeanDefinition();
    }

    private void parsePlaceholderResolverStrategyElement(ParserContext parserContext, BeanDefinitionBuilder factory, Element actualElement,
                                                         String strategyAcceptorPropName) {
        if (actualElement != null) {
            String ref = actualElement.getAttribute(ATTRIBUTE_STRATEGY_ACCEPTOR_REF);
            if (StringUtils.hasText(ref)) {
                factory.addPropertyValue(strategyAcceptorPropName, new RuntimeBeanReference(ref));
            } else {
                Element acceptorElement = DomUtils.getChildElementByTagName(actualElement, ELEMENT_STRATEGY_ACCEPTOR);
                if (acceptorElement != null) {
                    Element beanElement = DomUtils.getChildElementByTagName(acceptorElement, ELEMENT_BEAN);
                    if (beanElement != null) {
                        Object obj = parserContext.getDelegate().parsePropertySubElement(beanElement, null, null);
                        factory.addPropertyValue(strategyAcceptorPropName, obj);
                    } else {
                        throw new IllegalArgumentException(ELEMENT_STRATEGY_ACCEPTOR + " must have an embedded " + ELEMENT_BEAN + " element!");
                    }
                } else {
                    throw new IllegalArgumentException(actualElement.getTagName() + " must have either a " + ATTRIBUTE_STRATEGY_ACCEPTOR_REF + " attribute or an embedded " + ELEMENT_STRATEGY_ACCEPTOR + " child element!");
                }
            }
        }
    }

    protected void checkRefOrNestedBean(Element environmentElement, String tagName, String property, String refAttribute, BeanDefinitionBuilder factory, ParserContext parserContext) {
        Element element = DomUtils.getChildElementByTagName(environmentElement, tagName);
        if (element != null) {
            String ref = element.getAttribute(refAttribute);
            if (StringUtils.hasText(ref)) {
                factory.addPropertyValue(property, new RuntimeBeanReference(ref));
            } else {
                Element nestedBean = DomUtils.getChildElementByTagName(element, ELEMENT_BEAN);
                if (nestedBean != null) {
                    Object obj = parserContext.getDelegate().parsePropertySubElement(nestedBean, null, null);
                    factory.addPropertyValue(property, obj);
                } else {
                    throw new IllegalArgumentException(tagName + " must have either a " + refAttribute + " attribute or an embedded " + ELEMENT_BEAN + " child element!");
                }
            }
        }
    }
}
