package libcore.util;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.Socket;
import java.net.SocketException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.List;
import javax.net.ssl.SSLSocket;
import org.eclipse.jetty.npn.NextProtoNego;

/**
 * APIs for interacting with Android's core library. This mostly emulates the
 * Android core library for interoperability with other runtimes.
 */
public final class Libcore {

    private Libcore() {
    }

    private static boolean useAndroidTlsApis;
    private static Class<?> openSslSocketClass;
    private static Method setEnabledCompressionMethods;
    private static Method setUseSessionTickets;
    private static Method setHostname;
    private static boolean android23TlsOptionsAvailable;
    private static Method setNpnProtocols;
    private static Method getNpnSelectedProtocol;
    private static boolean android41TlsOptionsAvailable;

    static {
        try {
            openSslSocketClass = Class.forName(
                    "org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl");
            useAndroidTlsApis = true;
            setEnabledCompressionMethods = openSslSocketClass.getMethod(
                    "setEnabledCompressionMethods", String[].class);
            setUseSessionTickets = openSslSocketClass.getMethod(
                    "setUseSessionTickets", boolean.class);
            setHostname = openSslSocketClass.getMethod("setHostname", String.class);
            android23TlsOptionsAvailable = true;
            setNpnProtocols = openSslSocketClass.getMethod("setNpnProtocols", byte[].class);
            getNpnSelectedProtocol = openSslSocketClass.getMethod("getNpnSelectedProtocol");
            android41TlsOptionsAvailable = true;
        } catch (ClassNotFoundException ignored) {
            // This isn't an Android runtime.
        } catch (NoSuchMethodException ignored) {
            // This Android runtime is missing some optional TLS options.
        }
    }

    public static void makeTlsTolerant(SSLSocket socket, String socketHost, boolean tlsTolerant) {
        if (!tlsTolerant) {
            socket.setEnabledProtocols(new String[] {"SSLv3"});
            return;
        }

        if (android23TlsOptionsAvailable && openSslSocketClass.isInstance(socket)) {
            // This is Android: use reflection on OpenSslSocketImpl.
            try {
                String[] compressionMethods = {"ZLIB"};
                setEnabledCompressionMethods.invoke(socket,
                        new Object[] { compressionMethods });
                setUseSessionTickets.invoke(socket, true);
                setHostname.invoke(socket, socketHost);
            } catch (InvocationTargetException e) {
                throw new RuntimeException(e);
            } catch (IllegalAccessException e) {
                throw new AssertionError(e);
            }
        }
    }

    /**
     * Returns the negotiated protocol, or null if no protocol was negotiated.
     */
    public static byte[] getNpnSelectedProtocol(SSLSocket socket) {
        if (useAndroidTlsApis) {
            // This is Android: use reflection on OpenSslSocketImpl.
            if (android41TlsOptionsAvailable && openSslSocketClass.isInstance(socket)) {
                try {
                    return (byte[]) getNpnSelectedProtocol.invoke(socket);
                } catch (InvocationTargetException e) {
                    throw new RuntimeException(e);
                } catch (IllegalAccessException e) {
                    throw new AssertionError(e);
                }
            }
            return null;
        } else {
            // This is OpenJDK: use JettyNpnProvider.
            JettyNpnProvider provider = (JettyNpnProvider) NextProtoNego.get(socket);
            if (!provider.unsupported && provider.selected == null) {
                throw new IllegalStateException(
                        "No callback received. Is NPN configured properly?");
            }
            try {
                return provider.unsupported
                        ? null
                        : provider.selected.getBytes("US-ASCII");
            } catch (UnsupportedEncodingException e) {
                throw new AssertionError(e);
            }
        }
    }

    public static void setNpnProtocols(SSLSocket socket, byte[] npnProtocols) {
        if (useAndroidTlsApis) {
            // This is Android: use reflection on OpenSslSocketImpl.
            if (android41TlsOptionsAvailable && openSslSocketClass.isInstance(socket)) {
                try {
                    setNpnProtocols.invoke(socket, new Object[] { npnProtocols });
                } catch (IllegalAccessException e) {
                    throw new AssertionError(e);
                } catch (InvocationTargetException e) {
                    throw new RuntimeException(e);
                }
            }
        } else {
            // This is OpenJDK: use JettyNpnProvider.
            try {
                List<String> strings = new ArrayList<String>();
                for (int i = 0; i < npnProtocols.length;) {
                    int length = npnProtocols[i++];
                    strings.add(new String(npnProtocols, i, length, "US-ASCII"));
                    i += length;
                }
                JettyNpnProvider provider = new JettyNpnProvider();
                provider.protocols = strings;
                NextProtoNego.put(socket, provider);
            } catch (UnsupportedEncodingException e) {
                throw new AssertionError(e);
            }
        }
    }

    private static class JettyNpnProvider
            implements NextProtoNego.ClientProvider, NextProtoNego.ServerProvider {
        List<String> protocols;
        boolean unsupported;
        String selected;

        @Override public boolean supports() {
            return true;
        }
        @Override public List<String> protocols() {
            return protocols;
        }
        @Override public void unsupported() {
            this.unsupported = true;
        }
        @Override public void protocolSelected(String selected) {
            this.selected = selected;
        }
        @Override public String selectProtocol(List<String> strings) {
            // TODO: use OpenSSL's algorithm which uses 2 lists
            System.out.println("CLIENT PROTOCOLS: " + protocols + " SERVER PROTOCOLS: " + strings);
            String selected = protocols.get(0);
            protocolSelected(selected);
            return selected;
        }
    }

    public static void deleteIfExists(File file) throws IOException {
        // okhttp-changed: was Libcore.os.remove() in a try/catch block
        file.delete();
    }

    public static void logW(String warning) {
        // okhttp-changed: was System.logw()
        System.out.println(warning);
    }

    public static int getEffectivePort(URI uri) {
        return getEffectivePort(uri.getScheme(), uri.getPort());
    }

    public static int getEffectivePort(URL url) {
        return getEffectivePort(url.getProtocol(), url.getPort());
    }

    private static int getEffectivePort(String scheme, int specifiedPort) {
        if (specifiedPort != -1) {
            return specifiedPort;
        }

        if ("http".equalsIgnoreCase(scheme)) {
            return 80;
        } else if ("https".equalsIgnoreCase(scheme)) {
            return 443;
        } else {
            return -1;
        }
    }

    public static void checkOffsetAndCount(int arrayLength, int offset, int count) {
        if ((offset | count) < 0 || offset > arrayLength || arrayLength - offset < count) {
            throw new ArrayIndexOutOfBoundsException();
        }
    }

    public static void tagSocket(Socket socket) {
    }

    public static void untagSocket(Socket socket) throws SocketException {
    }

    public static URI toUriLenient(URL url) throws URISyntaxException {
        return url.toURI(); // this isn't as good as the built-in toUriLenient
    }

    public static void pokeInt(byte[] dst, int offset, int value, ByteOrder order) {
        if (order == ByteOrder.BIG_ENDIAN) {
            dst[offset++] = (byte) ((value >> 24) & 0xff);
            dst[offset++] = (byte) ((value >> 16) & 0xff);
            dst[offset++] = (byte) ((value >>  8) & 0xff);
            dst[offset  ] = (byte) ((value >>  0) & 0xff);
        } else {
            dst[offset++] = (byte) ((value >>  0) & 0xff);
            dst[offset++] = (byte) ((value >>  8) & 0xff);
            dst[offset++] = (byte) ((value >> 16) & 0xff);
            dst[offset  ] = (byte) ((value >> 24) & 0xff);
        }
    }
}
