package global.namespace.truelicense.core;

import global.namespace.fun.io.api.Filter;
import global.namespace.fun.io.api.Socket;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import static global.namespace.fun.io.bios.BIOS.buffer;

final class Filters {

    /**
     * Returns a filter which, upon writing, first compresses the data and then encrypts it using the given filters.
     * Upon reading, the filter first tries to do the inverse, that is to first decrypt the data and then decompress it.
     * Failing that, the filter tries to swap the underlying filter, that is to first decompress the data and then
     * decrypt it.
     * This is a workaround for a bug in TrueLicense 4.0.0 and 4.0.1, where these filters were accidentally swapped,
     * thus (a) generating license keys which were bigger than they would need to be (because compressing encrypted data
     * only adds overhead and thus makes it a little bigger instead of smaller) and (b) breaking compatibility with
     * license keys generated by previous versions of TrueLicense.
     * Trying the underlying filters in both orders effectively restores compatibility with license keys generated by
     * *all* previous versions, with only a very small performance penalty for license keys generated by TrueLicense
     * 4.0.0 and 4.0.1.
     * Last but not least, if the filters fail in both attempts, the exception of the first attempt (which first
     * tried to decrypt the data and then decompress it) will be thrown, effectively discarding the exception of the
     * second attempt.
     */
    static Filter compressionAndEncryption(final Filter compression, final Filter encryption) {
        assert compression != null;
        assert encryption != null;
        return new Filter() {

            @Override
            public Socket<OutputStream> output(Socket<OutputStream> output) {
                return compression.output(encryption.output(output));
            }

            @SuppressWarnings({"deprecation", "ResultOfMethodCallIgnored"})
            @Override
            public Socket<InputStream> input(Socket<InputStream> input) {
                return () -> {
                    try {
                        // Use a buffer filter and do a simple one byte read-ahead test:
                        return buffer().input(compression.input(encryption.input(input))).map(in -> {
                            in.mark(1);
                            in.read();
                            in.reset();
                            return in;
                        }).get();
                    } catch (final IOException e) {
                        try {
                            return encryption.input(compression.input(input)).get();
                        } catch (IOException ignored) {
                            throw e;
                        }
                    }
                };
            }
        };
    }
}
