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

/* $Id$ */

package org.apache.xmlgraphics.util.io;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.net.URL;

import org.junit.Test;

import static org.junit.Assert.fail;

import org.apache.commons.io.IOUtils;

/**
 * This test validates that the Base64 encoder/decoders work properly.
 *
 * @version $Id$
 */
public class Base64TestCase {

    private void innerBase64Test(String action, URL in, URL ref) throws Exception {
        InputStream inIS = dos2Unix(in);

        if (action.equals("ROUND")) {
            ref = in;
        } else if (!action.equals("ENCODE") && !action.equals("DECODE")) {
            fail("Bad action string");
        }

        InputStream refIS = dos2Unix(ref);

        if (action.equals("ENCODE") || action.equals("ROUND")) {
            // We need to encode the incomming data
            PipedOutputStream pos = new PipedOutputStream();
            OutputStream os = new Base64EncodeStream(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) {
            fail("Wrong result");
        }
    }

    private InputStream dos2Unix(URL url) throws IOException {
        InputStream is = url.openStream();
        byte[] data = IOUtils.toByteArray(is);
        if (data.length > 1 && data[data.length - 1] == '\n') {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            for (byte b : data) {
                if (b != '\r') {
                    bos.write(b);
                }
            }
            return new ByteArrayInputStream(bos.toByteArray());
        }
        return new ByteArrayInputStream(data);
    }

    private void innerBase64Test(String action, String in, String ref) throws Exception {
        final String baseURL = "file:src/test/resources/org/apache/xmlgraphics/util/io/";
        innerBase64Test(action, new URL(baseURL + in), new URL(baseURL + ref));
    }

    private void innerBase64Test(String in) throws Exception {
        innerBase64Test("ROUND", in, in);
    }

    private void testBase64Group(String name) throws Exception {
        innerBase64Test("ENCODE", name, name + ".64");
        innerBase64Test("DECODE", name + ".64", name);
        innerBase64Test(name);
    }

    /**
     * This method will only throw exceptions if some aspect
     * of the test's internal operation fails.
     */
    @Test
    public void testBase64() throws Exception {
        System.out.println(new File(".").getCanonicalPath());
        testBase64Group("zeroByte");
        testBase64Group("oneByte");
        testBase64Group("twoByte");
        testBase64Group("threeByte");
        testBase64Group("fourByte");
        testBase64Group("tenByte");
        testBase64Group("small");
        testBase64Group("medium");
        innerBase64Test("DECODE", "medium.pc.64", "medium");
        innerBase64Test("large");
}

    /**
     * Returns true if the contents of <tt>is1</tt> match the
     * contents of <tt>is2</tt>
     */
    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
            }
        }
    }
}
