/*

   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.batik.util;

import org.apache.batik.test.AbstractTest;
import org.apache.batik.test.DefaultTestReport;
import org.apache.batik.test.TestReport;

import java.io.PipedOutputStream;
import java.io.PipedInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.io.PrintWriter;

import java.net.URL;

/**
 * This test validates that the Base65 encoder/decoders work properly.
 *
 * @author <a href="mailto:deweese@apache.org">Thomas DeWeese</a>
 * @version $Id: Base64Test.java 1733416 2016-03-03 07:07:13Z gadams $
 */
public class Base64Test extends AbstractTest {
    /**
     * Error when bad action string given.
     * {0} = Bad action string
     */
    public static final String ERROR_BAD_ACTION_STRING
        = "Base64Test.error.bad.action.string";

    /**
     * Error when unable to read/open in URL
     * {0} = URL
     * {1} = exception stack trace.
     */
    public static final String ERROR_CANNOT_READ_IN_URL
        = "Base64Test.error.cannot.read.in.url";

    /**
     * Error when unable to read/open ref URL
     * {0} = URL
     * {1} = exception stack trace.
     */
    public static final String ERROR_CANNOT_READ_REF_URL
        = "Base64Test.error.cannot.read.ref.url";

    /**
     * Result didn't match reference result.
     * {0} = first byte of mismatch
     */
    public static final String ERROR_WRONG_RESULT
        = "Base64Test.error.wrong.result";

    public static final String ENTRY_KEY_ERROR_DESCRIPTION
        = "Base64Test.entry.key.error.description";

    protected String action = null;
    protected URL    in     = null;
    protected URL    ref    = null;

    /**
     * Constructor. ref is ignored if action == ROUND.
     * @param action The action to perform, one of:
     *               ROUND  : base64 encode then base64 decode.
     *               ENCODE : encode in to base 64 and compare result to ref.
     *               DECODE : decode in (must be base 64) and compare to ref.
     * @param in     The source file to apply 'action' to.
     * @param ref    The reference file.
     */
    public Base64Test(String action, URL in, URL ref) {
        this.action = action;
        this.in     = in;
        this.ref    = ref;
    }

    /**
     * Constructor, for round trip testing (only one file required).
     * @param in     The source file to round trip.
     */
    public Base64Test(URL in) {
        this.action = "ROUND";
        this.in     = in;
    }

    /**
     * Returns this Test's name
     */
    public String getName() {
        return action + " -- " + in + " -- " + super.getName();
    }

    /**
     * This method will only throw exceptions if some aspect
     * of the test's internal operation fails.
     */
    public TestReport runImpl() throws Exception {
        DefaultTestReport report
            = new DefaultTestReport(this);

        InputStream inIS;

        try {
            inIS = in.openStream();
        } catch(Exception e) {
            StringWriter trace = new StringWriter();
            e.printStackTrace(new PrintWriter(trace));
            report.setErrorCode(ERROR_CANNOT_READ_IN_URL);
            report.setDescription(new TestReport.Entry[] {
                new TestReport.Entry
                    (TestMessages.formatMessage
                     (ENTRY_KEY_ERROR_DESCRIPTION, null),
                     TestMessages.formatMessage
                     (ERROR_CANNOT_READ_IN_URL,
                      new String[]{in.toString(), trace.toString()}))
                    });
            report.setPassed(false);
            return report;
        }

        if (action.equals("ROUND"))
            this.ref = in;
        else if (!action.equals("ENCODE") &&
                 !action.equals("DECODE")) {
            report.setErrorCode(ERROR_BAD_ACTION_STRING);
            report.setDescription(new TestReport.Entry[] {
                new TestReport.Entry
                    (TestMessages.formatMessage
                     (ENTRY_KEY_ERROR_DESCRIPTION, null),
                     TestMessages.formatMessage(ERROR_BAD_ACTION_STRING,
                                            new String[]{action}))
                    });
            report.setPassed(false);
            return report;
        }

        InputStream refIS;
        try {
            refIS = ref.openStream();
        } catch(Exception e) {
            StringWriter trace = new StringWriter();
            e.printStackTrace(new PrintWriter(trace));
            report.setErrorCode(ERROR_CANNOT_READ_REF_URL);
            report.setDescription(new TestReport.Entry[] {
                new TestReport.Entry
                    (TestMessages.formatMessage
                     (ENTRY_KEY_ERROR_DESCRIPTION, null),
                     TestMessages.formatMessage
                     (ERROR_CANNOT_READ_REF_URL,
                      new String[]{ref.toString(), trace.toString()}))
                    });
            report.setPassed(false);
            return report;
        }

        if (action.equals("ENCODE") ||
            action.equals("ROUND")) {
          // We need to encode the incomming data
          PipedOutputStream pos = new PipedOutputStream();
          OutputStream os = new Base64EncoderStream(pos);

          // Copy the input to the Base64 Encoder (in a seperate thread).
          Thread t = new StreamCopier(inIS, os);

          // Read that from the piped output stream.
          inIS = new PipedInputStream(pos);
          t.start();
        }

        if (action.equals("DECODE")||
            action.equals("ROUND")) {
            inIS = new Base64DecodeStream(inIS);
        }


        int mismatch = compareStreams(inIS, refIS, action.equals("ENCODE"));

        if (mismatch == -1) {
          report.setPassed(true);
          return report;
        }

        report.setErrorCode(ERROR_WRONG_RESULT);
        report.setDescription(new TestReport.Entry[] {
          new TestReport.Entry
            (TestMessages.formatMessage(ENTRY_KEY_ERROR_DESCRIPTION, null),
             TestMessages.formatMessage(ERROR_WRONG_RESULT,
                                    new String[]{ String.valueOf( mismatch )} ))
            });
        report.setPassed(false);
        return report;
    }

    /**
     * Returns true if the contents of <code>is1</code> match the
     * contents of <code>is2</code>
     */
    public static int compareStreams(InputStream is1, InputStream is2,
                              boolean skipws) {
        byte [] data1 = new byte[100];
        byte [] data2 = new byte[100];
        int off1=0;
        int off2=0;
        int idx=0;

        try {
            while(true) {
                int len1 = is1.read(data1, off1, data1.length-off1);
                int len2 = is2.read(data2, off2, data2.length-off2);

                if (off1 != 0) {
                    if (len1 == -1)
                        len1 = off1;
                    else
                        len1 += off1;
                }

                if (off2 != 0) {
                    if (len2 == -1)
                        len2 = off2;
                    else
                        len2 += off2;
                }

                if (len1 == -1) {
                    if (len2 == -1)
                        break; // Both done...

                    // Only is1 is done...
                    if (!skipws)
                        return idx;

                    // check if the rest of is2 is whitespace...
                    for (int i2=0; i2<len2; i2++)
                        if ((data2[i2] != '\n') &&
                            (data2[i2] != '\r') &&
                            (data2[i2] != ' '))
                            return idx+i2;
                    off1 = off2 = 0;
                    continue;
                }

                if (len2 == -1) {
                    // Only is2 is done...
                    if (!skipws)
                        return idx;

                    // Check if rest of is1 is whitespace...
                    for (int i1=0; i1<len1; i1++)
                        if ((data1[i1] != '\n') &&
                            (data1[i1] != '\r') &&
                            (data1[i1] != ' '))
                            return idx+i1;
                    off1 = off2 = 0;
                    continue;
                }

                int i1=0;
                int i2=0;
                while((i1<len1) && (i2<len2)) {
                    if (skipws) {
                        if ((data1[i1] == '\n') ||
                            (data1[i1] == '\r') ||
                            (data1[i1] == ' ')) {
                            i1++;
                            continue;
                        }
                        if ((data2[i2] == '\n') ||
                            (data2[i2] == '\r') ||
                            (data2[i2] == ' ')) {
                            i2++;
                            continue;
                        }
                    }
                    if (data1[i1] != data2[i2])
                        return idx+i2;

                    i1++;
                    i2++;
                }

                if (i1 != len1)
                    System.arraycopy(data1, i1, data1, 0, len1-i1);
                if (i2 != len2)
                    System.arraycopy(data2, i2, data2, 0, len2-i2);
                off1 = len1-i1;
                off2 = len2-i2;
                idx+=i2;
            }
        } catch(IOException ioe) {
            ioe.printStackTrace();
            return idx;
        }

        return -1;
    }


    static class StreamCopier extends Thread {
        InputStream src;
        OutputStream dst;

        public StreamCopier(InputStream src,
                            OutputStream dst) {
            this.src = src;
            this.dst = dst;
        }

        public void run() {
            try {
                byte [] data = new byte[1000];
                while(true) {
                    int len = src.read(data, 0, data.length);
                    if (len == -1) break;

                    dst.write(data, 0, len);
                }
            } catch (IOException ioe) {
                // Nothing
            }
            try {
                dst.close();
            } catch (IOException ioe) {
                // Nothing
            }
        }
    }
}
