/*
 * Copyright (C) 2015 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.android.voicemail.impl.sync;

import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.VoicemailContract;
import android.provider.VoicemailContract.Voicemails;
import android.support.annotation.NonNull;
import android.telecom.PhoneAccountHandle;
import com.android.dialer.common.Assert;
import com.android.voicemail.impl.Voicemail;
import java.util.ArrayList;
import java.util.List;

/** Construct queries to interact with the voicemails table. */
public class VoicemailsQueryHelper {
  static final String[] PROJECTION =
      new String[] {
        Voicemails._ID, // 0
        Voicemails.SOURCE_DATA, // 1
        Voicemails.IS_READ, // 2
        Voicemails.DELETED, // 3
        Voicemails.TRANSCRIPTION // 4
      };

  public static final int _ID = 0;
  public static final int SOURCE_DATA = 1;
  public static final int IS_READ = 2;
  public static final int DELETED = 3;
  public static final int TRANSCRIPTION = 4;

  static final String DELETED_SELECTION = Voicemails.DELETED + "=1";
  static final String ARCHIVED_SELECTION = Voicemails.ARCHIVED + "=0";

  private Context context;
  private ContentResolver contentResolver;
  private Uri sourceUri;

  public VoicemailsQueryHelper(Context context) {
    this.context = context;
    contentResolver = context.getContentResolver();
    sourceUri = VoicemailContract.Voicemails.buildSourceUri(this.context.getPackageName());
  }

  /**
   * Get all the locally deleted voicemails that have not been synced to the server.
   *
   * @return A list of deleted voicemails.
   */
  public List<Voicemail> getDeletedVoicemails(@NonNull PhoneAccountHandle phoneAccountHandle) {
    return getLocalVoicemails(phoneAccountHandle, DELETED_SELECTION);
  }

  /**
   * Get all voicemails locally stored.
   *
   * @return A list of all locally stored voicemails.
   */
  public List<Voicemail> getAllVoicemails(@NonNull PhoneAccountHandle phoneAccountHandle) {
    return getLocalVoicemails(phoneAccountHandle, null);
  }

  /**
   * Utility method to make queries to the voicemail database.
   *
   * <p>TODO(a bug) add PhoneAccountHandle filtering back
   *
   * @param selection A filter declaring which rows to return. {@code null} returns all rows.
   * @return A list of voicemails according to the selection statement.
   */
  private List<Voicemail> getLocalVoicemails(
      @NonNull PhoneAccountHandle unusedPhoneAccountHandle, String selection) {
    Cursor cursor = contentResolver.query(sourceUri, PROJECTION, selection, null, null);
    if (cursor == null) {
      return null;
    }
    try {
      List<Voicemail> voicemails = new ArrayList<Voicemail>();
      while (cursor.moveToNext()) {
        final long id = cursor.getLong(_ID);
        final String sourceData = cursor.getString(SOURCE_DATA);
        final boolean isRead = cursor.getInt(IS_READ) == 1;
        final String transcription = cursor.getString(TRANSCRIPTION);
        Voicemail voicemail =
            Voicemail.createForUpdate(id, sourceData)
                .setIsRead(isRead)
                .setTranscription(transcription)
                .build();
        voicemails.add(voicemail);
      }
      return voicemails;
    } finally {
      cursor.close();
    }
  }

  /**
   * Deletes a list of voicemails from the voicemail content provider.
   *
   * @param voicemails The list of voicemails to delete
   * @return The number of voicemails deleted
   */
  public int deleteFromDatabase(List<Voicemail> voicemails) {
    int count = voicemails.size();
    if (count == 0) {
      return 0;
    }

    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < count; i++) {
      if (i > 0) {
        sb.append(",");
      }
      sb.append(voicemails.get(i).getId());
    }

    String selectionStatement = String.format(Voicemails._ID + " IN (%s)", sb.toString());
    return contentResolver.delete(Voicemails.CONTENT_URI, selectionStatement, null);
  }

  /** Utility method to delete a single voicemail that is not archived. */
  public void deleteNonArchivedFromDatabase(Voicemail voicemail) {
    contentResolver.delete(
        Voicemails.CONTENT_URI,
        Voicemails._ID + "=? AND " + Voicemails.ARCHIVED + "= 0",
        new String[] {Long.toString(voicemail.getId())});
  }

  public int markReadInDatabase(List<Voicemail> voicemails) {
    int count = voicemails.size();
    for (int i = 0; i < count; i++) {
      markReadInDatabase(voicemails.get(i));
    }
    return count;
  }

  /** Utility method to mark single message as read. */
  public void markReadInDatabase(Voicemail voicemail) {
    Uri uri = ContentUris.withAppendedId(sourceUri, voicemail.getId());
    ContentValues contentValues = new ContentValues();
    contentValues.put(Voicemails.IS_READ, "1");
    contentResolver.update(uri, contentValues, null, null);
  }

  /**
   * Sends an update command to the voicemail content provider for a list of voicemails. From the
   * view of the provider, since the updater is the owner of the entry, a blank "update" means that
   * the voicemail source is indicating that the server has up-to-date information on the voicemail.
   * This flips the "dirty" bit to "0".
   *
   * @param voicemails The list of voicemails to update
   * @return The number of voicemails updated
   */
  public int markCleanInDatabase(List<Voicemail> voicemails) {
    int count = voicemails.size();
    for (int i = 0; i < count; i++) {
      markCleanInDatabase(voicemails.get(i));
    }
    return count;
  }

  /** Utility method to mark single message as clean. */
  public void markCleanInDatabase(Voicemail voicemail) {
    Uri uri = ContentUris.withAppendedId(sourceUri, voicemail.getId());
    ContentValues contentValues = new ContentValues();
    contentResolver.update(uri, contentValues, null, null);
  }

  /** Utility method to add a transcription to the voicemail. */
  public void updateWithTranscription(Voicemail voicemail, String transcription) {
    Uri uri = ContentUris.withAppendedId(sourceUri, voicemail.getId());
    ContentValues contentValues = new ContentValues();
    contentValues.put(Voicemails.TRANSCRIPTION, transcription);
    contentResolver.update(uri, contentValues, null, null);
  }

  /**
   * Voicemail is unique if the tuple of (phone account component name, phone account id, source
   * data) is unique. If the phone account is missing, we also consider this unique since it's
   * simply an "unknown" account.
   *
   * @param voicemail The voicemail to check if it is unique.
   * @return {@code true} if the voicemail is unique, {@code false} otherwise.
   */
  public boolean isVoicemailUnique(Voicemail voicemail) {
    Cursor cursor = null;
    PhoneAccountHandle phoneAccount = voicemail.getPhoneAccount();
    if (phoneAccount != null) {
      String phoneAccountComponentName = phoneAccount.getComponentName().flattenToString();
      String phoneAccountId = phoneAccount.getId();
      String sourceData = voicemail.getSourceData();
      if (phoneAccountComponentName == null || phoneAccountId == null || sourceData == null) {
        return true;
      }
      try {
        String whereClause =
            Voicemails.PHONE_ACCOUNT_COMPONENT_NAME
                + "=? AND "
                + Voicemails.PHONE_ACCOUNT_ID
                + "=? AND "
                + Voicemails.SOURCE_DATA
                + "=?";
        String[] whereArgs = {phoneAccountComponentName, phoneAccountId, sourceData};
        cursor = contentResolver.query(sourceUri, PROJECTION, whereClause, whereArgs, null);
        if (cursor.getCount() == 0) {
          return true;
        } else {
          return false;
        }
      } finally {
        if (cursor != null) {
          cursor.close();
        }
      }
    }
    return true;
  }

  /**
   * Marks voicemails in the local database as archived. This indicates that the voicemails from the
   * server were removed automatically to make space for new voicemails, and are stored locally on
   * the users devices, without a corresponding server copy.
   */
  public void markArchivedInDatabase(List<Voicemail> voicemails) {
    for (Voicemail voicemail : voicemails) {
      markArchiveInDatabase(voicemail);
    }
  }

  /** Utility method to mark single voicemail as archived. */
  public void markArchiveInDatabase(Voicemail voicemail) {
    Uri uri = ContentUris.withAppendedId(sourceUri, voicemail.getId());
    ContentValues contentValues = new ContentValues();
    contentValues.put(Voicemails.ARCHIVED, "1");
    contentResolver.update(uri, contentValues, null, null);
  }

  /** Find the oldest voicemails that are on the device, and also on the server. */
  public List<Voicemail> oldestVoicemailsOnServer(int numVoicemails) {
    if (numVoicemails <= 0) {
      Assert.fail("Query for remote voicemails cannot be <= 0");
    }

    String sortAndLimit = "date ASC limit " + numVoicemails;

    try (Cursor cursor =
        contentResolver.query(sourceUri, PROJECTION, ARCHIVED_SELECTION, null, sortAndLimit)) {

      Assert.isNotNull(cursor);

      List<Voicemail> voicemails = new ArrayList<>();
      while (cursor.moveToNext()) {
        final long id = cursor.getLong(_ID);
        final String sourceData = cursor.getString(SOURCE_DATA);
        Voicemail voicemail = Voicemail.createForUpdate(id, sourceData).build();
        voicemails.add(voicemail);
      }

      if (voicemails.size() != numVoicemails) {
        Assert.fail(
            String.format(
                "voicemail count (%d) doesn't matched expected (%d)",
                voicemails.size(), numVoicemails));
      }
      return voicemails;
    }
  }
}
