/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

#include <arrow-glib/error.hpp>

#include <plasma-glib/client.hpp>
#include <plasma-glib/object.hpp>

G_BEGIN_DECLS

/**
 * SECTION: object
 * @section_id: object-classes
 * @title: Object related classes
 * @include: plasma-glib/plasma-glib.h
 *
 * #GPlasmaObjectID is a class for an object ID.
 *
 * #GPlasmaObject is a base class for an object stored in plasma store.
 *
 * #GPlasmaCreatedObject is a class for a created object. You can
 * change data of the object until the object is sealed or aborted.
 *
 * #GPlasmaReferredObject is a class for a created object. You can
 * only refer the data and metadata of the object. You can't change
 * the data of the object.
 *
 * Since: 0.12.0
 */

typedef struct GPlasmaObjectIDPrivate_ {
  plasma::ObjectID id;
} GPlasmaObjectIDPrivate;

G_DEFINE_TYPE_WITH_PRIVATE(GPlasmaObjectID,
                           gplasma_object_id,
                           G_TYPE_OBJECT)

#define GPLASMA_OBJECT_ID_GET_PRIVATE(object)   \
  static_cast<GPlasmaObjectIDPrivate *>(        \
    gplasma_object_id_get_instance_private(     \
      GPLASMA_OBJECT_ID(object)))

static void
gplasma_object_id_init(GPlasmaObjectID *object)
{
}

static void
gplasma_object_id_class_init(GPlasmaObjectIDClass *klass)
{
}

/**
 * gplasma_object_id_new:
 * @id: (array length=size): The raw ID bytes.
 * @size: The number of bytes of the ID. It must be 1..20.
 * @error: (nullable): Return location for a #GError or %NULL.
 *
 * Returns: (nullable): A newly created #GPlasmaObjectID on success,
 *   %NULL on error.
 *
 * Since: 0.12.0
 */
GPlasmaObjectID *
gplasma_object_id_new(const guint8 *id,
                      gsize size,
                      GError **error)
{
  if (size == 0 || size > plasma::kUniqueIDSize) {
    g_set_error(error,
                GARROW_ERROR,
                GARROW_ERROR_INVALID,
                "[plasma][object-id][new] "
                "ID must be 1..20 bytes: <%" G_GSIZE_FORMAT ">",
                size);
    return NULL;
  }

  auto object_id = g_object_new(GPLASMA_TYPE_OBJECT_ID, NULL);
  auto priv = GPLASMA_OBJECT_ID_GET_PRIVATE(object_id);
  memcpy(priv->id.mutable_data(), id, size);
  if (size != plasma::kUniqueIDSize) {
    memset(priv->id.mutable_data() + size, 0, plasma::kUniqueIDSize - size);
  }
  return GPLASMA_OBJECT_ID(object_id);
}

/**
 * gplasma_object_id_to_binary:
 * @id: A #GPlasmaObjectID.
 * @size: (nullable) (out): The number of bytes of the byte string of
 *   the object ID. It's always 20. 20 is `plasma::kUniqueIDSize`.
 *
 * Returns: (array length=size): The byte string of the object ID.
 *
 * Since: 0.12.0
 */
const guint8 *
gplasma_object_id_to_binary(GPlasmaObjectID *id,
                            gsize *size)
{
  auto priv = GPLASMA_OBJECT_ID_GET_PRIVATE(id);
  if (size) {
    *size = plasma::kUniqueIDSize;
  }
  return priv->id.data();
}

/**
 * gplasma_object_id_to_hex:
 * @id: A #GPlasmaObjectID.
 *
 * Returns: (transfer full): The hex representation of the object ID.
 *
 *   It should be freed with g_free() when no longer needed.
 *
 * Since: 0.12.0
 */
gchar *
gplasma_object_id_to_hex(GPlasmaObjectID *id)
{
  auto priv = GPLASMA_OBJECT_ID_GET_PRIVATE(id);
  return g_strdup(priv->id.hex().c_str());
}

typedef struct GPlasmaObjectPrivate_ {
  GPlasmaClient *client;
  GPlasmaObjectID *id;
  std::shared_ptr<arrow::Buffer> raw_data;
  GArrowBuffer *data;
  std::shared_ptr<arrow::Buffer> raw_metadata;
  GArrowBuffer *metadata;
  gint gpu_device;
} GPlasmaObjectPrivate;

enum {
  PROP_CLIENT = 1,
  PROP_ID,
  PROP_RAW_DATA,
  PROP_DATA,
  PROP_RAW_METADATA,
  PROP_METADATA,
  PROP_GPU_DEVICE
};

G_DEFINE_TYPE_WITH_PRIVATE(GPlasmaObject,
                           gplasma_object,
                           G_TYPE_OBJECT)

#define GPLASMA_OBJECT_GET_PRIVATE(object)      \
  static_cast<GPlasmaObjectPrivate *>(          \
    gplasma_object_get_instance_private(        \
      GPLASMA_OBJECT(object)))

static void
gplasma_object_dispose(GObject *object)
{
  auto priv = GPLASMA_OBJECT_GET_PRIVATE(object);

  // Properties except priv->id must be disposed in subclass.

  if (priv->id) {
    g_object_unref(priv->id);
    priv->id = nullptr;
  }

  G_OBJECT_CLASS(gplasma_object_parent_class)->dispose(object);
}

static void
gplasma_object_finalize(GObject *object)
{
  auto priv = GPLASMA_OBJECT_GET_PRIVATE(object);

  priv->raw_data.~shared_ptr();
  priv->raw_metadata.~shared_ptr();

  G_OBJECT_CLASS(gplasma_object_parent_class)->finalize(object);
}

static void
gplasma_object_set_property(GObject *object,
                            guint prop_id,
                            const GValue *value,
                            GParamSpec *pspec)
{
  auto priv = GPLASMA_OBJECT_GET_PRIVATE(object);

  switch (prop_id) {
  case PROP_CLIENT:
    priv->client = GPLASMA_CLIENT(g_value_dup_object(value));
    break;
  case PROP_ID:
    priv->id = GPLASMA_OBJECT_ID(g_value_dup_object(value));
    break;
  case PROP_RAW_DATA:
    priv->raw_data =
      *static_cast<std::shared_ptr<arrow::Buffer> *>(g_value_get_pointer(value));
    break;
  case PROP_DATA:
    priv->data = GARROW_BUFFER(g_value_dup_object(value));
    break;
  case PROP_RAW_METADATA:
    {
      auto raw_metadata =
        static_cast<std::shared_ptr<arrow::Buffer> *>(g_value_get_pointer(value));
      if (raw_metadata) {
        priv->raw_metadata = *raw_metadata;
      } else {
        priv->raw_metadata = nullptr;
      }
    }
    break;
  case PROP_METADATA:
    priv->metadata = GARROW_BUFFER(g_value_dup_object(value));
    break;
  case PROP_GPU_DEVICE:
    priv->gpu_device = g_value_get_int(value);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
    break;
  }
}

static void
gplasma_object_get_property(GObject *object,
                            guint prop_id,
                            GValue *value,
                            GParamSpec *pspec)
{
  auto priv = GPLASMA_OBJECT_GET_PRIVATE(object);

  switch (prop_id) {
  case PROP_CLIENT:
    g_value_set_object(value, priv->client);
    break;
  case PROP_ID:
    g_value_set_object(value, priv->id);
    break;
  case PROP_DATA:
    g_value_set_object(value, priv->data);
    break;
  case PROP_METADATA:
    g_value_set_object(value, priv->metadata);
    break;
  case PROP_GPU_DEVICE:
    g_value_set_int(value, priv->gpu_device);
    break;
  default:
    G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
    break;
  }
}

static void
gplasma_object_init(GPlasmaObject *object)
{
  auto priv = GPLASMA_OBJECT_GET_PRIVATE(object);
  new(&priv->raw_data) std::shared_ptr<arrow::Buffer>;
  new(&priv->raw_metadata) std::shared_ptr<arrow::Buffer>;
}

static void
gplasma_object_class_init(GPlasmaObjectClass *klass)
{
  auto gobject_class = G_OBJECT_CLASS(klass);

  gobject_class->dispose      = gplasma_object_dispose;
  gobject_class->finalize     = gplasma_object_finalize;
  gobject_class->set_property = gplasma_object_set_property;
  gobject_class->get_property = gplasma_object_get_property;

  GParamSpec *spec;
  spec = g_param_spec_object("client",
                             "Client",
                             "The client",
                             GPLASMA_TYPE_CLIENT,
                             static_cast<GParamFlags>(G_PARAM_READWRITE |
                                                      G_PARAM_CONSTRUCT_ONLY));
  g_object_class_install_property(gobject_class, PROP_CLIENT, spec);

  spec = g_param_spec_object("id",
                             "ID",
                             "The ID of this object",
                             GPLASMA_TYPE_OBJECT_ID,
                             static_cast<GParamFlags>(G_PARAM_READWRITE |
                                                      G_PARAM_CONSTRUCT_ONLY));
  g_object_class_install_property(gobject_class, PROP_ID, spec);

  spec = g_param_spec_pointer("raw-data",
                              "Raw data",
                              "The raw data of this object",
                              static_cast<GParamFlags>(G_PARAM_WRITABLE |
                                                       G_PARAM_CONSTRUCT_ONLY));
  g_object_class_install_property(gobject_class, PROP_RAW_DATA, spec);

  spec = g_param_spec_object("data",
                             "Data",
                             "The data of this object",
                             GARROW_TYPE_BUFFER,
                             static_cast<GParamFlags>(G_PARAM_READWRITE |
                                                      G_PARAM_CONSTRUCT_ONLY));
  g_object_class_install_property(gobject_class, PROP_DATA, spec);

  spec = g_param_spec_pointer("raw-metadata",
                              "Raw metadata",
                              "The raw metadata of this object",
                              static_cast<GParamFlags>(G_PARAM_WRITABLE |
                                                       G_PARAM_CONSTRUCT_ONLY));
  g_object_class_install_property(gobject_class, PROP_RAW_METADATA, spec);

  spec = g_param_spec_object("metadata",
                             "Metadata",
                             "The metadata of this object",
                             GARROW_TYPE_BUFFER,
                             static_cast<GParamFlags>(G_PARAM_READWRITE |
                                                      G_PARAM_CONSTRUCT_ONLY));
  g_object_class_install_property(gobject_class, PROP_METADATA, spec);

  spec = g_param_spec_int("gpu-device",
                          "GPU device",
                          "The GPU device number. -1 means GPU isn't used.",
                          -1,
                          G_MAXINT,
                          -1,
                          static_cast<GParamFlags>(G_PARAM_READWRITE |
                                                   G_PARAM_CONSTRUCT_ONLY));
  g_object_class_install_property(gobject_class, PROP_GPU_DEVICE, spec);
}

static bool
gplasma_object_check_not_released(GPlasmaObjectPrivate *priv,
                                  GError **error,
                                  const gchar *context)
{
  if (priv->client) {
    return true;
  }

  auto id_priv = GPLASMA_OBJECT_ID_GET_PRIVATE(priv->id);
  auto id_hex = id_priv->id.hex();
  g_set_error(error,
              GARROW_ERROR,
              GARROW_ERROR_INVALID,
              "%s: Can't process released object: <%s>",
              context,
              id_hex.c_str());
  return false;
}

static void
gplasma_object_release_resources(GPlasmaObjectPrivate *priv)
{
  if (priv->client) {
    g_object_unref(priv->client);
    priv->client = nullptr;
  }

  if (priv->data) {
    g_object_unref(priv->data);
    priv->data = nullptr;
  }

  if (priv->metadata) {
    g_object_unref(priv->metadata);
    priv->metadata = nullptr;
  }
}

G_DEFINE_TYPE(GPlasmaCreatedObject,
              gplasma_created_object,
              GPLASMA_TYPE_OBJECT)

static void
gplasma_created_object_dispose(GObject *object)
{
  auto priv = GPLASMA_OBJECT_GET_PRIVATE(object);

  if (priv->client) {
    gplasma_created_object_abort(GPLASMA_CREATED_OBJECT(object), NULL);
  }

  G_OBJECT_CLASS(gplasma_created_object_parent_class)->dispose(object);
}

static void
gplasma_created_object_init(GPlasmaCreatedObject *object)
{
}

static void
gplasma_created_object_class_init(GPlasmaCreatedObjectClass *klass)
{
  auto gobject_class = G_OBJECT_CLASS(klass);

  gobject_class->dispose = gplasma_created_object_dispose;
}

/**
 * gplasma_created_object_seal:
 * @object: A #GPlasmaCreatedObject.
 * @error: (nullable): Return location for a #GError or %NULL.
 *
 * Seals the object in the object store. You can't use the sealed
 * object anymore.
 *
 * Returns: %TRUE on success, %FALSE on error.
 *
 * Since: 0.12.0
 */
gboolean
gplasma_created_object_seal(GPlasmaCreatedObject *object,
                            GError **error)
{
  const auto context = "[plasma][created-object][seal]";

  auto priv = GPLASMA_OBJECT_GET_PRIVATE(object);
  if (!gplasma_object_check_not_released(priv, error, context)) {
    return FALSE;
  }

  auto plasma_client = gplasma_client_get_raw(priv->client);
  auto id_priv = GPLASMA_OBJECT_ID_GET_PRIVATE(priv->id);
  auto status = plasma_client->Seal(id_priv->id);
  auto success = garrow_error_check(error, status, context);
  if (success) {
    status = plasma_client->Release(id_priv->id);
    success = garrow_error_check(error, status, context);
    gplasma_object_release_resources(priv);
  }
  return success;
}

/**
 * gplasma_created_object_abort:
 * @object: A #GPlasmaCreatedObject.
 * @error: (nullable): Return location for a #GError or %NULL.
 *
 * Aborts the object in the object store. You can't use the aborted
 * object anymore.
 *
 * Returns: %TRUE on success, %FALSE on error.
 *
 * Since: 0.12.0
 */
gboolean
gplasma_created_object_abort(GPlasmaCreatedObject *object,
                             GError **error)
{
  const auto context = "[plasma][created-object][abort]";

  auto priv = GPLASMA_OBJECT_GET_PRIVATE(object);
  if (!gplasma_object_check_not_released(priv, error, context)) {
    return FALSE;
  }

  auto plasma_client = gplasma_client_get_raw(priv->client);
  auto id_priv = GPLASMA_OBJECT_ID_GET_PRIVATE(priv->id);
  auto status = plasma_client->Release(id_priv->id);
  auto success = garrow_error_check(error, status, context);
  if (success) {
    status = plasma_client->Abort(id_priv->id);
    success = garrow_error_check(error, status, context);
    gplasma_object_release_resources(priv);
  }
  return success;
}


G_DEFINE_TYPE(GPlasmaReferredObject,
              gplasma_referred_object,
              GPLASMA_TYPE_OBJECT)

static void
gplasma_referred_object_dispose(GObject *object)
{
  auto priv = GPLASMA_OBJECT_GET_PRIVATE(object);

  gplasma_object_release_resources(priv);

  G_OBJECT_CLASS(gplasma_referred_object_parent_class)->dispose(object);
}

static void
gplasma_referred_object_init(GPlasmaReferredObject *object)
{
}

static void
gplasma_referred_object_class_init(GPlasmaReferredObjectClass *klass)
{
  auto gobject_class = G_OBJECT_CLASS(klass);

  gobject_class->dispose = gplasma_referred_object_dispose;
}

/**
 * gplasma_referred_object_release:
 * @object: A #GPlasmaReferredObject.
 * @error: (nullable): Return location for a #GError or %NULL.
 *
 * Releases the object explicitly. The object is no longer valid.
 *
 * Returns: %TRUE on success, %FALSE on error.
 *
 * Since: 0.12.0
 */
gboolean
gplasma_referred_object_release(GPlasmaReferredObject *object,
                                GError **error)
{
  const auto context = "[plasma][referred-object][release]";

  auto priv = GPLASMA_OBJECT_GET_PRIVATE(object);
  if (!gplasma_object_check_not_released(priv, error, context)) {
    return FALSE;
  }

  gplasma_object_release_resources(priv);
  return TRUE;
}

G_END_DECLS

plasma::ObjectID
gplasma_object_id_get_raw(GPlasmaObjectID *id)
{
  auto priv = GPLASMA_OBJECT_ID_GET_PRIVATE(id);
  return priv->id;
}

GPlasmaCreatedObject *
gplasma_created_object_new_raw(GPlasmaClient *client,
                               GPlasmaObjectID *id,
                               std::shared_ptr<arrow::Buffer> *raw_data,
                               GArrowBuffer *data,
                               std::shared_ptr<arrow::Buffer> *raw_metadata,
                               GArrowBuffer *metadata,
                               gint gpu_device)
{
  auto object = g_object_new(GPLASMA_TYPE_CREATED_OBJECT,
                             "client", client,
                             "id", id,
                             "raw-data", raw_data,
                             "data", data,
                             "raw-metadata", raw_metadata,
                             "metadata", metadata,
                             "gpu-device", gpu_device,
                             NULL);
  return GPLASMA_CREATED_OBJECT(object);
}

GPlasmaReferredObject *
gplasma_referred_object_new_raw(GPlasmaClient *client,
                                GPlasmaObjectID *id,
                                std::shared_ptr<arrow::Buffer> *raw_data,
                                GArrowBuffer *data,
                                std::shared_ptr<arrow::Buffer> *raw_metadata,
                                GArrowBuffer *metadata,
                                gint gpu_device)
{
  auto object = g_object_new(GPLASMA_TYPE_REFERRED_OBJECT,
                             "client", client,
                             "id", id,
                             "raw-data", raw_data,
                             "data", data,
                             "raw-metadata", raw_metadata,
                             "metadata", metadata,
                             "gpu-device", gpu_device,
                             NULL);
  return GPLASMA_REFERRED_OBJECT(object);
}
