package com.insidesecure.drm.android.reference;

import java.io.IOException;
import java.util.concurrent.*;

import android.annotation.SuppressLint;
import android.os.*;
import android.util.Log;

/**
 * @author
 */
public final class Loader {

  /**
   * Thrown when an unexpected exception is encountered during loading.
   */
  public static final class UnexpectedLoaderException extends IOException
  {

    public UnexpectedLoaderException(Exception cause) {
      super("Unexpected " + cause.getClass().getSimpleName() + ": " + cause.getMessage(), cause);
    }

  }

  /**
   * Interface definition of an object that can be loaded using a {@link com.insidesecure.drm.android.reference.Loader}.
   */
  public interface Loadable {

    /**
     * Cancels the load.
     */
    void cancelLoad();

    /**
     * Whether the load has been canceled.
     *
     * @return True if the load has been canceled. False otherwise.
     */
    boolean isLoadCanceled();

    /**
     * Performs the load, returning on completion or cancelation.
     *
     * @throws java.io.IOException
     * @throws InterruptedException
     */
    void load() throws IOException, InterruptedException;

  }

  /**
   * Interface definition for a callback to be notified of {@link com.insidesecure.drm.android.reference.Loader} events.
   */
  public interface Callback {

    /**
     * Invoked when loading has been canceled.
     *
     * @param loadable The loadable whose load has been canceled.
     */
    void onLoadCanceled(Loadable loadable);

    /**
     * Invoked when the data source has been fully loaded.
     *
     * @param loadable The loadable whose load has completed.
     */
    void onLoadCompleted(Loadable loadable);

    /**
     * Invoked when the data source is stopped due to an error.
     *
     * @param loadable The loadable whose load has failed.
     */
    void onLoadError(Loadable loadable, IOException exception);

  }

  private static final int MSG_END_OF_SOURCE = 0;
  private static final int MSG_ERROR = 1;

  private final ExecutorService downloadExecutorService;

  private LoadTask currentTask;
  private boolean loading;

  /**
   * @param threadName A name for the loader's thread.
   */
  public Loader(final String threadName) {
    this.downloadExecutorService = Executors.newSingleThreadExecutor(new ThreadFactory()
        {
            @Override
            public Thread newThread(Runnable r)
            {
                return new Thread(r, threadName);
            }
        });
  }

  /**
   * Invokes {@link #startLoading(android.os.Looper, Loadable, Callback)}, using the {@link android.os.Looper}
   * associated with the calling thread.
   *
   * @param loadable The {@link Loadable} to load.
   * @param callback A callback to invoke when the load ends.
   * @throws IllegalStateException If the calling thread does not have an associated {@link android.os.Looper}.
   */
  public void startLoading(Loadable loadable, Callback callback) {
    Looper myLooper = Looper.myLooper();
    Assertions.checkState(myLooper != null);
    startLoading(myLooper, loadable, callback);
  }

  /**
   * Start loading a {@link Loadable}.
   * <p>
   * A {@link com.insidesecure.drm.android.reference.Loader} instance can only load one {@link Loadable} at a time, and so this method
   * must not be called when another load is in progress.
   *
   * @param looper The looper of the thread on which the callback should be invoked.
   * @param loadable The {@link Loadable} to load.
   * @param callback A callback to invoke when the load ends.
   */
  public void startLoading(Looper looper, Loadable loadable, Callback callback) {
    Assertions.checkState(!loading);
    loading = true;
    currentTask = new LoadTask(looper, loadable, callback);
    downloadExecutorService.submit(currentTask);
  }

  /**
   * Whether the {@link com.insidesecure.drm.android.reference.Loader} is currently loading a {@link Loadable}.
   *
   * @return Whether the {@link com.insidesecure.drm.android.reference.Loader} is currently loading a {@link Loadable}.
   */
  public boolean isLoading() {
    return loading;
  }

  /**
   * Cancels the current load.
   * <p>
   * This method should only be called when a load is in progress.
   */
  public void cancelLoading() {
    Assertions.checkState(loading);
    currentTask.quit();
  }

  /**
   * Releases the {@link com.insidesecure.drm.android.reference.Loader}.
   * <p>
   * This method should be called when the {@link com.insidesecure.drm.android.reference.Loader} is no longer required.
   */
  public void release() {
    if (loading) {
      cancelLoading();
    }
    downloadExecutorService.shutdown();
  }

  @SuppressLint("HandlerLeak")
  private final class LoadTask extends Handler implements Runnable {

    private static final String TAG = "LoadTask";

    private final Loadable loadable;
    private final Loader.Callback callback;

    private volatile Thread executorThread;

    public LoadTask(Looper looper, Loadable loadable, Loader.Callback callback) {
      super(looper);
      this.loadable = loadable;
      this.callback = callback;
    }

    public void quit() {
      loadable.cancelLoad();
      if (executorThread != null) {
        executorThread.interrupt();
      }
    }

    @Override
    public void run() {
      try {
        executorThread = Thread.currentThread();
        if (!loadable.isLoadCanceled()) {
          loadable.load();
        }
        sendEmptyMessage(MSG_END_OF_SOURCE);
      } catch (IOException e) {
        obtainMessage(MSG_ERROR, e).sendToTarget();
      } catch (InterruptedException e) {
        // The load was canceled.
        Assertions.checkState(loadable.isLoadCanceled());
        sendEmptyMessage(MSG_END_OF_SOURCE);
      } catch (Exception e) {
        // This should never happen, but handle it anyway.
        Log.e(TAG, "Unexpected error loading stream", e);
        obtainMessage(MSG_ERROR, new UnexpectedLoaderException(e)).sendToTarget();
      }
    }

    @Override
    public void handleMessage(Message msg) {
      onFinished();
      if (loadable.isLoadCanceled()) {
        callback.onLoadCanceled(loadable);
        return;
      }
      switch (msg.what) {
        case MSG_END_OF_SOURCE:
          callback.onLoadCompleted(loadable);
          break;
        case MSG_ERROR:
          callback.onLoadError(loadable, (IOException) msg.obj);
          break;
      }
    }

    private void onFinished() {
      loading = false;
      currentTask = null;
    }

  }

}