/*
 * Copyright 2020 Google LLC
 *
 * 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
 *
 *      https://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.google.android.apps.exposurenotification.storage;

import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import androidx.room.Transaction;
import com.google.android.apps.exposurenotification.storage.DiagnosisEntity.Shared;
import com.google.android.apps.exposurenotification.storage.DiagnosisEntity.TestResult;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.List;

/**
 * Dao for the {@link DiagnosisEntity} table in the exposure notification database.
 */
@Dao
abstract class DiagnosisDao {

  @Query("SELECT * FROM DiagnosisEntity")
  abstract List<DiagnosisEntity> getAll();

  @Query("SELECT * FROM DiagnosisEntity WHERE id = :id")
  abstract DiagnosisEntity getById(long id);

  @Query("SELECT * FROM DiagnosisEntity WHERE verificationCode = :verificationCode")
  abstract ListenableFuture<List<DiagnosisEntity>> getByVerificationCodeAsync(
      String verificationCode);

  @Query("SELECT * FROM DiagnosisEntity WHERE id = :id")
  abstract ListenableFuture<DiagnosisEntity> getByIdAsync(long id);

  @Query("SELECT * FROM DiagnosisEntity WHERE id = :id")
  abstract LiveData<DiagnosisEntity> getByIdLiveData(long id);

  @Query("SELECT * FROM DiagnosisEntity ORDER BY id DESC")
  abstract LiveData<List<DiagnosisEntity>> getAllLiveData();

  @Query("SELECT * FROM DiagnosisEntity WHERE sharedStatus IN (:statuses)"
      + " ORDER BY createdTimestampMs DESC LIMIT 1")
  abstract ListenableFuture<DiagnosisEntity> maybeGetLastDiagnosisWithSharedStatusInStatusesAsync(
      List<Shared> statuses);

  @Query("SELECT * FROM DiagnosisEntity WHERE isPreAuth == 1"
      + " ORDER BY createdTimestampMs DESC LIMIT 1")
  abstract ListenableFuture<Optional<DiagnosisEntity>> maybeGetLastPreAuthDiagnosisAsync();

  @Query("SELECT * FROM DiagnosisEntity WHERE testResult IN (:testResults)"
      + " AND sharedStatus IN (:statuses) AND lastUpdatedTimestampMs > :minTimestampMs"
      + " ORDER BY lastUpdatedTimestampMs DESC LIMIT 1")
  abstract LiveData<Optional<DiagnosisEntity>> getDiagnosisWithTestResultAndStatusLastUpdatedAfterThresholdLiveData(
      List<TestResult> testResults, List<Shared> statuses, long minTimestampMs);

  @Transaction
  public Long createOrMutateById(
      long id, Function<DiagnosisEntity, DiagnosisEntity> mutator) {
    // If the given ID does not exist in the DB, create a new record.
    DiagnosisEntity diagnosis = getById(id);
    diagnosis = diagnosis == null ? DiagnosisEntity.newBuilder().build() : diagnosis;
    // Apply the mutation and write to storage.
    return upsert(mutator.apply(diagnosis));
  }

  @Transaction
  public Long upsert(DiagnosisEntity entity) {
    if (!Strings.isNullOrEmpty(entity.getRevisionToken())) {
      // When we have a revision token, we store it in its own table, so that it survives deletion
      // of the DiagnosisEntity.
      upsertRevisionTokenInternal(RevisionTokenEntity.create(
          // Room demands the create() method accept an ID; zero allows it to be autogenerated.
          0L,
          entity.getCreatedTimestampMs(),
          entity.getRevisionToken()));
    }

    return upsertDiagnosisInternal(entity);
  }

  @Insert(onConflict = OnConflictStrategy.REPLACE)
  abstract Long upsertDiagnosisInternal(DiagnosisEntity entity);

  @Insert(onConflict = OnConflictStrategy.REPLACE)
  abstract Long upsertRevisionTokenInternal(RevisionTokenEntity entity);

  @Query(
      "SELECT revisionToken FROM RevisionTokenEntity WHERE revisionToken IS NOT NULL"
          + " ORDER BY createdTimestampMs DESC LIMIT 1")
  abstract ListenableFuture<String> getMostRecentRevisionTokenAsync();

  @Query("DELETE FROM DiagnosisEntity WHERE id = :id")
  abstract ListenableFuture<Void> deleteById(long id);

  @Query("DELETE FROM RevisionTokenEntity")
  abstract ListenableFuture<Void> deleteAllRevisionTokens();

}
