/*   This file is part of My Expenses.
 *   My Expenses is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   My Expenses is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with My Expenses.  If not, see <http://www.gnu.org/licenses/>.
 */
//based on http://www.androiddesignpatterns.com/2013/04/retaining-objects-across-config-changes.html

package org.totschnig.myexpenses.task;


import java.io.Serializable;
import java.util.ArrayList;

import org.apache.commons.csv.CSVRecord;
import org.totschnig.myexpenses.MyApplication;
import org.totschnig.myexpenses.export.qif.QifDateFormat;
import org.totschnig.myexpenses.fragment.CsvImportDataFragment;
import org.totschnig.myexpenses.model.Account;
import org.totschnig.myexpenses.provider.DatabaseConstants;
import org.totschnig.myexpenses.util.SparseBooleanArrayParcelable;
import org.totschnig.myexpenses.util.Utils;

import android.app.Activity;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;

/**
 * This Fragment manages a single background task and retains itself across
 * configuration changes. It handles several task that each operate on a single
 * db object identified by its row id
 */
public class TaskExecutionFragment<T> extends Fragment {
  private static final String KEY_EXTRA = "extra";
  public static final String KEY_OBJECT_IDS = "objectIds";
  private static final String KEY_RUNNING = "running";
  private static final String KEY_TASKID = "taskId";
  public static final String KEY_WITH_PARTIES = "withParties";
  public static final String KEY_WITH_CATEGORIES = "withCategories";
  public static final String KEY_WITH_TRANSACTIONS = "withTransactions";
  public static final String KEY_FILE_PATH = "filePath";
  public static final String KEY_EXTERNAL = "external";
  public static final String KEY_DATE_FORMAT = "dateFormat";
  public static final String KEY_ENCODING = "encoding";
  public static final String KEY_FORMAT = "format";
  public static final String KEY_DELIMITER = "delimiter";

  //public static final int TASK_CLONE = 1;
  public static final int TASK_INSTANTIATE_TRANSACTION = 2;
  public static final int TASK_INSTANTIATE_TEMPLATE = 3;
  public static final int TASK_INSTANTIATE_TRANSACTION_FROM_TEMPLATE = 4;
  public static final int TASK_REQUIRE_ACCOUNT = 5;
  public static final int TASK_DELETE_TRANSACTION = 6;
  public static final int TASK_DELETE_ACCOUNT = 7;
  public static final int TASK_DELETE_PAYMENT_METHODS = 8;
  public static final int TASK_DELETE_PAYEES = 9;
  public static final int TASK_DELETE_TEMPLATES = 10;
  public static final int TASK_TOGGLE_CRSTATUS = 11;
  public static final int TASK_MOVE = 12;
  public static final int TASK_NEW_FROM_TEMPLATE = 13;
  public static final int TASK_DELETE_CATEGORY = 14;
  public static final int TASK_INSTANTIATE_PLAN = 15;
  public static final int TASK_CANCEL_PLAN_INSTANCE = 17;
  public static final int TASK_RESET_PLAN_INSTANCE = 18;
  public static final int TASK_GRISBI_IMPORT = 19;
  public static final int TASK_QIF_IMPORT = 20;
  public static final int TASK_EXPORT = 21;
  public static final int TASK_BACKUP = 22;
  public static final int TASK_RESTORE = 23;
  public static final int TASK_BALANCE = 24;
  public static final int TASK_PRINT = 25;
  /**
   * same as {@link TaskExecutionFragment#TASK_INSTANTIATE_TRANSACTION}
   * but
   * * does not prepare split transactions for edit
   * * allows Activity to define a second callback
   */
  public static final int TASK_INSTANTIATE_TRANSACTION_2 = 26;
  public static final int TASK_UPDATE_SORT_KEY = 27;
  public static final int TASK_CHANGE_FRACTION_DIGITS = 28;
  public static final int TASK_TOGGLE_EXCLUDE_FROM_TOTALS = 29;
  public static final int TASK_SPLIT = 30;
  //public static final int TASK_HAS_STALE_IMAGES = 31;
  public static final int TASK_DELETE_IMAGES = 32;
  public static final int TASK_SAVE_IMAGES = 33;
  public static final int TASK_UNDELETE_TRANSACTION = 34;
  public static final int TASK_EXPORT_CATEGRIES = 35;
  public static final int TASK_CSV_PARSE = 36;
  public static final int TASK_CSV_IMPORT = 37;
  public static final int TASK_MOVE_CATEGORY = 38;
  public static final int TASK_SWAP_SORT_KEY = 39;
  public static final int TASK_MOVE_UNCOMMITED_SPLIT_PARTS = 40;


  /**
   * Callback interface through which the fragment will report the task's
   * progress and results back to the Activity.
   */
  public static interface TaskCallbacks {
    void onPreExecute();

    void onProgressUpdate(Object progress);

    void onCancelled();

    /**
     * @param taskId with which TaskExecutionFragment was created
     * @param o      object that the activity expects from the task, for example an
     *               instantiated DAO
     */
    void onPostExecute(int taskId, Object o);
  }

  TaskCallbacks mCallbacks;

  public static <T> TaskExecutionFragment newInstance(int taskId, T[] objectIds,
                                                      Serializable extra) {
    TaskExecutionFragment<T> f = new TaskExecutionFragment<>();
    Bundle bundle = new Bundle();
    bundle.putInt(KEY_TASKID, taskId);
    if (objectIds != null) {
      //TODO would be safer to use putLongArray/putStringArray
      bundle.putSerializable(KEY_OBJECT_IDS, objectIds);
    }
    if (extra != null) {
      bundle.putSerializable(KEY_EXTRA, extra);
    }
    f.setArguments(bundle);
    return f;
  }

  public static TaskExecutionFragment newInstanceGrisbiImport(boolean external,
                                                              Uri mUri, boolean withCategories, boolean withParties) {
    TaskExecutionFragment f = new TaskExecutionFragment();
    Bundle bundle = new Bundle();
    bundle.putInt(KEY_TASKID, TASK_GRISBI_IMPORT);
    bundle.putBoolean(KEY_EXTERNAL, external);
    bundle.putParcelable(KEY_FILE_PATH, mUri);
    bundle.putBoolean(KEY_WITH_PARTIES, withParties);
    bundle.putBoolean(KEY_WITH_CATEGORIES, withCategories);
    f.setArguments(bundle);
    return f;
  }

  public static TaskExecutionFragment newInstanceQifImport(
      Uri mUri, QifDateFormat qifDateFormat, long accountId, String currency,
      boolean withTransactions, boolean withCategories, boolean withParties,
      String encoding) {
    TaskExecutionFragment f = new TaskExecutionFragment();
    Bundle bundle = new Bundle();
    bundle.putInt(KEY_TASKID, TASK_QIF_IMPORT);
    bundle.putParcelable(KEY_FILE_PATH, mUri);
    bundle.putSerializable(KEY_DATE_FORMAT, qifDateFormat);
    bundle.putLong(DatabaseConstants.KEY_ACCOUNTID, accountId);
    bundle.putString(DatabaseConstants.KEY_CURRENCY, currency);
    bundle.putBoolean(KEY_WITH_TRANSACTIONS, withTransactions);
    bundle.putBoolean(KEY_WITH_PARTIES, withParties);
    bundle.putBoolean(KEY_WITH_CATEGORIES, withCategories);
    bundle.putString(KEY_ENCODING, encoding);
    f.setArguments(bundle);
    return f;
  }

  public static TaskExecutionFragment newInstanceCSVParse(
      Uri mUri, char delimiter, String encoding) {
    TaskExecutionFragment f = new TaskExecutionFragment();
    Bundle bundle = new Bundle();
    bundle.putInt(KEY_TASKID, TASK_CSV_PARSE);
    bundle.putParcelable(KEY_FILE_PATH, mUri);
    bundle.putChar(KEY_DELIMITER, delimiter);
    bundle.putString(KEY_ENCODING, encoding);
    f.setArguments(bundle);
    return f;
  }

  public static TaskExecutionFragment newInstanceCSVImport(
      ArrayList<CSVRecord> data,
      int[] fieldToColumnMap,
      SparseBooleanArrayParcelable discardedRows,
      QifDateFormat qifDateFormat,
      long accountId, String currency, Account.Type type) {
    TaskExecutionFragment f = new TaskExecutionFragment();
    Bundle bundle = new Bundle();
    bundle.putInt(KEY_TASKID, TASK_CSV_IMPORT);
    bundle.putSerializable(CsvImportDataFragment.KEY_DATASET, data);
    bundle.putSerializable(CsvImportDataFragment.KEY_FIELD_TO_COLUMN, fieldToColumnMap);
    bundle.putParcelable(CsvImportDataFragment.KEY_DISCARDED_ROWS, discardedRows);
    bundle.putLong(DatabaseConstants.KEY_ACCOUNTID, accountId);
    bundle.putString(DatabaseConstants.KEY_CURRENCY, currency);
    bundle.putSerializable(KEY_DATE_FORMAT, qifDateFormat);
    bundle.putSerializable(DatabaseConstants.KEY_TYPE,type);
    f.setArguments(bundle);
    return f;
  }

  public static TaskExecutionFragment newInstanceExport(Bundle b) {
    TaskExecutionFragment f = new TaskExecutionFragment();
    b.putInt(KEY_TASKID, TASK_EXPORT);
    f.setArguments(b);
    return f;
  }

  public static TaskExecutionFragment newInstanceRestore(Bundle b) {
    TaskExecutionFragment f = new TaskExecutionFragment();
    b.putInt(KEY_TASKID, TASK_RESTORE);
    f.setArguments(b);
    return f;
  }

  public static TaskExecutionFragment newInstancePrint(Bundle b) {
    TaskExecutionFragment f = new TaskExecutionFragment();
    b.putInt(KEY_TASKID, TASK_PRINT);
    f.setArguments(b);
    return f;
  }

  /**
   * Hold a reference to the parent Activity so we can report the task's current
   * progress and results. The Android framework will pass us a reference to the
   * newly created Activity after each configuration change.
   */
  @Override
  public void onAttach(Activity activity) {
    super.onAttach(activity);
    mCallbacks = (TaskCallbacks) activity;
  }

  @Override
  public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putBoolean(KEY_RUNNING, true);
  }

  /**
   * This method will only be called once when the retained Fragment is first
   * created.
   */
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (savedInstanceState != null
        && savedInstanceState.getBoolean(KEY_RUNNING, true)) {
      // if we are recreated, prevent the task from being executed twice
      return;
    }
    // Retain this fragment across configuration changes.
    setRetainInstance(true);

    // Create and execute the background task.
    Bundle args = getArguments();
    int taskId = args.getInt(KEY_TASKID);
    Log.i(MyApplication.TAG, "TaskExecutionFragment created for task " + taskId +
        " with objects: " + Utils.printDebug((T[]) args.getSerializable(KEY_OBJECT_IDS)));
    try {
      switch (taskId) {
        case TASK_GRISBI_IMPORT:
          new GrisbiImportTask(this, args).execute();
          break;
        case TASK_QIF_IMPORT:
          new QifImportTask(this, args).execute();
          break;
        case TASK_CSV_PARSE:
          new CsvParseTask(this, args).execute();
          break;
        case TASK_CSV_IMPORT:
          new CsvImportTask(this, args).execute();
          break;
        case TASK_EXPORT:
          new ExportTask(this, args).execute();
          break;
        case TASK_RESTORE:
          new RestoreTask(this, args).execute();
          break;
        case TASK_PRINT:
          new PrintTask(this, args).execute();
          break;
        default:
          new GenericTask<T>(this, taskId, args.getSerializable(KEY_EXTRA))
              .execute((T[]) args.getSerializable(KEY_OBJECT_IDS));
      }
    } catch (ClassCastException e) {
      // the cast could fail, if Fragment is recreated,
      // but we are cancelling above in that case
    }
  }

  /**
   * Set the callback to null so we don't accidentally leak the Activity
   * instance.
   */
  @Override
  public void onDetach() {
    super.onDetach();
    mCallbacks = null;
  }
}