// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.net.test;

import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.RemoteException;

import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.annotations.CalledByNative;
import org.chromium.base.annotations.JNINamespace;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.library_loader.LibraryProcessType;
import org.chromium.base.library_loader.ProcessInitException;
import org.chromium.base.test.util.UrlUtils;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Java bindings for running a net::test_server::EmbeddedTestServer.
 *
 * This should not be used directly. Use {@link EmbeddedTestServer} instead.
 */
@JNINamespace("net::test_server")
public class EmbeddedTestServerImpl extends IEmbeddedTestServerImpl.Stub {
    private static final String TAG = "cr_TestServer";

    private static AtomicInteger sCount = new AtomicInteger();

    private final Context mContext;
    private Handler mHandler;
    private HandlerThread mHandlerThread;
    private long mNativeEmbeddedTestServer;
    private IConnectionListener mConnectionListener;

    /** Create an uninitialized EmbeddedTestServer. */
    public EmbeddedTestServerImpl(Context context) {
        mContext = context;
    }

    private <V> V runOnHandlerThread(Callable<V> c) {
        FutureTask<V> t = new FutureTask<>(c);
        mHandler.post(t);
        try {
            return t.get();
        } catch (ExecutionException e) {
            Log.e(TAG, "Exception raised from native EmbeddedTestServer", e);
        } catch (InterruptedException e) {
            Log.e(TAG, "Interrupted while waiting for native EmbeddedTestServer", e);
        }
        return null;
    }

    /** Initialize the native EmbeddedTestServer object.
     *
     *  @param https True if the server should use HTTPS, and false otherwise.
     *  @return Whether the native object was successfully initialized.
     */
    @Override
    public boolean initializeNative(final boolean https) {
        // This is necessary as EmbeddedTestServerImpl is in a different process than the tests
        // using it, so it needs to initialize its own application context.
        ContextUtils.initApplicationContext(mContext.getApplicationContext());
        try {
            LibraryLoader.getInstance().ensureInitialized(LibraryProcessType.PROCESS_BROWSER);
        } catch (ProcessInitException e) {
            Log.e(TAG, "Failed to load native libraries.", e);
            return false;
        }

        mHandlerThread = new HandlerThread("EmbeddedTestServer" + sCount.getAndIncrement());
        mHandlerThread.start();
        mHandler = new Handler(mHandlerThread.getLooper());

        runOnHandlerThread(new Callable<Void>() {
            @Override
            public Void call() {
                if (mNativeEmbeddedTestServer == 0) {
                    nativeInit(UrlUtils.getIsolatedTestRoot(), https);
                }
                assert mNativeEmbeddedTestServer != 0;
                return null;
            }
        });
        return true;
    }

    /** Starts the server.
     *
     *  Note that this should be called after handlers are set up, including any relevant calls
     *  serveFilesFromDirectory.
     *
     *  @param port The port to use for the server, 0 to auto-select an unused port.
     *
     *  @return Whether the server was successfully started.
     */
    @Override
    public boolean start(int port) {
        return runOnHandlerThread(new Callable<Boolean>() {
            @Override
            public Boolean call() {
                return nativeStart(mNativeEmbeddedTestServer, port);
            }
        });
    }

    /** Returns the path to a PEM file containing the server's root certificate.
     *
     *  @return The path to a PEM file containing the server's root certificate.
     */
    @Override
    public String getRootCertPemPath() {
        return runOnHandlerThread(new Callable<String>() {
            @Override
            public String call() {
                return nativeGetRootCertPemPath(mNativeEmbeddedTestServer);
            }
        });
    }

    /** Add the default handlers and serve files from the provided directory relative to the
     *  external storage directory.
     *
     *  @param directoryPath The path of the directory from which files should be served, relative
     *      to the external storage directory.
     */
    @Override
    public void addDefaultHandlers(final String directoryPath) {
        runOnHandlerThread(new Callable<Void>() {
            @Override
            public Void call() {
                nativeAddDefaultHandlers(mNativeEmbeddedTestServer, directoryPath);
                return null;
            }
        });
    }

    /** Configure the server to use a particular type of SSL certificate.
     *
     * @param serverCertificate The type of certificate the server should use.
     */
    @Override
    public void setSSLConfig(final int serverCertificate) {
        runOnHandlerThread(new Callable<Void>() {
            @Override
            public Void call() {
                nativeSetSSLConfig(mNativeEmbeddedTestServer, serverCertificate);
                return null;
            }
        });
    }

    /** Register multiple request handlers.
     *  Handlers must be registered before starting the server.
     *
     *  @param handler The pointer of handler to be registered.
     */
    public void registerRequestHandler(final long handler) {
        runOnHandlerThread(new Callable<Void>() {
            @Override
            public Void call() {
                nativeRegisterRequestHandler(mNativeEmbeddedTestServer, handler);
                return null;
            }
        });
    }

    /** Serve files from the provided directory.
     *
     *  @param directoryPath The path of the directory from which files should be served.
     */
    @Override
    public void serveFilesFromDirectory(final String directoryPath) {
        runOnHandlerThread(new Callable<Void>() {
            @Override
            public Void call() {
                nativeServeFilesFromDirectory(mNativeEmbeddedTestServer, directoryPath);
                return null;
            }
        });
    }

    /** Sets a connection listener to be notified of new connections and socket reads.
     *
     * Must be done before starting the server. Setting a new one erases the previous one.
     *
     * @param listener Listener to notify.
     */
    @Override
    public void setConnectionListener(final IConnectionListener listener) {
        runOnHandlerThread(new Callable<Void>() {
            @Override
            public Void call() {
                mConnectionListener = listener;
                return null;
            }
        });
    }

    /** Get the full URL for the given relative URL.
     *
     *  @param relativeUrl The relative URL for which a full URL should be returned.
     *  @return The URL as a String.
     */
    @Override
    public String getURL(final String relativeUrl) {
        return runOnHandlerThread(new Callable<String>() {
            @Override
            public String call() {
                return nativeGetURL(mNativeEmbeddedTestServer, relativeUrl);
            }
        });
    }

    /** Get the full URL for the given relative URL. Similar to the above method but uses the given
     *  hostname instead of 127.0.0.1. The hostname should be resolved to 127.0.0.1.
     *
     *  @param hostName The host name which should be used.
     *  @param relativeUrl The relative URL for which a full URL should be returned.
     *  @return The URL as a String.
     */
    @Override
    public String getURLWithHostName(final String hostName, final String relativeUrl) {
        return runOnHandlerThread(new Callable<String>() {
            @Override
            public String call() {
                return nativeGetURLWithHostName(mNativeEmbeddedTestServer, hostName, relativeUrl);
            }
        });
    }

    /** Shut down the server.
     *
     *  @return Whether the server was successfully shut down.
     */
    @Override
    public boolean shutdownAndWaitUntilComplete() {
        return runOnHandlerThread(new Callable<Boolean>() {
            @Override
            public Boolean call() {
                return nativeShutdownAndWaitUntilComplete(mNativeEmbeddedTestServer);
            }
        });
    }

    /** Destroy the native EmbeddedTestServer object. */
    @Override
    public void destroy() {
        runOnHandlerThread(new Callable<Void>() {
            @Override
            public Void call() {
                assert mNativeEmbeddedTestServer != 0;
                nativeDestroy(mNativeEmbeddedTestServer);
                assert mNativeEmbeddedTestServer == 0;
                return null;
            }
        });

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            mHandlerThread.quitSafely();
        } else {
            runOnHandlerThread(new Callable<Void>() {
                @Override
                public Void call() {
                    mHandlerThread.quit();
                    return null;
                }
            });
        }

        try {
            mHandlerThread.join();
        } catch (InterruptedException e) {
        }
    }

    @CalledByNative
    private void acceptedSocket(long socketId) {
        if (mConnectionListener == null) return;
        try {
            mConnectionListener.acceptedSocket(socketId);
        } catch (RemoteException e) {
            // Callback, ignore exception.
        }
    }

    @CalledByNative
    private void readFromSocket(long socketId) {
        if (mConnectionListener == null) return;
        try {
            mConnectionListener.readFromSocket(socketId);
        } catch (RemoteException e) {
            // Callback, ignore exception.
        }
    }

    @CalledByNative
    private void setNativePtr(long nativePtr) {
        assert mNativeEmbeddedTestServer == 0;
        mNativeEmbeddedTestServer = nativePtr;
    }

    @CalledByNative
    private void clearNativePtr() {
        assert mNativeEmbeddedTestServer != 0;
        mNativeEmbeddedTestServer = 0;
    }

    private native void nativeInit(String testDataDir, boolean https);
    private native void nativeDestroy(long nativeEmbeddedTestServerAndroid);
    private native boolean nativeStart(long nativeEmbeddedTestServerAndroid, int port);
    private native String nativeGetRootCertPemPath(long nativeEmbeddedTestServerAndroid);
    private native boolean nativeShutdownAndWaitUntilComplete(long nativeEmbeddedTestServerAndroid);
    private native void nativeAddDefaultHandlers(
            long nativeEmbeddedTestServerAndroid, String directoryPath);
    private native void nativeSetSSLConfig(
            long nativeEmbeddedTestServerAndroid, int serverCertificate);
    private native void nativeRegisterRequestHandler(
            long nativeEmbeddedTestServerAndroid, long handler);
    private native String nativeGetURL(long nativeEmbeddedTestServerAndroid, String relativeUrl);
    private native String nativeGetURLWithHostName(
            long nativeEmbeddedTestServerAndroid, String hostName, String relativeUrl);
    private native void nativeServeFilesFromDirectory(
            long nativeEmbeddedTestServerAndroid, String directoryPath);
}
