/**
 * 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.wss4j.stax.impl.processor.output;

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

import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;

import org.apache.wss4j.common.ext.WSSecurityException;
import org.apache.wss4j.stax.ext.WSSConstants;
import org.apache.wss4j.stax.ext.WSSSecurityProperties;
import org.apache.wss4j.stax.utils.WSSUtils;
import org.apache.xml.security.exceptions.XMLSecurityException;
import org.apache.xml.security.stax.ext.AbstractOutputProcessor;
import org.apache.xml.security.stax.ext.OutputProcessorChain;
import org.apache.xml.security.stax.ext.SecurePart;
import org.apache.xml.security.stax.ext.stax.XMLSecAttribute;
import org.apache.xml.security.stax.ext.stax.XMLSecEndElement;
import org.apache.xml.security.stax.ext.stax.XMLSecEvent;
import org.apache.xml.security.stax.ext.stax.XMLSecStartElement;

/**
 * Processor to build the Security Header structure
 */
public class SecurityHeaderOutputProcessor extends AbstractOutputProcessor {

    public SecurityHeaderOutputProcessor() throws XMLSecurityException {
        super();
        setPhase(WSSConstants.Phase.PREPROCESSING);
    }

    @Override
    public void processEvent(XMLSecEvent xmlSecEvent, OutputProcessorChain outputProcessorChain)
        throws XMLStreamException, XMLSecurityException {

        boolean eventHandled = false;

        switch (xmlSecEvent.getEventType()) {
            case XMLStreamConstants.START_ELEMENT:
                XMLSecStartElement xmlSecStartElement = xmlSecEvent.asStartElement();
                final String soapMessageVersion = WSSUtils.getSOAPMessageVersionNamespace(xmlSecStartElement);
                int level = xmlSecStartElement.getDocumentLevel();

                if (level == 1 && soapMessageVersion == null) {
                    throw new WSSecurityException(WSSecurityException.ErrorCode.FAILURE, "notASOAPMessage");
                } else if (level == 1) {
                    //set correct namespace on secure parts
                    List<SecurePart> encryptionParts = securityProperties.getEncryptionSecureParts();
                    if (!encryptionParts.isEmpty()) {
                        for (SecurePart securePart : encryptionParts) {
                            // Check to see if the wrong SOAP NS was used
                            changeSOAPNamespace(securePart, soapMessageVersion);

                            if (securePart.getIdToSecure() == null) {
                                outputProcessorChain.getSecurityContext().putAsMap(
                                        WSSConstants.ENCRYPTION_PARTS,
                                        securePart.getName(),
                                        securePart
                                );
                            } else {
                                outputProcessorChain.getSecurityContext().putAsMap(
                                        WSSConstants.ENCRYPTION_PARTS,
                                        securePart.getIdToSecure(),
                                        securePart
                                );
                            }
                        }
                    }
                    List<SecurePart> signatureParts = securityProperties.getSignatureSecureParts();
                    if (!signatureParts.isEmpty()) {
                        for (SecurePart securePart : signatureParts) {
                            // Check to see if the wrong SOAP NS was used
                            changeSOAPNamespace(securePart, soapMessageVersion);

                            if (securePart.getIdToSecure() == null) {
                                outputProcessorChain.getSecurityContext().putAsMap(
                                        WSSConstants.SIGNATURE_PARTS,
                                        securePart.getName(),
                                        securePart
                                );
                            } else {
                                outputProcessorChain.getSecurityContext().putAsMap(
                                        WSSConstants.SIGNATURE_PARTS,
                                        securePart.getIdToSecure(),
                                        securePart
                                );
                            }
                        }
                    }
                } else if (WSSUtils.isSecurityHeaderElement(xmlSecEvent, ((WSSSecurityProperties) getSecurityProperties()).getActor())) {
                        //remove this processor. its no longer needed.
                        outputProcessorChain.removeProcessor(this);
                } else if (level == 2
                        && WSSConstants.TAG_SOAP_BODY_LN.equals(xmlSecStartElement.getName().getLocalPart())
                        && xmlSecStartElement.getName().getNamespaceURI().equals(soapMessageVersion)) {
                    //hmm it seems we don't have a soap header in the current document
                    //so output one and add securityHeader

                    //create subchain and output soap-header and securityHeader
                    OutputProcessorChain subOutputProcessorChain =
                        outputProcessorChain.createSubChain(this, xmlSecStartElement.getParentXMLSecStartElement());
                    createStartElementAndOutputAsEvent(subOutputProcessorChain,
                            new QName(soapMessageVersion, WSSConstants.TAG_SOAP_HEADER_LN, WSSConstants.PREFIX_SOAPENV), true, null);
                    boolean mustUnderstand = ((WSSSecurityProperties) getSecurityProperties()).isMustUnderstand();
                    buildSecurityHeader(soapMessageVersion, subOutputProcessorChain, mustUnderstand);
                    createEndElementAndOutputAsEvent(subOutputProcessorChain,
                            new QName(soapMessageVersion, WSSConstants.TAG_SOAP_HEADER_LN, WSSConstants.PREFIX_SOAPENV));

                    //output current soap-header event
                    outputProcessorChain.processEvent(xmlSecEvent);
                    //remove this processor. its no longer needed.
                    outputProcessorChain.removeProcessor(this);

                    eventHandled = true;
                }
                break;
            case XMLStreamConstants.END_ELEMENT:
                XMLSecEndElement xmlSecEndElement = xmlSecEvent.asEndElement();
                int documentLevel = xmlSecEndElement.getDocumentLevel();
                String soapMessageVersionNS =
                    WSSUtils.getSOAPMessageVersionNamespace(xmlSecEndElement.getParentXMLSecStartElement());
                if (documentLevel == 2 && WSSConstants.TAG_SOAP_HEADER_LN.equals(xmlSecEndElement.getName().getLocalPart())
                        && xmlSecEndElement.getName().getNamespaceURI().equals(soapMessageVersionNS)) {
                    OutputProcessorChain subOutputProcessorChain = outputProcessorChain.createSubChain(this);
                    boolean mustUnderstand = ((WSSSecurityProperties) getSecurityProperties()).isMustUnderstand();
                    buildSecurityHeader(xmlSecEndElement.getName().getNamespaceURI(), subOutputProcessorChain, mustUnderstand);
                    //output current soap-header event
                    outputProcessorChain.processEvent(xmlSecEvent);
                    //remove this processor. its no longer needed.
                    outputProcessorChain.removeProcessor(this);

                    eventHandled = true;
                }
                break;
        }

        if (!eventHandled) {
            outputProcessorChain.processEvent(xmlSecEvent);
        }
    }

    private void changeSOAPNamespace(SecurePart securePart, String soapVersion) {
        final QName secureName = securePart.getName();
        if (secureName != null) {
            QName newName = secureName;

            if (WSSConstants.NS_SOAP11.equals(secureName.getNamespaceURI())
                && WSSConstants.NS_SOAP12.equals(soapVersion)) {
                newName = new QName(soapVersion, secureName.getLocalPart(), secureName.getPrefix());
            } else if (WSSConstants.NS_SOAP12.equals(secureName.getNamespaceURI())
                && WSSConstants.NS_SOAP11.equals(soapVersion)) {
                newName = new QName(soapVersion, secureName.getLocalPart(), secureName.getPrefix());
            }

            if (!secureName.equals(newName)) {
                securePart.setName(newName);
            }
        }
    }

    private void buildSecurityHeader(
        String soapMessageVersion,
        OutputProcessorChain subOutputProcessorChain,
        boolean mustUnderstand
    ) throws XMLStreamException, XMLSecurityException {
        List<XMLSecAttribute> attributes = new ArrayList<>(1);
        final String actor = ((WSSSecurityProperties) getSecurityProperties()).getActor();
        if (actor != null && !actor.isEmpty()) {
            if (WSSConstants.NS_SOAP11.equals(soapMessageVersion)) {
                attributes.add(createAttribute(WSSConstants.ATT_SOAP11_ACTOR, actor));
            } else {
                attributes.add(createAttribute(WSSConstants.ATT_SOAP12_ROLE, actor));
            }
        }
        if (mustUnderstand) {
            if (WSSConstants.NS_SOAP11.equals(soapMessageVersion)) {
                attributes.add(createAttribute(WSSConstants.ATT_SOAP11_MUST_UNDERSTAND, "1"));
            } else {
                attributes.add(createAttribute(WSSConstants.ATT_SOAP12_MUST_UNDERSTAND, "true"));
            }
        }
        createStartElementAndOutputAsEvent(subOutputProcessorChain, WSSConstants.TAG_WSSE_SECURITY, true, attributes);
        createEndElementAndOutputAsEvent(subOutputProcessorChain, WSSConstants.TAG_WSSE_SECURITY);
    }
}
