/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed 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.
 */

package com.googlecode.android_scripting;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.preference.PreferenceManager;

import com.googlecode.android_scripting.exception.Sl4aException;
import com.googlecode.android_scripting.interpreter.InterpreterConstants;
import com.googlecode.android_scripting.interpreter.InterpreterDescriptor;
import com.googlecode.android_scripting.interpreter.InterpreterUtils;

import java.io.File;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

/**
 * AsyncTask for installing interpreters.
 *
 */
public abstract class InterpreterInstaller extends AsyncTask<Void, Void, Boolean> {

  protected final InterpreterDescriptor mDescriptor;
  protected final AsyncTaskListener<Boolean> mTaskListener;
  protected final Queue<RequestCode> mTaskQueue;
  protected final Context mContext;

  protected final Handler mainThreadHandler;
  protected Handler mBackgroundHandler;

  protected volatile AsyncTask<Void, Integer, Long> mTaskHolder;

  protected final String mInterpreterRoot;

  protected static enum RequestCode {
    DOWNLOAD_INTERPRETER, DOWNLOAD_INTERPRETER_EXTRAS, DOWNLOAD_SCRIPTS, EXTRACT_INTERPRETER,
    EXTRACT_INTERPRETER_EXTRAS, EXTRACT_SCRIPTS
  }

  // Executed in the UI thread.
  private final Runnable mTaskStarter = new Runnable() {
    @Override
    public void run() {
      RequestCode task = mTaskQueue.peek();
      try {
        AsyncTask<Void, Integer, Long> newTask = null;
        switch (task) {
        case DOWNLOAD_INTERPRETER:
          newTask = downloadInterpreter();
          break;
        case DOWNLOAD_INTERPRETER_EXTRAS:
          newTask = downloadInterpreterExtras();
          break;
        case DOWNLOAD_SCRIPTS:
          newTask = downloadScripts();
          break;
        case EXTRACT_INTERPRETER:
          newTask = extractInterpreter();
          break;
        case EXTRACT_INTERPRETER_EXTRAS:
          newTask = extractInterpreterExtras();
          break;
        case EXTRACT_SCRIPTS:
          newTask = extractScripts();
          break;
        }
        mTaskHolder = newTask.execute();
      } catch (Exception e) {
        Log.v(e.getMessage(), e);
      }

      if (mBackgroundHandler != null) {
        mBackgroundHandler.post(mTaskWorker);
      }
    }
  };

  // Executed in the background.
  private final Runnable mTaskWorker = new Runnable() {
    @Override
    public void run() {
      RequestCode request = mTaskQueue.peek();
      try {
        if (mTaskHolder != null && mTaskHolder.get() != null) {
          mTaskQueue.remove();
          mTaskHolder = null;
          // Post processing.
          if (request == RequestCode.EXTRACT_INTERPRETER && !chmodIntepreter()) {
            // Chmod returned false.
            Looper.myLooper().quit();
          } else if (mTaskQueue.size() == 0) {
            // We're done here.
            Looper.myLooper().quit();
            return;
          } else if (mainThreadHandler != null) {
            // There's still some work to do.
            mainThreadHandler.post(mTaskStarter);
            return;
          }
        }
      } catch (Exception e) {
        Log.e(e);
      }
      // Something went wrong...
      switch (request) {
      case DOWNLOAD_INTERPRETER:
        Log.e("Downloading interpreter failed.");
        break;
      case DOWNLOAD_INTERPRETER_EXTRAS:
        Log.e("Downloading interpreter extras failed.");
        break;
      case DOWNLOAD_SCRIPTS:
        Log.e("Downloading scripts failed.");
        break;
      case EXTRACT_INTERPRETER:
        Log.e("Extracting interpreter failed.");
        break;
      case EXTRACT_INTERPRETER_EXTRAS:
        Log.e("Extracting interpreter extras failed.");
        break;
      case EXTRACT_SCRIPTS:
        Log.e("Extracting scripts failed.");
        break;
      }
      Looper.myLooper().quit();
    }
  };

  // TODO(Alexey): Add Javadoc.
  public InterpreterInstaller(InterpreterDescriptor descriptor, Context context,
      AsyncTaskListener<Boolean> taskListener) throws Sl4aException {
    super();
    mDescriptor = descriptor;
    mContext = context;
    mTaskListener = taskListener;
    mainThreadHandler = new Handler();
    mTaskQueue = new LinkedList<RequestCode>();

    String packageName = mDescriptor.getClass().getPackage().getName();

    if (packageName.length() == 0) {
      throw new Sl4aException("Interpreter package name is empty.");
    }

    mInterpreterRoot = InterpreterConstants.SDCARD_ROOT + packageName;

    if (mDescriptor == null) {
      throw new Sl4aException("Interpreter description not provided.");
    }
    if (mDescriptor.getName() == null) {
      throw new Sl4aException("Interpreter not specified.");
    }
    if (isInstalled()) {
      throw new Sl4aException("Interpreter is installed.");
    }

    if (mDescriptor.hasInterpreterArchive()) {
      mTaskQueue.offer(RequestCode.DOWNLOAD_INTERPRETER);
      mTaskQueue.offer(RequestCode.EXTRACT_INTERPRETER);
    }
    if (mDescriptor.hasExtrasArchive()) {
      mTaskQueue.offer(RequestCode.DOWNLOAD_INTERPRETER_EXTRAS);
      mTaskQueue.offer(RequestCode.EXTRACT_INTERPRETER_EXTRAS);
    }
    if (mDescriptor.hasScriptsArchive()) {
      mTaskQueue.offer(RequestCode.DOWNLOAD_SCRIPTS);
      mTaskQueue.offer(RequestCode.EXTRACT_SCRIPTS);
    }
  }

  @Override
  protected Boolean doInBackground(Void... params) {
    new Thread(new Runnable() {
      @Override
      public void run() {
        executeInBackground();
        final boolean result = (mTaskQueue.size() == 0);
        mainThreadHandler.post(new Runnable() {
          @Override
          public void run() {
            finish(result);
          }
        });
      }
    }).start();
    return true;
  }

  private boolean executeInBackground() {

    File root = new File(mInterpreterRoot);
    if (root.exists()) {
      FileUtils.delete(root);
    }
    if (!root.mkdirs()) {
      Log.e("Failed to make directories: " + root.getAbsolutePath());
      return false;
    }

    if (Looper.myLooper() == null) {
      Looper.prepare();
    }
    mBackgroundHandler = new Handler(Looper.myLooper());
    mainThreadHandler.post(mTaskStarter);
    Looper.loop();
    // Have we executed all the tasks?
    return (mTaskQueue.size() == 0);
  }

  protected void finish(boolean result) {
    if (result && setup()) {
      mTaskListener.onTaskFinished(true, "Installation successful.");
    } else {
      if (mTaskHolder != null) {
        mTaskHolder.cancel(true);
      }
      cleanup();
      mTaskListener.onTaskFinished(false, "Installation failed.");
    }
  }

  protected AsyncTask<Void, Integer, Long> download(String in) throws MalformedURLException {
    String out = mInterpreterRoot;
    return new UrlDownloaderTask(in, out, mContext);
  }

  protected AsyncTask<Void, Integer, Long> downloadInterpreter() throws MalformedURLException {
    return download(mDescriptor.getInterpreterArchiveUrl());
  }

  protected AsyncTask<Void, Integer, Long> downloadInterpreterExtras() throws MalformedURLException {
    return download(mDescriptor.getExtrasArchiveUrl());
  }

  protected AsyncTask<Void, Integer, Long> downloadScripts() throws MalformedURLException {
    return download(mDescriptor.getScriptsArchiveUrl());
  }

  protected AsyncTask<Void, Integer, Long> extract(String in, String out, boolean replaceAll)
      throws Sl4aException {
    return new ZipExtractorTask(in, out, mContext, replaceAll);
  }

  protected AsyncTask<Void, Integer, Long> extractInterpreter() throws Sl4aException {
    String in =
        new File(mInterpreterRoot, mDescriptor.getInterpreterArchiveName()).getAbsolutePath();
    String out = InterpreterUtils.getInterpreterRoot(mContext).getAbsolutePath();
    return extract(in, out, true);
  }

  protected AsyncTask<Void, Integer, Long> extractInterpreterExtras() throws Sl4aException {
    String in = new File(mInterpreterRoot, mDescriptor.getExtrasArchiveName()).getAbsolutePath();
    String out = mInterpreterRoot + InterpreterConstants.INTERPRETER_EXTRAS_ROOT;
    return extract(in, out, true);
  }

  protected AsyncTask<Void, Integer, Long> extractScripts() throws Sl4aException {
    String in = new File(mInterpreterRoot, mDescriptor.getScriptsArchiveName()).getAbsolutePath();
    String out = InterpreterConstants.SCRIPTS_ROOT;
    return extract(in, out, false);
  }

  protected boolean chmodIntepreter() {
    int dataChmodErrno;
    boolean interpreterChmodSuccess;
    try {
      dataChmodErrno = FileUtils.chmod(InterpreterUtils.getInterpreterRoot(mContext), 0755);
      interpreterChmodSuccess =
          FileUtils.recursiveChmod(InterpreterUtils.getInterpreterRoot(mContext, mDescriptor
              .getName()), 0755);
    } catch (Exception e) {
      Log.e(e);
      return false;
    }
    return dataChmodErrno == 0 && interpreterChmodSuccess;
  }

  protected boolean isInstalled() {
    SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
    return preferences.getBoolean(InterpreterConstants.INSTALLED_PREFERENCE_KEY, false);
  }

  private void cleanup() {
    List<File> directories = new ArrayList<File>();

    directories.add(new File(mInterpreterRoot));

    if (mDescriptor.hasInterpreterArchive()) {
      if (!mTaskQueue.contains(RequestCode.EXTRACT_INTERPRETER)) {
        directories.add(InterpreterUtils.getInterpreterRoot(mContext, mDescriptor.getName()));
      }
    }

    for (File directory : directories) {
      FileUtils.delete(directory);
    }
  }

  protected abstract boolean setup();
}