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

import org.apache.wss4j.common.ext.WSSecurityException;
import org.apache.wss4j.dom.handler.WSHandlerConstants;
import org.apache.xml.security.stax.securityEvent.SecurityEvent;
import org.apache.wss4j.stax.WSSec;
import org.apache.wss4j.stax.ext.InboundWSSec;
import org.apache.wss4j.stax.ext.OutboundWSSec;
import org.apache.wss4j.stax.ext.WSSConstants;
import org.apache.wss4j.stax.ext.WSSSecurityProperties;
import org.apache.wss4j.stax.test.CallbackHandlerImpl;
import org.apache.wss4j.stax.test.utils.XmlReaderToWriter;
import org.testng.annotations.*;
import org.w3c.dom.Document;

import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import java.io.*;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;

public class PerformanceMemoryTest extends AbstractTestBase {

    private FileWriter outSamples;

    @BeforeClass
    public void createDir() throws Exception {
        new File("target/performanceMemoryTest").mkdirs();
    }

    @BeforeGroups(groups = {"memory-out"})
    public void createSampleFileOut() throws Exception {
        outSamples = new FileWriter("target/memory-out-samples.txt");
    }

    //warm up.
    @BeforeMethod(groups = {"memory-out"})
    public void setUpOut() throws Exception {
        File input = genBigFile(1);
        doDOMSecurityOutbound(input, new File("target/performanceMemoryTest/bigfile-dom.xml"));
        doStreamingSecurityOutbound(input, new File("target/performanceMemoryTest/bigfile-stream.xml"));
    }

    @AfterGroups(groups = {"memory-out"})
    public void tearDownOut() throws Exception {
        outSamples.close();
    }

    @DataProvider(name = "xmlsizes")
    public Object[][] getXMLSizes() throws Exception {
        genBigFile(1);
        int tagCount = 0;
        File target = new File("target/performanceMemoryTest/tmp.xml");
        XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(new BufferedInputStream(new FileInputStream(target)));
        while (xmlStreamReader.hasNext()) {
            int eventType = xmlStreamReader.next();
            if (eventType == XMLStreamConstants.START_ELEMENT) {
                tagCount++;
            }
        }
        /*Object[][] objectArray = new Object[1][2];
        objectArray[0][0] = 10;
        objectArray[0][1] = (tagCount - 4) * 10;*/

        int size = 16;
        Object[][] objectArray = new Object[size][2];
        for (int i = 0; i < size; i++) {
            objectArray[i][0] = i + 1;
            objectArray[i][1] = (tagCount - 4) * (i + 1) * 40;
        }
        return objectArray;
    }

    @BeforeGroups(groups = {"memory-out"}, dependsOnMethods = {"createSampleFileOut"})
    public void printTagCountsOut() throws Exception {
        Object[][] sizes = getXMLSizes();
        for (int i = 0; i < sizes.length; i++) {
            Object[] size = sizes[i];
            outSamples.write("" + size[1] + " ");
        }
    }


    @Test(groups = "memory-out", dataProvider = "xmlsizes", dependsOnMethods = {"doBeforeStreamOut"})
    public void testOutStreamingMemoryPerformance(int run, int tagCount) throws Exception {

        System.out.println("Run " + run);
        File input = genBigFile(run * 40);
        long startMem = getUsedMemory();

        ThreadStopper threadStopper = new ThreadStopper();
        Thread thread = new Thread(new MemorySamplerThread(threadStopper, outSamples, startMem));
        thread.setPriority(9);
        thread.start();

        doStreamingSecurityOutbound(input, new File("target/performanceMemoryTest/stream-" + tagCount + ".xml"));

        threadStopper.setStop(true);
        thread.join();
    }

    @Test(groups = "memory-out", dataProvider = "xmlsizes", dependsOnMethods = {"doBeforeStreamCompressedOut"})
    public void testOutStreamingCompressedMemoryPerformance(int run, int tagCount) throws Exception {

        System.out.println("Run " + run);
        File input = genBigFile(run * 40);
        long startMem = getUsedMemory();

        ThreadStopper threadStopper = new ThreadStopper();
        Thread thread = new Thread(new MemorySamplerThread(threadStopper, outSamples, startMem));
        thread.setPriority(9);
        thread.start();

        doStreamingSecurityOutboundCompressed(input, new File("target/performanceMemoryTest/stream-compressed-" + tagCount + ".xml"), "http://www.apache.org/2012/04/xmlsec/gzip");

        threadStopper.setStop(true);
        thread.join();
    }

    @Test(groups = "memory-out", dataProvider = "xmlsizes", dependsOnMethods = {"doBeforeDOMOut"})
    public void testOutDOMMemoryPerformance(int run, int tagCount) throws Exception {

        System.out.println("Run " + run);
        File input = genBigFile(run * 40);
        long startMem = getUsedMemory();

        ThreadStopper threadStopper = new ThreadStopper();
        Thread thread = new Thread(new MemorySamplerThread(threadStopper, outSamples, startMem));
        thread.setPriority(9);
        thread.start();

        doDOMSecurityOutbound(input, new File("target/performanceMemoryTest/dom-" + tagCount + ".xml"));

        threadStopper.setStop(true);
        thread.join();
    }

    @Test(groups = {"memory-out"})
    public void doBeforeDOMOut() throws Exception {
        outSamples.write("\n");
    }

    @Test(groups = {"memory-out"})
    public void doBeforeStreamOut() throws Exception {
        outSamples.write("\n");
    }

    @Test(groups = {"memory-out"})
    public void doBeforeStreamCompressedOut() throws Exception {
        outSamples.write("\n");
    }

    @Test(groups = {"memory-in"})
    public void doBeforeDOMIn() throws Exception {
        inSamples.write("\n");
    }

    @Test(groups = {"memory-in"})
    public void doBeforeStreamIn() throws Exception {
        inSamples.write("\n");
    }

    @Test(groups = {"memory-in"})
    public void doBeforeStreamCompressedIn() throws Exception {
        inSamples.write("\n");
    }

    private FileWriter inSamples;

    @BeforeGroups(groups = {"memory-in"})
    public void createSampleFileIn() throws Exception {
        inSamples = new FileWriter("target/memory-in-samples.txt");
    }

    //warm up.
    @BeforeMethod(groups = {"memory-in"}, dependsOnGroups = {"memory-out"})
    public void setUpIn() throws Exception {
        genBigFile(1);
        doDOMInSecurity(new File("target/performanceMemoryTest/bigfile-dom.xml"));
        doStreamingInSecurity(new File("target/performanceMemoryTest/bigfile-stream.xml"));
    }

    @AfterGroups(groups = {"memory-in"})
    public void tearDownIn() throws Exception {
        inSamples.close();
    }

    @BeforeGroups(groups = {"memory-in"}, dependsOnMethods = {"createSampleFileIn"})
    public void printTagCountsIn() throws Exception {
        Object[][] sizes = getXMLSizes();
        for (int i = 0; i < sizes.length; i++) {
            Object[] size = sizes[i];
            inSamples.write("" + size[1] + " ");
        }
    }


    @Test(groups = "memory-in", dataProvider = "xmlsizes", dependsOnMethods = {"doBeforeStreamIn", "testOutStreamingMemoryPerformance"})
    public void testInboundStreamingMemoryPerformance(int run, int tagCount) throws Exception {

        System.out.println("Run " + run);
        long startMem = getUsedMemory();

        ThreadStopper threadStopper = new ThreadStopper();
        Thread thread = new Thread(new MemorySamplerThread(threadStopper, inSamples, startMem));
        thread.setPriority(9);
        thread.start();

        doStreamingInSecurity(new File("target/performanceMemoryTest/stream-" + tagCount + ".xml"));

        threadStopper.setStop(true);
        thread.join();
    }

    @Test(groups = "memory-in", dataProvider = "xmlsizes", dependsOnMethods = {"doBeforeStreamCompressedIn", "testOutStreamingCompressedMemoryPerformance"})
    public void testInboundStreamingCompressedMemoryPerformance(int run, int tagCount) throws Exception {

        System.out.println("Run " + run);
        long startMem = getUsedMemory();

        ThreadStopper threadStopper = new ThreadStopper();
        Thread thread = new Thread(new MemorySamplerThread(threadStopper, inSamples, startMem));
        thread.setPriority(9);
        thread.start();

        doStreamingInSecurity(new File("target/performanceMemoryTest/stream-compressed-" + tagCount + ".xml"));

        threadStopper.setStop(true);
        thread.join();
    }

    @Test(groups = "memory-in", dataProvider = "xmlsizes", dependsOnMethods = {"doBeforeDOMIn", "testOutDOMMemoryPerformance"})
    public void testInboundDOMMemoryPerformance(int run, int tagCount) throws Exception {

        System.out.println("Run " + run);
        long startMem = getUsedMemory();

        ThreadStopper threadStopper = new ThreadStopper();
        Thread thread = new Thread(new MemorySamplerThread(threadStopper, inSamples, startMem));
        thread.setPriority(9);
        thread.start();

        doDOMInSecurity(new File("target/performanceMemoryTest/dom-" + tagCount + ".xml"));

        threadStopper.setStop(true);
        thread.join();
    }


    private OutboundWSSec outboundWSSec;

    private void doStreamingSecurityOutbound(File source, File output) throws Exception {
        if (outboundWSSec == null) {
            WSSSecurityProperties securityProperties = new WSSSecurityProperties();
            securityProperties.setCallbackHandler(new CallbackHandlerImpl());
            securityProperties.setEncryptionUser("receiver");
            securityProperties.loadEncryptionKeystore(this.getClass().getClassLoader().getResource("transmitter.jks"), "default".toCharArray());
            securityProperties.setSignatureUser("transmitter");
            securityProperties.loadSignatureKeyStore(this.getClass().getClassLoader().getResource("transmitter.jks"), "default".toCharArray());
            List<WSSConstants.Action> actions = new ArrayList<>();
            actions.add(WSSConstants.TIMESTAMP);
            actions.add(WSSConstants.SIGNATURE);
            actions.add(WSSConstants.ENCRYPTION);
            securityProperties.setActions(actions);
            securityProperties.setTimestampTTL(60 * 60 * 24 * 7); //a week for testing:)
            outboundWSSec = WSSec.getOutboundWSSec(securityProperties);
        }

        InputStream sourceDocument = new BufferedInputStream(new FileInputStream(source));
        XMLStreamWriter xmlStreamWriter = outboundWSSec.processOutMessage(new BufferedOutputStream(new FileOutputStream(output)), "UTF-8", new ArrayList<>());
        XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(sourceDocument);
        XmlReaderToWriter.writeAll(xmlStreamReader, xmlStreamWriter);
        xmlStreamWriter.close();
        xmlStreamReader.close();
    }

    private OutboundWSSec outboundWSSecCompressed;

    private void doStreamingSecurityOutboundCompressed(File source, File output, String compress) throws Exception {
        if (outboundWSSecCompressed == null) {
            WSSSecurityProperties securityProperties = new WSSSecurityProperties();
            securityProperties.setCallbackHandler(new CallbackHandlerImpl());
            securityProperties.setEncryptionUser("receiver");
            securityProperties.loadEncryptionKeystore(this.getClass().getClassLoader().getResource("transmitter.jks"), "default".toCharArray());
            securityProperties.setSignatureUser("transmitter");
            securityProperties.loadSignatureKeyStore(this.getClass().getClassLoader().getResource("transmitter.jks"), "default".toCharArray());
            List<WSSConstants.Action> actions = new ArrayList<>();
            actions.add(WSSConstants.TIMESTAMP);
            actions.add(WSSConstants.SIGNATURE);
            actions.add(WSSConstants.ENCRYPTION);
            securityProperties.setActions(actions);
            securityProperties.setTimestampTTL(60 * 60 * 24 * 7); //a week for testing:)
            securityProperties.setEncryptionCompressionAlgorithm(compress);
            outboundWSSecCompressed = WSSec.getOutboundWSSec(securityProperties);
        }

        InputStream sourceDocument = new BufferedInputStream(new FileInputStream(source));
        XMLStreamWriter xmlStreamWriter = outboundWSSecCompressed.processOutMessage(new BufferedOutputStream(new FileOutputStream(output)), "UTF-8", new ArrayList<>());
        XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(sourceDocument);
        XmlReaderToWriter.writeAll(xmlStreamReader, xmlStreamWriter);
        xmlStreamWriter.close();
        xmlStreamReader.close();
    }

    private void doDOMSecurityOutbound(File input, File output) throws WSSecurityException, FileNotFoundException, TransformerException {
        Properties properties = new Properties();
        properties.setProperty(WSHandlerConstants.ENC_SYM_ALGO, "http://www.w3.org/2001/04/xmlenc#aes256-cbc");
        properties.setProperty(WSHandlerConstants.ENC_KEY_TRANSPORT, "http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p");
        properties.setProperty(WSHandlerConstants.TTL_TIMESTAMP, "" + 60 * 60 * 24 * 7);
        Document doc = doOutboundSecurityWithWSS4J(new FileInputStream(input), WSHandlerConstants.TIMESTAMP + " " + WSHandlerConstants.SIGNATURE + " " + WSHandlerConstants.ENCRYPTION, properties);
        Transformer transformer = TransformerFactory.newInstance().newTransformer();
        transformer.transform(new DOMSource(doc), new StreamResult(output));
    }

    private File genBigFile(int factor) throws IOException {
        File target = new File("target/performanceMemoryTest/tmp.xml");
        FileWriter fileWriter = new FileWriter(target, false);
        fileWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
                "<env:Envelope xmlns:env=\"http://schemas.xmlsoap.org/soap/envelope/\">\n" +
                "<env:Header></env:Header>\n" +
                "<env:Body><test xmlns=\"http://www.example.com\">");
        fileWriter.close();
        FileOutputStream fileOutputStream = new FileOutputStream(target, true);
        for (int i = 0; i < factor; i++) {
            int read = 0;
            byte[] buffer = new byte[4096];
            InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("testdata/plain-soap-1.1.xml");
            while ((read = inputStream.read(buffer)) != -1) {
                fileOutputStream.write(buffer, 0, read);
            }
            inputStream.close();
        }
        fileOutputStream.close();
        fileWriter = new FileWriter(target, true);
        fileWriter.write("</test></env:Body>\n" +
                "</env:Envelope>");
        fileWriter.close();

        return target;
    }

    private void doDOMInSecurity(File input) throws Exception {
        String action = WSHandlerConstants.TIMESTAMP + " " + WSHandlerConstants.SIGNATURE + " " + WSHandlerConstants.ENCRYPTION;
        Properties properties = new Properties();
        properties.setProperty(WSHandlerConstants.TTL_TIMESTAMP, "" + 60 * 60 * 24 * 7);
        doInboundSecurityWithWSS4J_1(documentBuilderFactory.newDocumentBuilder().parse(input), action, properties, false);
    }

    private InboundWSSec inboundWSSec;

    private void doStreamingInSecurity(File input) throws Exception {
        if (inboundWSSec == null) {
            WSSSecurityProperties inSecurityProperties = new WSSSecurityProperties();
            inSecurityProperties.setStrictTimestampCheck(false);
            inSecurityProperties.loadSignatureVerificationKeystore(this.getClass().getClassLoader().getResource("receiver.jks"), "default".toCharArray());
            inSecurityProperties.loadDecryptionKeystore(this.getClass().getClassLoader().getResource("receiver.jks"), "default".toCharArray());
            inSecurityProperties.setCallbackHandler(new CallbackHandlerImpl());

            inboundWSSec = WSSec.getInboundWSSec(inSecurityProperties);
        }
        InputStream fileInputStream = new BufferedInputStream(new FileInputStream(input));
        XMLStreamReader outXmlStreamReader = inboundWSSec.processInMessage(xmlInputFactory.createXMLStreamReader(fileInputStream));

        while (outXmlStreamReader.hasNext()) {
            outXmlStreamReader.next();
        }
        fileInputStream.close();
        outXmlStreamReader.close();
    }


    private static void gc() {
        System.gc();
        System.runFinalization();
        System.gc();
    }


    private static long getUsedMemory() {
        gc();
        gc();
        long totalMemory = Runtime.getRuntime().totalMemory();
        long freeMemory = Runtime.getRuntime().freeMemory();
        return totalMemory - freeMemory;
    }

    class ThreadStopper {
        private volatile boolean stop = false;

        public boolean isStop() {
            return stop;
        }

        public void setStop(boolean stop) {
            this.stop = stop;
        }
    }

    class MemorySamplerThread implements Runnable {

        private ThreadStopper threadStopper;
        private FileWriter fileWriter;
        private long memoryDiff;

        private List<Integer> memory = new LinkedList<>();

        MemorySamplerThread(ThreadStopper threadStopper, FileWriter fileWriter, long memoryDiff) {
            this.threadStopper = threadStopper;
            this.fileWriter = fileWriter;
            this.memoryDiff = memoryDiff;
        }

        @Override
        public void run() {

            int sleepTime = 100;

            while (!threadStopper.isStop()) {
                try {
                    Thread.sleep(sleepTime);
                    if (threadStopper.isStop()) {
                        break;
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //parentThread.suspend();
                memory.add((int) (((getUsedMemory()) - memoryDiff) / 1024.0 / 1024.0));
                //System.out.println("Sample: " + memory.get(memory.size() - 1));
                //parentThread.resume();
            }

            System.out.println("Collected " + memory.size() + " samples");

            int maxMem = Integer.MIN_VALUE;
            for (int i = 0; i < memory.size(); i++) {
                //System.out.println("Sample: " + memory.get(i));
                int mem = memory.get(i);
                maxMem = mem > maxMem ? mem : maxMem;
            }

            try {
                fileWriter.write("" + maxMem + " ");
                fileWriter.flush();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }

            System.out.println("Max memory usage: " + maxMem + "MB");
        }
    }
}
