/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you 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.apache.synapse.mediators.elementary;

import org.apache.axiom.om.OMAttribute;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMNode;
import org.apache.axiom.om.OMText;
import org.apache.axiom.om.util.AXIOMUtil;
import org.apache.axiom.soap.SOAPBody;
import org.apache.axiom.soap.SOAPEnvelope;
import org.apache.axis2.AxisFault;
import org.apache.synapse.MessageContext;
import org.apache.synapse.SynapseException;
import org.apache.synapse.SynapseLog;
import org.apache.synapse.util.xpath.SynapseXPath;
import org.jaxen.JaxenException;

import javax.xml.stream.XMLStreamException;
import java.util.ArrayList;

/**
 * Inset an Axiom element to the current message. The target to insert the OMElement can be
 * 1. A property
 * 2. SOAP Body child element
 * 3. SOAP envelope
 * 4. A XPath expression to get the correct node
 * <p/>
 * In case the target is an SOAP Envelope, the current SOAP envelope will be replaced by the
 * OMNode. So the OMNode must me a SOAPEnvelope.
 * <p/>
 * In case of Body the first child of the Body will be replaced by the new Node or a sibling
 * will be added to it depending on the replace property.
 * <p/>
 * In case of Expression a SOAP Element will be chosen based on the XPath. If replace is true
 * that element will be replaced, otherwise a sibling will be added to that element.
 * <p/>
 * Property case is simple. The OMNode will be stored in the given property
 */

public class Target {

    private SynapseXPath xpath = null;

    private String property = null;

    private int targetType = EnrichMediator.CUSTOM;

    public static final String ACTION_REPLACE = "replace";

    public static final String ACTION_ADD_CHILD = "child";

    public static final String ACTION_ADD_SIBLING = "sibling";

    private String action = ACTION_REPLACE;

    public void insert(MessageContext synContext,
                       ArrayList<OMNode> sourceNodeList, SynapseLog synLog) throws JaxenException {

        if (targetType == EnrichMediator.CUSTOM) {
            assert xpath != null : "Xpath cannot be null for CUSTOM";

            if (sourceNodeList.isEmpty()) {
                synLog.error("Cannot Enrich message from an empty source.");
                return;
            }

            Object targetObj = xpath.selectSingleNode(synContext);

            if (targetObj instanceof OMElement) {
                OMElement targetElem = (OMElement) targetObj;
                insertElement(sourceNodeList, targetElem, synLog);
            } else if (targetObj instanceof OMText) {
                OMText targetText = (OMText) targetObj;
                if (sourceNodeList.get(0) instanceof OMText) {
                    if (targetText.getParent() != null) {
                        Object parent = targetText.getParent();
                        if (parent instanceof OMElement) {
                            ((OMElement)parent).setText(((OMText) sourceNodeList.get(0)).getText());
                        }
                    }
                } else if (sourceNodeList.get(0) instanceof OMElement) {
                    Object targetParent = targetText.getParent();
                    if (targetParent instanceof OMElement) {
                        targetText.detach();
                        ((OMElement)targetParent).addChild(sourceNodeList.get(0));
                    }
                }
            } else if (targetObj instanceof OMAttribute){
                OMAttribute attribute = (OMAttribute)targetObj;
                if (sourceNodeList.get(0) instanceof OMText){
                    attribute.setAttributeValue(((OMText)sourceNodeList.get(0)).getText());
                }
            } else {
                synLog.error("Invalid Target object to enrich.");
                throw new SynapseException("Invalid Target object to enrich.");
            }
        } else if (targetType == EnrichMediator.BODY) {
            SOAPEnvelope env = synContext.getEnvelope();
            SOAPBody body = env.getBody();

            OMElement e = body.getFirstElement();

            if (e != null) {
                insertElement(sourceNodeList, e, synLog);
            } else {
                // if the body is empty just add as a child
                for (OMNode elem : sourceNodeList) {
                    if (elem instanceof OMElement) {
                        body.addChild(elem);
                    } else {
                        synLog.error("Invalid Object type to be inserted into message body");
                    }
                }
            }
        } else if (targetType == EnrichMediator.ENVELOPE) {
            OMNode node = sourceNodeList.get(0);
            if (node instanceof SOAPEnvelope) {
                try {
                    synContext.setEnvelope((SOAPEnvelope) node);
                } catch (AxisFault axisFault) {
                    synLog.error("Failed to set the SOAP Envelope");
                    throw new SynapseException("Failed to set the SOAP Envelope");
                }
            } else {
                synLog.error("SOAPEnvelope is expected");
                throw new SynapseException("A SOAPEnvelope is expected");
            }
        } else if (targetType == EnrichMediator.PROPERTY) {
            assert property != null : "Property cannot be null for PROPERTY type";
            Object propertyObj = synContext.getProperty(property);
            if (propertyObj != null && ACTION_ADD_CHILD.equals(action)) {
                OMElement documentElement;
                if (propertyObj instanceof OMElement) {
                    documentElement = (OMElement) propertyObj;
                } else {
                    try {
                        documentElement = AXIOMUtil.stringToOM(propertyObj.toString());
                    } catch (XMLStreamException e) {
                        throw new SynapseException("Error while converting property value into" +
                                " an OMElement");
                    }
                }
                insertElement(sourceNodeList, documentElement, synLog);
                synContext.setProperty(property, documentElement);
            } else {
                synContext.setProperty(property, sourceNodeList);
            }
        }
    }

    private void insertElement(ArrayList<OMNode> sourceNodeList, OMElement e, SynapseLog synLog) {
        if (action.equals(ACTION_REPLACE)) {
            boolean isInserted = false;
            for (OMNode elem : sourceNodeList) {
                if (elem instanceof OMElement) {
                    e.insertSiblingAfter(elem);
                    isInserted = true;
                } else if (elem instanceof OMText) {
                    e.setText(((OMText) elem).getText());
                } else {
                    synLog.error("Invalid Source object to be inserted.");
                }
            }
            if (isInserted) {
                e.detach();
            }
        } else if (action.equals(ACTION_ADD_CHILD)) {
            for (OMNode elem : sourceNodeList) {
                if (elem instanceof OMElement) {
                    e.addChild(elem);
                }
            }
        } else if (action.equals(ACTION_ADD_SIBLING)) {
            for (OMNode elem : sourceNodeList) {
                if (elem instanceof OMElement) {
                    e.insertSiblingAfter(elem);
                }
            }
        }
    }

    public SynapseXPath getXpath() {
        return xpath;
    }

    public String getProperty() {
        return property;
    }

    public int getTargetType() {
        return targetType;
    }

    public void setXpath(SynapseXPath xpath) {
        this.xpath = xpath;
    }

    public void setProperty(String property) {
        this.property = property;
    }

    public void setTargetType(int targetType) {
        this.targetType = targetType;
    }

    public String getAction() {
        return action;
    }

    public void setAction(String action) {
        this.action = action;
    }
}


