@SuppressWarnings("CheckStyle")
    @Override
    protected List<?>[] doInBackground(Byte... statsToLoad) {
      StatsListFragment fragment = mFragment.get();
      if (fragment == null) return null;
      MainActivity mainActivity = (MainActivity) fragment.getActivity();
      if (mainActivity == null) return null;
      MainActivity.waitForSaveThreads(new WeakReference<>(mainActivity));

      final byte toLoad = statsToLoad[0];
      Cursor cursor;
      int[][] statValues;
      List<String> listStatHeaders = new ArrayList<>();
      List<List<Pair<String, String>>> listStatNamesAndValues = new ArrayList<>();

      fragment.prepareListData(mainActivity, toLoad, listStatHeaders, listStatNamesAndValues);
      statValues = new int[listStatHeaders.size()][];
      for (int i = 0; i < statValues.length; i++)
        statValues[i] = new int[listStatNamesAndValues.get(i).size()];

      switch (toLoad) {
        case StatUtils.LOADING_BOWLER_STATS:
          fragment.mNumberOfGeneralDetails = 1;
          cursor = fragment.getBowlerOrLeagueCursor(false);
          break;
        case StatUtils.LOADING_LEAGUE_STATS:
          fragment.mNumberOfGeneralDetails = 2;
          listStatNamesAndValues
              .get(fragment.mStatsGeneral)
              .add(1, Pair.create("League/Event", mainActivity.getLeagueName().substring(1)));
          cursor = fragment.getBowlerOrLeagueCursor(true);
          break;
        case StatUtils.LOADING_SERIES_STATS:
          fragment.mNumberOfGeneralDetails = 3;
          listStatNamesAndValues
              .get(fragment.mStatsGeneral)
              .add(1, Pair.create("League/Event", mainActivity.getLeagueName().substring(1)));
          listStatNamesAndValues
              .get(fragment.mStatsGeneral)
              .add(2, Pair.create("Date", mainActivity.getSeriesDate()));
          cursor = fragment.getSeriesCursor();
          break;
        case StatUtils.LOADING_GAME_STATS:
          fragment.mNumberOfGeneralDetails = 4;
          listStatNamesAndValues
              .get(fragment.mStatsGeneral)
              .add(1, Pair.create("League/Event", mainActivity.getLeagueName().substring(1)));
          listStatNamesAndValues
              .get(fragment.mStatsGeneral)
              .add(2, Pair.create("Date", mainActivity.getSeriesDate()));
          listStatNamesAndValues
              .get(fragment.mStatsGeneral)
              .add(3, Pair.create("Game #", String.valueOf(mainActivity.getGameNumber())));
          cursor = fragment.getGameCursor();
          break;
        default:
          throw new IllegalArgumentException(
              "invalid value for toLoad: " + toLoad + ". must be between 0 and 3 (inclusive)");
      }

      /**
       * Passes through rows in cursor and updates stats which are affected as each frame is
       * analyzed
       */
      final byte numberOfGames =
          (toLoad >= StatUtils.LOADING_LEAGUE_STATS
              ? ((mainActivity.getLeagueName().substring(1).equals(Constants.NAME_OPEN_LEAGUE))
                  ? 5
                  : mainActivity.getDefaultNumberOfGames())
              : 20);
      int totalShotsAtMiddle = 0;
      int spareChances = 0;
      int seriesTotal = 0;
      int[] totalByGame = new int[numberOfGames];
      int[] countByGame = new int[numberOfGames];
      if (cursor.moveToFirst()) {
        while (!cursor.isAfterLast()) {
          byte frameNumber =
              (byte) cursor.getInt(cursor.getColumnIndex(Contract.FrameEntry.COLUMN_FRAME_NUMBER));
          if (toLoad != StatUtils.LOADING_GAME_STATS && frameNumber == 1) {
            short gameScore =
                cursor.getShort(cursor.getColumnIndex(Contract.GameEntry.COLUMN_SCORE));
            byte gameNumber =
                (byte) cursor.getInt(cursor.getColumnIndex(Contract.GameEntry.COLUMN_GAME_NUMBER));

            totalByGame[gameNumber - 1] += gameScore;
            countByGame[gameNumber - 1]++;

            byte matchResults =
                (byte) (cursor.getInt(cursor.getColumnIndex(Contract.GameEntry.COLUMN_MATCH_PLAY)));
            if (matchResults > 0) statValues[fragment.mStatsMatch][matchResults - 1]++;

            if (statValues[fragment.mStatsOverall][StatUtils.STAT_HIGH_SINGLE] < gameScore)
              statValues[fragment.mStatsOverall][StatUtils.STAT_HIGH_SINGLE] = gameScore;
            statValues[fragment.mStatsOverall][StatUtils.STAT_TOTAL_PINS] += gameScore;
            statValues[fragment.mStatsOverall][StatUtils.STAT_NUMBER_OF_GAMES]++;

            if (gameNumber == 1) {
              if (statValues[fragment.mStatsOverall][StatUtils.STAT_HIGH_SERIES] < seriesTotal)
                statValues[fragment.mStatsOverall][StatUtils.STAT_HIGH_SERIES] = seriesTotal;
              seriesTotal = gameScore;
            } else seriesTotal += gameScore;
          }

          boolean gameIsManual =
              (cursor.getInt(cursor.getColumnIndex(Contract.GameEntry.COLUMN_IS_MANUAL)) == 1);
          if (gameIsManual) {
            cursor.moveToNext();
            continue;
          }
          boolean frameAccessed =
              (cursor.getInt(cursor.getColumnIndex(Contract.FrameEntry.COLUMN_IS_ACCESSED)) == 1);
          if (toLoad == StatUtils.LOADING_GAME_STATS && !frameAccessed) break;

          String frameFouls =
              Score.foulIntToString(
                  cursor.getInt(cursor.getColumnIndex(Contract.FrameEntry.COLUMN_FOULS)));

          boolean[][] pinState = new boolean[3][5];
          for (byte i = 0; i < pinState.length; i++) {
            pinState[i] =
                Score.ballIntToBoolean(
                    cursor.getInt(cursor.getColumnIndex(Contract.FrameEntry.COLUMN_PIN_STATE[i])));
          }

          for (byte i = 1; i <= 3; i++) {
            if (frameFouls.contains(String.valueOf(i))) statValues[fragment.mStatsFouls][0]++;
          }

          if (frameNumber == Constants.NUMBER_OF_FRAMES) {
            totalShotsAtMiddle++;
            int ballValue = fragment.getFirstBallValue(pinState[0]);
            if (ballValue != -1) statValues[fragment.mStatsGeneral][StatUtils.STAT_MIDDLE_HIT]++;
            fragment.increaseFirstBallStat(ballValue, statValues, 0);
            if (ballValue < 5 && ballValue != Constants.BALL_VALUE_STRIKE) spareChances++;

            if (ballValue != 0) {
              if (Arrays.equals(pinState[1], Constants.FRAME_PINS_DOWN)) {
                statValues[fragment.mStatsGeneral][StatUtils.STAT_SPARE_CONVERSIONS]++;
                fragment.increaseFirstBallStat(ballValue, statValues, 1);

                if (ballValue >= 5) spareChances++;
              } else {
                statValues[fragment.mStatsPins][StatUtils.STAT_PINS_LEFT] +=
                    fragment.countPinsLeftStanding(pinState[2]);
              }
            } else {
              totalShotsAtMiddle++;
              ballValue = fragment.getFirstBallValue(pinState[1]);
              if (ballValue != -1) statValues[fragment.mStatsGeneral][StatUtils.STAT_MIDDLE_HIT]++;
              fragment.increaseFirstBallStat(ballValue, statValues, 0);

              if (ballValue != 0) {
                if (Arrays.equals(pinState[2], Constants.FRAME_PINS_DOWN)) {
                  statValues[fragment.mStatsGeneral][StatUtils.STAT_SPARE_CONVERSIONS]++;
                  fragment.increaseFirstBallStat(ballValue, statValues, 1);

                  if (ballValue >= 5) spareChances++;
                } else {
                  statValues[fragment.mStatsPins][StatUtils.STAT_PINS_LEFT] +=
                      fragment.countPinsLeftStanding(pinState[2]);
                }
              } else {
                totalShotsAtMiddle++;
                ballValue = fragment.getFirstBallValue(pinState[2]);
                if (ballValue != -1)
                  statValues[fragment.mStatsGeneral][StatUtils.STAT_MIDDLE_HIT]++;
                fragment.increaseFirstBallStat(ballValue, statValues, 0);

                if (ballValue != 0) {
                  statValues[fragment.mStatsPins][StatUtils.STAT_PINS_LEFT] +=
                      fragment.countPinsLeftStanding(pinState[2]);
                }
              }
            }
          } else {
            totalShotsAtMiddle++;
            int ballValue = fragment.getFirstBallValue(pinState[0]);
            if (ballValue != -1) statValues[fragment.mStatsGeneral][StatUtils.STAT_MIDDLE_HIT]++;
            fragment.increaseFirstBallStat(ballValue, statValues, 0);

            if (ballValue < 5 && ballValue != Constants.BALL_VALUE_STRIKE) spareChances++;

            if (ballValue != 0) {
              if (Arrays.equals(pinState[1], Constants.FRAME_PINS_DOWN)) {
                statValues[fragment.mStatsGeneral][StatUtils.STAT_SPARE_CONVERSIONS]++;
                fragment.increaseFirstBallStat(ballValue, statValues, 1);

                if (ballValue >= 5) spareChances++;
              } else {
                statValues[fragment.mStatsPins][StatUtils.STAT_PINS_LEFT] +=
                    fragment.countPinsLeftStanding(pinState[2]);
              }
            }
          }

          cursor.moveToNext();
        }
      }

      if (toLoad != StatUtils.LOADING_GAME_STATS) {
        if (statValues[fragment.mStatsOverall][StatUtils.STAT_HIGH_SERIES] < seriesTotal)
          statValues[fragment.mStatsOverall][StatUtils.STAT_HIGH_SERIES] = seriesTotal;

        if (toLoad != StatUtils.LOADING_SERIES_STATS) {
          for (byte i = 0; i < numberOfGames; i++)
            statValues[fragment.mStatsGameAverage][i] =
                (countByGame[i] > 0) ? totalByGame[i] / countByGame[i] : 0;
        }

        if (statValues[fragment.mStatsOverall][StatUtils.STAT_NUMBER_OF_GAMES] > 0) {
          statValues[fragment.mStatsOverall][StatUtils.STAT_AVERAGE] =
              statValues[fragment.mStatsOverall][StatUtils.STAT_TOTAL_PINS]
                  / statValues[fragment.mStatsOverall][StatUtils.STAT_NUMBER_OF_GAMES];
          statValues[fragment.mStatsPins][StatUtils.STAT_PINS_AVERAGE] =
              statValues[fragment.mStatsPins][StatUtils.STAT_PINS_LEFT]
                  / statValues[fragment.mStatsOverall][StatUtils.STAT_NUMBER_OF_GAMES];
        }
      }
      cursor.close();
      fragment.setGeneralAndDetailedStatValues(
          listStatNamesAndValues,
          statValues,
          totalShotsAtMiddle,
          spareChances,
          fragment.mNumberOfGeneralDetails,
          toLoad);

      return new List<?>[] {listStatHeaders, listStatNamesAndValues};
    }
  /**
   * Upgrades database from oldVersion 2 to newVersion 3.
   *
   * @param db to upgrade
   */
  @SuppressWarnings("CheckStyle")
  private void upgradeDatabaseFrom2To3(SQLiteDatabase db) {
    db.execSQL("DROP INDEX IF EXISTS frame_id_index");
    db.execSQL("DROP INDEX IF EXISTS frame_game_fk_index");

    db.execSQL(
        "CREATE TABLE frame2 ("
            + FrameEntry._ID
            + " INTEGER PRIMARY KEY, "
            + FrameEntry.COLUMN_FRAME_NUMBER
            + " INTEGER NOT NULL, "
            + FrameEntry.COLUMN_IS_ACCESSED
            + " INTEGER NOT NULL DEFAULT 0, "
            + FrameEntry.COLUMN_PIN_STATE[0]
            + " INTEGER NOT NULL DEFAULT 0, "
            + FrameEntry.COLUMN_PIN_STATE[1]
            + " INTEGER NOT NULL DEFAULT 0, "
            + FrameEntry.COLUMN_PIN_STATE[2]
            + " INTEGER NOT NULL DEFAULT 0, "
            + FrameEntry.COLUMN_FOULS
            + " INTEGER NOT NULL DEFAULT 0, "
            + FrameEntry.COLUMN_GAME_ID
            + " INTEGER NOT NULL"
            + " REFERENCES "
            + GameEntry.TABLE_NAME
            + " ON UPDATE CASCADE ON DELETE CASCADE, "
            + "CHECK ("
            + FrameEntry.COLUMN_FRAME_NUMBER
            + " >= 1 AND "
            + FrameEntry.COLUMN_FRAME_NUMBER
            + " <= 10), "
            + "CHECK ("
            + FrameEntry.COLUMN_IS_ACCESSED
            + " = 0 OR "
            + FrameEntry.COLUMN_IS_ACCESSED
            + " = 1)"
            + ");");
    db.execSQL(
        "INSERT INTO frame2 ("
            + FrameEntry._ID
            + ", "
            + FrameEntry.COLUMN_FRAME_NUMBER
            + ", "
            + FrameEntry.COLUMN_IS_ACCESSED
            + ", "
            + FrameEntry.COLUMN_PIN_STATE[0]
            + ", "
            + FrameEntry.COLUMN_PIN_STATE[1]
            + ", "
            + FrameEntry.COLUMN_PIN_STATE[2]
            + ", "
            + FrameEntry.COLUMN_FOULS
            + ", "
            + FrameEntry.COLUMN_GAME_ID
            + ")"
            + " SELECT "
            + FrameEntry._ID
            + ", "
            + FrameEntry.COLUMN_FRAME_NUMBER
            + ", "
            + FrameEntry.COLUMN_IS_ACCESSED
            + ", "
            + FrameEntry.COLUMN_PIN_STATE[0]
            + ", "
            + FrameEntry.COLUMN_PIN_STATE[1]
            + ", "
            + FrameEntry.COLUMN_PIN_STATE[2]
            + ", "
            + FrameEntry.COLUMN_FOULS
            + ", "
            + FrameEntry.COLUMN_GAME_ID
            + " FROM "
            + FrameEntry.TABLE_NAME);
    db.execSQL("DROP TABLE " + FrameEntry.TABLE_NAME);
    db.execSQL("ALTER TABLE frame2 RENAME TO " + FrameEntry.TABLE_NAME);

    db.execSQL(
        "CREATE INDEX frame_id_index ON " + FrameEntry.TABLE_NAME + "(" + FrameEntry._ID + ")");
    db.execSQL(
        "CREATE INDEX frame_game_fk_index ON "
            + FrameEntry.TABLE_NAME
            + "("
            + FrameEntry.COLUMN_GAME_ID
            + ")");

    try {
      db.beginTransaction();
      for (int i = 0; i < 32; i++) {
        for (int j = 0; j < 3; j++) {
          ContentValues values = new ContentValues();
          values.put(FrameEntry.COLUMN_PIN_STATE[j], i);
          db.update(
              FrameEntry.TABLE_NAME,
              values,
              FrameEntry.COLUMN_PIN_STATE[j] + "=?",
              new String[] {String.format("%5s", Integer.toBinaryString(i)).replace(' ', '0')});
        }
      }
      for (int i = 24; i < 31; i++) {
        ContentValues values = new ContentValues();
        values.put(FrameEntry.COLUMN_FOULS, i);
        db.update(
            FrameEntry.TABLE_NAME,
            values,
            FrameEntry.COLUMN_FOULS + "=?",
            new String[] {Score.foulIntToString(i)});
      }
      db.setTransactionSuccessful();
    } catch (Exception ex) {
      Log.e(TAG, "Error upgrading db from 2 to 3", ex);
      dropTablesAndRecreate(db);
    } finally {
      db.endTransaction();
    }
  }