/**
 * 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.dom.message;

import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import javax.xml.namespace.QName;
import javax.xml.transform.TransformerException;

import org.apache.wss4j.common.util.XMLUtils;


import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * Applies message transformations to the tests in
 * org.apache.wss4j.dom.message.RequireSignedEncryptedDataElementsTest
 */
public class TestMessageTransformer {
    private static final org.slf4j.Logger LOG = org.slf4j.LoggerFactory.getLogger(TestMessageTransformer.class);

    public static Element duplicateEncryptedDataInWsseHeader(Element saaj, boolean moveReferenceList) throws TransformerException, IOException {
        if (moveReferenceList) {
            moveReferenceList(saaj);
        }
        Element body = getFirstChildElement(saaj, new QName("http://schemas.xmlsoap.org/soap/envelope/",
                                                            "Body"), true);
        Element encData = getFirstChildElement(body, new QName("http://www.w3.org/2001/04/xmlenc#",
                                                               "EncryptedData"), true);
        Element newEncData = createNewEncryptedData(encData);
        Element sh = getFirstChildElement(saaj, new QName("http://schemas.xmlsoap.org/soap/envelope/",
                                                          "Header"), true);
        Element wsseHeader = getFirstChildElement(sh,
                                                  new QName(
                                                            "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",
                                                            "Security"), true);

        Node newWsseHeader = wsseHeader.cloneNode(false);
        Node cur = wsseHeader.getFirstChild();

        String newId = newEncData.getAttributeNS(null, "Id");
        while (cur != null) {
            cur = copyHeadersAndUpdateRefList(cur, newWsseHeader, newId);
        }
        newWsseHeader.appendChild(newEncData);

        if (!moveReferenceList) {
            updateEncryptedKeyRefList(newWsseHeader, newId);
        }

        Node parent = wsseHeader.getParentNode();
        parent.removeChild(wsseHeader);
        parent.appendChild(newWsseHeader);
        print(saaj.getOwnerDocument());
        return newEncData;
    }

    public static Element duplicateEncryptedDataInWsseWrapperHeader(Element saaj, boolean moveReferenceList) throws TransformerException, IOException {
        if (moveReferenceList) {
            moveReferenceList(saaj);
        }
        Element body = getFirstChildElement(saaj, new QName("http://schemas.xmlsoap.org/soap/envelope/",
                                                            "Body"), true);
        Element encData = getFirstChildElement(body, new QName("http://www.w3.org/2001/04/xmlenc#",
                                                               "EncryptedData"), true);
        Element newEncData = createNewEncryptedData(encData);
        Element sh = getFirstChildElement(saaj, new QName("http://schemas.xmlsoap.org/soap/envelope/",
                                                          "Header"), true);
        Element signature = getFirstChildElement(sh, new QName("http://www.w3.org/2000/09/xmldsig#",
                                                               "Signature"), true);

        Node wsseHeader = signature.getParentNode();
        Node newWsseHeader = wsseHeader.cloneNode(false);
        Node cur = wsseHeader.getFirstChild();
        String newId = newEncData.getAttributeNS(null, "Id");
        while (!cur.isSameNode(signature)) {
            cur = copyHeadersAndUpdateRefList(cur, newWsseHeader, newId);
        }
        Element wrapper = encData.getOwnerDocument().createElementNS(null, "a");
        wrapper.appendChild(newEncData);
        newWsseHeader.appendChild(wrapper);
        while (cur != null) {
            cur = copyHeadersAndUpdateRefList(cur, newWsseHeader, newId);
        }

        if (!moveReferenceList) {
            updateEncryptedKeyRefList(newWsseHeader, newId);
        }

        Node parent = wsseHeader.getParentNode();
        parent.removeChild(wsseHeader);
        parent.appendChild(newWsseHeader);
        print(saaj.getOwnerDocument());
        return newEncData;
    }

    public static Element duplicateEncryptedDataInWrapperBody(Element saaj) throws TransformerException, IOException {
        Element body = getFirstChildElement(saaj, new QName("http://schemas.xmlsoap.org/soap/envelope/",
                                                            "Body"), true);
        Element encData = getFirstChildElement(body, new QName("http://www.w3.org/2001/04/xmlenc#",
                                                               "EncryptedData"), true);
        Element newEncData = createNewEncryptedData(encData);
        Element sh = getFirstChildElement(saaj, new QName("http://schemas.xmlsoap.org/soap/envelope/",
                                                          "Header"), true);
        Element signature = getFirstChildElement(sh, new QName("http://www.w3.org/2000/09/xmldsig#",
                                                               "Signature"), true);

        Node wsseHeader = signature.getParentNode();
        Node newWsseHeader = wsseHeader.cloneNode(false);
        Node cur = wsseHeader.getFirstChild();
        String newId = newEncData.getAttributeNS(null, "Id");
        while (!cur.isSameNode(signature)) {
            cur = copyHeadersAndUpdateRefList(cur, newWsseHeader, newId);
        }
        Element wrapper = encData.getOwnerDocument().createElementNS(null, "a");
        wrapper.appendChild(newEncData);

        body.appendChild(wrapper);
        while (cur != null) {
            cur = copyHeadersAndUpdateRefList(cur, newWsseHeader, newId);
        }

        updateEncryptedKeyRefList(newWsseHeader, newId);

        Node parent = wsseHeader.getParentNode();
        parent.removeChild(wsseHeader);
        parent.appendChild(newWsseHeader);
        print(saaj.getOwnerDocument());
        return newEncData;
    }

    public static Element duplicateEncryptedDataAfterWrapperBody(Element saaj) throws TransformerException, IOException {
        Element body = getFirstChildElement(saaj, new QName("http://schemas.xmlsoap.org/soap/envelope/",
                                                            "Body"), true);
        Element encData = getFirstChildElement(body, new QName("http://www.w3.org/2001/04/xmlenc#",
                                                               "EncryptedData"), true);
        Element newEncData = createNewEncryptedData(encData);
        Element sh = getFirstChildElement(saaj, new QName("http://schemas.xmlsoap.org/soap/envelope/",
                                                          "Header"), true);
        Element signature = getFirstChildElement(sh, new QName("http://www.w3.org/2000/09/xmldsig#",
                                                               "Signature"), true);

        Node wsseHeader = signature.getParentNode();
        Node newWsseHeader = wsseHeader.cloneNode(false);
        Node cur = wsseHeader.getFirstChild();
        String newId = newEncData.getAttributeNS(null, "Id");
        while (!cur.isSameNode(signature)) {
            cur = copyHeadersAndUpdateRefList(cur, newWsseHeader, newId);
        }
        Element wrapper = encData.getOwnerDocument().createElementNS(null, "a");
        Node clonedBody = body.cloneNode(false);
        clonedBody.appendChild(newEncData);
        wrapper.appendChild(clonedBody);

        body.getParentNode().appendChild(wrapper);
        while (cur != null) {
            cur = copyHeadersAndUpdateRefList(cur, newWsseHeader, newId);
        }

        updateEncryptedKeyRefList(newWsseHeader, newId);

        Node parent = wsseHeader.getParentNode();
        parent.removeChild(wsseHeader);
        parent.appendChild(newWsseHeader);
        print(saaj.getOwnerDocument());
        return newEncData;
    }

    public static Element duplicateEncryptedDataInExternalWrapperElement(Element saaj,
                                                                         boolean moveReferenceList) throws TransformerException, IOException {
        if (moveReferenceList) {
            moveReferenceList(saaj);
        }
        Element body = getFirstChildElement(saaj, new QName("http://schemas.xmlsoap.org/soap/envelope/",
                                                            "Body"), true);
        Element encData = getFirstChildElement(body, new QName("http://www.w3.org/2001/04/xmlenc#",
                                                               "EncryptedData"), true);
        Element newEncData = createNewEncryptedData(encData);
        Element sh = getFirstChildElement(saaj, new QName("http://schemas.xmlsoap.org/soap/envelope/",
                                                          "Header"), true);
        Element wsseHeader = getFirstChildElement(sh,
                                                  new QName(
                                                            "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",
                                                            "Security"), true);

        Node newWsseHeader = wsseHeader.cloneNode(false);
        Node cur = wsseHeader.getFirstChild();
        String newId = newEncData.getAttributeNS(null, "Id");
        while (cur != null) {
            cur = copyHeadersAndUpdateRefList(cur, newWsseHeader, newId);
        }
        sh.removeChild(wsseHeader);
        sh.appendChild(newWsseHeader);

        if (!moveReferenceList) {
            updateEncryptedKeyRefList(newWsseHeader, newId);
        }

        Element wrapper = encData.getOwnerDocument().createElementNS(null, "a");
        wrapper.setAttributeNS("http://schemas.xmlsoap.org/soap/envelope/", "mustUnderstand", "0");
        wrapper.setAttributeNS("http://schemas.xmlsoap.org/soap/envelope/", "actor", "foo");
        wrapper.appendChild(newEncData);
        sh.appendChild(wrapper);
        print(saaj.getOwnerDocument());
        return newEncData;
    }

    public static Element addEncryptedDataWithEmbeddedEncryptedKeyInWsseHeader(Element saaj) throws TransformerException, IOException {
        moveReferenceList(saaj);
        Element body = getFirstChildElement(saaj, new QName("http://schemas.xmlsoap.org/soap/envelope/",
                                                            "Body"), true);
        Element encData = getFirstChildElement(body, new QName("http://www.w3.org/2001/04/xmlenc#",
                                                               "EncryptedData"), true);

        Element newEncData = (Element)encData.cloneNode(true);
        String newId = newEncData.getAttributeNS(null, "Id") + "b";
        newEncData.setAttributeNS(null, "Id", newId);

        Element encKey = getFirstChildElement(saaj, new QName("http://www.w3.org/2001/04/xmlenc#",
                                                              "EncryptedKey"), true);
        Element newEncKey = (Element)encKey.cloneNode(true);
        String newEcId = newEncKey.getAttributeNS(null, "Id") + "b";
        newEncKey.setAttributeNS(null, "Id", newEcId);

        Element keyInfo = getFirstChildElement(newEncData, new QName("http://www.w3.org/2000/09/xmldsig#",
                                                                     "KeyInfo"), true);
        Element str = getFirstChildElement(newEncData,
                                           new QName(
                                                     "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",
                                                     "SecurityTokenReference"), true);
        keyInfo.replaceChild(newEncKey, str);

        Element wsseHeader = getFirstChildElement(saaj,
                                                  new QName(
                                                            "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd",
                                                            "Security"), true);

        Node newWsseHeader = wsseHeader.cloneNode(false);
        Node cur = wsseHeader.getFirstChild();

        while (cur != null) {
            cur = copyHeadersAndUpdateRefList(cur, newWsseHeader, newId);
        }
        newWsseHeader.appendChild(newEncData);

        Node parent = wsseHeader.getParentNode();
        parent.removeChild(wsseHeader);
        parent.appendChild(newWsseHeader);
        print(saaj.getOwnerDocument());
        return newEncData;

    }

    private static void moveReferenceList(Element saaj) {
        Element sh = getFirstChildElement(saaj, new QName("http://schemas.xmlsoap.org/soap/envelope/",
                                                          "Header"), true);
        Element encKey = getFirstChildElement(sh, new QName("http://www.w3.org/2001/04/xmlenc#",
                                                            "EncryptedKey"), true);
        Element refList = getFirstChildElement(encKey, new QName("http://www.w3.org/2001/04/xmlenc#",
                                                                 "ReferenceList"), true);

        Node wsseHeader = encKey.getParentNode();
        encKey.removeChild(refList);
        wsseHeader.appendChild(refList);
    }

    private static void updateEncryptedKeyRefList(Node wsseHeader, String newId) {
        Element encryptedKey = getFirstChildElement(wsseHeader,
                                                    new QName("http://www.w3.org/2001/04/xmlenc#",
                                                              "EncryptedKey"), true);
        Element ref = getFirstChildElement(encryptedKey, new QName("http://www.w3.org/2001/04/xmlenc#",
                                                                   "DataReference"), true);
        Element newRef = (Element)ref.cloneNode(true);
        newRef.setAttributeNS(null, "URI", "#" + newId);
        ref.getParentNode().appendChild(newRef);
    }

    private static void print(Document doc) throws TransformerException, IOException {
        if (LOG.isDebugEnabled()) {
            LOG.debug("After transformation....");
            String outputString = XMLUtils.prettyDocumentToString(doc);
            LOG.debug(outputString);
        }
    }

    private static Element createNewEncryptedData(Element encData) {
        Element newEncData = (Element)encData.cloneNode(true);
        String id = newEncData.getAttributeNS(null, "Id");
        String newId = id + "b";
        newEncData.setAttributeNS(null, "Id", newId);
        return newEncData;
    }

    private static Node copyHeadersAndUpdateRefList(Node cur, Node dest, String newId) {
        Node temp = cur.cloneNode(true);
        dest.appendChild(temp);
        if (newId != null && temp.getNodeType() == Node.ELEMENT_NODE) {
            Element t = (Element)temp;
            if (t.getLocalName().equals("ReferenceList")) {
                Element ref = getFirstChildElement(t, new QName("http://www.w3.org/2001/04/xmlenc#",
                                                                "DataReference"), true);
                Element newRef = (Element)ref.cloneNode(true);
                newRef.setAttributeNS(null, "URI", "#" + newId);
                t.appendChild(newRef);
            }
        }
        return cur.getNextSibling();
    }

    private static Element getFirstChildElement(Node node, QName nodeName, boolean recursive) {
        Element childElement = null;
        Iterator<Element> it = getChildElements(node, nodeName, recursive).iterator();
        if (it.hasNext()) {
            childElement = it.next();
        }
        return childElement;
    }

    private static List<Element> getChildElements(Node node, QName nodeName, boolean recursive) {
        List<Element> list = new LinkedList<>();

        NodeList nlist = node.getChildNodes();
        int len = nlist.getLength();
        for (int i = 0; i < len; i++) {
            Node child = nlist.item(i);
            if (child.getNodeType() == Node.ELEMENT_NODE) {
                search(list, (Element)child, nodeName, recursive);
            }
        }
        return list;
    }

    private static void search(List<Element> list, Element baseElement, QName nodeName, boolean recursive) {
        if (nodeName == null) {
            list.add(baseElement);
        } else {
            QName qname;
            if (nodeName.getNamespaceURI().length() > 0) {
                qname = new QName(baseElement.getNamespaceURI(), baseElement.getLocalName());
            } else {
                qname = new QName(baseElement.getLocalName());
            }
            if (qname.equals(nodeName)) {
                list.add(baseElement);
            }
        }
        if (recursive) {
            NodeList nlist = baseElement.getChildNodes();
            int len = nlist.getLength();
            for (int i = 0; i < len; i++) {
                Node child = nlist.item(i);
                if (child.getNodeType() == Node.ELEMENT_NODE) {
                    search(list, (Element)child, nodeName, recursive);
                }
            }
        }
    }

}
