// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "impeller/archivist/archive.h"

#include <iterator>

#include "flutter/fml/logging.h"
#include "impeller/archivist/archive_class_registration.h"
#include "impeller/archivist/archive_database.h"
#include "impeller/archivist/archive_location.h"
#include "impeller/base/validation.h"

namespace impeller {

Archive::Archive(const std::string& path)
    : database_(std::make_unique<ArchiveDatabase>(path)) {}

Archive::~Archive() {
  FML_DCHECK(transaction_count_ == 0)
      << "There must be no pending transactions";
}

bool Archive::IsValid() const {
  return database_->IsValid();
}

std::optional<int64_t /* row id */> Archive::ArchiveInstance(
    const ArchiveDef& definition,
    const Archivable& archivable) {
  if (!IsValid()) {
    return std::nullopt;
  }

  auto transaction = database_->CreateTransaction(transaction_count_);

  const auto* registration =
      database_->GetRegistrationForDefinition(definition);

  if (registration == nullptr) {
    return std::nullopt;
  }

  auto statement = registration->CreateInsertStatement();

  if (!statement.IsValid() || !statement.Reset()) {
    /*
     *  Must be able to reset the statement for a new write
     */
    return std::nullopt;
  }

  auto primary_key = archivable.GetPrimaryKey();

  /*
   *  The lifecycle of the archive item is tied to this scope and there is no
   *  way for the user to create an instance of an archive item. So its safe
   *  for its members to be references. It does not manage the lifetimes of
   *  anything.
   */
  ArchiveLocation item(*this, statement, *registration, primary_key);

  /*
   *  If the item provides its own primary key, we need to bind it now.
   * Otherwise, one will be automatically assigned to it.
   */
  if (primary_key.has_value() &&
      !statement.WriteValue(ArchiveClassRegistration::kPrimaryKeyIndex,
                            primary_key.value())) {
    return std::nullopt;
  }

  if (!archivable.Write(item)) {
    return std::nullopt;
  }

  if (statement.Execute() != ArchiveStatement::Result::kDone) {
    return std::nullopt;
  }

  int64_t lastInsert = database_->GetLastInsertRowID();

  if (primary_key.has_value() &&
      lastInsert != static_cast<int64_t>(primary_key.value())) {
    return std::nullopt;
  }

  /*
   *  If any of the nested calls fail, we would have already checked for the
   *  failure and returned.
   */
  transaction.MarkWritesAsReadyForCommit();

  return lastInsert;
}

bool Archive::UnarchiveInstance(const ArchiveDef& definition,
                                PrimaryKey name,
                                Archivable& archivable) {
  UnarchiveStep stepper = [&archivable](ArchiveLocation& item) {
    archivable.Read(item);
    return false /* no-more after single read */;
  };

  return UnarchiveInstances(definition, stepper, name) == 1;
}

size_t Archive::UnarchiveInstances(const ArchiveDef& definition,
                                   const Archive::UnarchiveStep& stepper,
                                   PrimaryKey primary_key) {
  if (!IsValid()) {
    return 0;
  }

  const auto* registration =
      database_->GetRegistrationForDefinition(definition);

  if (registration == nullptr) {
    return 0;
  }

  const bool isQueryingSingle = primary_key.has_value();

  auto statement = registration->CreateQueryStatement(isQueryingSingle);

  if (!statement.IsValid() || !statement.Reset()) {
    return 0;
  }

  if (isQueryingSingle) {
    /*
     *  If a single statement is being queried for, bind the primary key as a
     * statement argument.
     */
    if (!statement.WriteValue(ArchiveClassRegistration::kPrimaryKeyIndex,
                              primary_key.value())) {
      return 0;
    }
  }

  if (statement.GetColumnCount() !=
      registration->GetMemberCount() + 1 /* primary key */) {
    return 0;
  }

  /*
   *  Acquire a transaction but never mark it successful since we will never
   *  be committing any writes to the database during unarchiving.
   */
  auto transaction = database_->CreateTransaction(transaction_count_);

  size_t itemsRead = 0;

  while (statement.Execute() == ArchiveStatement::Result::kRow) {
    itemsRead++;

    /*
     *  Prepare a fresh archive item for the given statement
     */
    ArchiveLocation item(*this, statement, *registration, primary_key);

    if (!stepper(item)) {
      break;
    }

    if (isQueryingSingle) {
      break;
    }
  }

  return itemsRead;
}

}  // namespace impeller
