/* SPDX-FileCopyrightText: 2020 Blender Authors
 *
 * SPDX-License-Identifier: GPL-2.0-or-later */

#include "abc_archive.h"

#include "BKE_blender_version.h"
#include "BKE_main.hh"

#include "DNA_scene_types.h"

#include <Alembic/Abc/ArchiveInfo.h>
#include <Alembic/Abc/ErrorHandler.h>
#include <Alembic/Abc/Foundation.h>
#include <Alembic/Abc/OArchive.h>
#include <Alembic/AbcCoreAbstract/MetaData.h>
#include <Alembic/AbcCoreAbstract/TimeSampling.h>
#include <Alembic/AbcCoreAbstract/TimeSamplingType.h>
#include <Alembic/AbcCoreOgawa/ReadWrite.h>
#include <Alembic/AbcGeom/ArchiveBounds.h>

#ifdef WIN32
#  include "BLI_path_utils.hh"
#  include "BLI_string.h"

#  include "utfconv.hh"
#endif

namespace blender::io::alembic {

using Alembic::Abc::ErrorHandler;
using Alembic::Abc::kWrapExisting;
using Alembic::Abc::MetaData;
using Alembic::Abc::OArchive;
using Alembic::Abc::TimeSampling;
using Alembic::Abc::TimeSamplingPtr;
using Alembic::Abc::TimeSamplingType;

static MetaData create_abc_metadata(const Main *bmain, double scene_fps)
{
  MetaData abc_metadata;

  std::string abc_user_description(bmain->filepath);
  if (abc_user_description.empty()) {
    abc_user_description = "unknown";
  }

  abc_metadata.set(Alembic::Abc::kApplicationNameKey, "Blender");
  abc_metadata.set(Alembic::Abc::kUserDescriptionKey, abc_user_description);
  abc_metadata.set("blender_version", std::string("v") + BKE_blender_version_string());
  abc_metadata.set("FramesPerTimeUnit", std::to_string(scene_fps));

  time_t raw_time;
  time(&raw_time);
  char buffer[128];

#if defined _WIN32 || defined _WIN64
  ctime_s(buffer, 128, &raw_time);
#else
  ctime_r(&raw_time, buffer);
#endif

  const std::size_t buffer_len = strlen(buffer);
  if (buffer_len > 0 && buffer[buffer_len - 1] == '\n') {
    buffer[buffer_len - 1] = '\0';
  }

  abc_metadata.set(Alembic::Abc::kDateWrittenKey, buffer);
  return abc_metadata;
}

static OArchive *create_archive(std::ofstream *abc_ostream,
                                const std::string &filepath,
                                MetaData &abc_metadata)
{
  /* Use stream to support unicode character paths on Windows. */
#ifdef WIN32
  char filepath_cstr[FILE_MAX];
  BLI_strncpy(filepath_cstr, filepath.c_str(), FILE_MAX);

  UTF16_ENCODE(filepath_cstr);
  std::wstring wstr(filepath_cstr_16);
  abc_ostream->open(wstr.c_str(), std::ios::out | std::ios::binary);
  UTF16_UN_ENCODE(filepath_cstr);
#else
  abc_ostream->open(filepath, std::ios::out | std::ios::binary);
#endif

  ErrorHandler::Policy policy = ErrorHandler::kThrowPolicy;

  Alembic::AbcCoreOgawa::WriteArchive archive_writer;
  return new OArchive(archive_writer(abc_ostream, abc_metadata), kWrapExisting, policy);
}

/* Construct list of shutter samples.
 *
 * These are taken from the interval [shutter open, shutter close),
 * uniformly sampled with 'nr_of_samples' samples.
 *
 * TODO(Sybren): test that the above interval is indeed half-open.
 *
 * If 'time_relative' is true, samples are returned as time (in seconds) from params.frame_start.
 * If 'time_relative' is false, samples are returned as fractional frames from 0.
 */
static void get_shutter_samples(double scene_fps,
                                const AlembicExportParams &params,
                                int nr_of_samples,
                                bool time_relative,
                                std::vector<double> &r_samples)
{
  double frame_offset = time_relative ? params.frame_start : 0.0;
  double time_factor = time_relative ? scene_fps : 1.0;
  double shutter_open = params.shutter_open;
  double shutter_close = params.shutter_close;
  double time_inc = (shutter_close - shutter_open) / nr_of_samples;

  /* sample between shutter open & close */
  for (int sample = 0; sample < nr_of_samples; sample++) {
    double sample_time = shutter_open + time_inc * sample;
    double time = (frame_offset + sample_time) / time_factor;

    r_samples.push_back(time);
  }
}

static TimeSamplingPtr create_time_sampling(double scene_fps,
                                            const AlembicExportParams &params,
                                            int nr_of_samples)
{
  std::vector<double> samples;

  if (params.frame_start == params.frame_end) {
    return TimeSamplingPtr(new TimeSampling());  // NOLINT: modernize-make-shared
  }

  get_shutter_samples(scene_fps, params, nr_of_samples, true, samples);

  TimeSamplingType ts(uint32_t(samples.size()), 1.0 / scene_fps);
  return TimeSamplingPtr(new TimeSampling(ts, samples));  // NOLINT: modernize-make-shared
}

static void get_frames(double scene_fps,
                       const AlembicExportParams &params,
                       uint nr_of_samples,
                       std::set<double> &r_frames)
{
  /* Get one set of shutter samples, then add those around each frame to export. */
  std::vector<double> shutter_samples;
  get_shutter_samples(scene_fps, params, nr_of_samples, false, shutter_samples);

  for (double frame = params.frame_start; frame <= params.frame_end; frame += 1.0) {
    for (size_t j = 0; j < nr_of_samples; j++) {
      r_frames.insert(frame + shutter_samples[j]);
    }
  }
}

/* ****************************************************************** */

ABCArchive::ABCArchive(const Main *bmain,
                       const Scene *scene,
                       AlembicExportParams params,
                       const std::string &filepath)
    : archive(nullptr)
{
  double scene_fps = FPS;
  MetaData abc_metadata = create_abc_metadata(bmain, scene_fps);

  /* Create the Archive. */
  archive = create_archive(&abc_ostream_, filepath, abc_metadata);

  /* Create time samples for transforms and shapes. */
  TimeSamplingPtr ts_xform;
  TimeSamplingPtr ts_shapes;

  ts_xform = create_time_sampling(scene_fps, params, params.frame_samples_xform);
  time_sampling_index_transforms_ = archive->addTimeSampling(*ts_xform);

  const bool export_animation = params.frame_start != params.frame_end;
  if (!export_animation || params.frame_samples_shape == params.frame_samples_xform) {
    ts_shapes = ts_xform;
    time_sampling_index_shapes_ = time_sampling_index_transforms_;
  }
  else {
    ts_shapes = create_time_sampling(scene_fps, params, params.frame_samples_shape);
    time_sampling_index_shapes_ = archive->addTimeSampling(*ts_shapes);
  }

  /* Construct the frames to export. */
  get_frames(scene_fps, params, params.frame_samples_xform, xform_frames_);
  get_frames(scene_fps, params, params.frame_samples_shape, shape_frames_);

  /* Merge all frames to get the final set of frames to export. */
  export_frames_.insert(xform_frames_.begin(), xform_frames_.end());
  export_frames_.insert(shape_frames_.begin(), shape_frames_.end());

  abc_archive_bbox_ = Alembic::AbcGeom::CreateOArchiveBounds(*archive,
                                                             time_sampling_index_transforms_);
}

ABCArchive::~ABCArchive()
{
  delete archive;
}

uint32_t ABCArchive::time_sampling_index_transforms() const
{
  return time_sampling_index_transforms_;
}

uint32_t ABCArchive::time_sampling_index_shapes() const
{
  return time_sampling_index_shapes_;
}

ABCArchive::Frames::const_iterator ABCArchive::frames_begin() const
{
  return export_frames_.begin();
}
ABCArchive::Frames::const_iterator ABCArchive::frames_end() const
{
  return export_frames_.end();
}
size_t ABCArchive::total_frame_count() const
{
  return export_frames_.size();
}

bool ABCArchive::is_xform_frame(double frame) const
{
  return xform_frames_.find(frame) != xform_frames_.end();
}
bool ABCArchive::is_shape_frame(double frame) const
{
  return shape_frames_.find(frame) != shape_frames_.end();
}
ExportSubset ABCArchive::export_subset_for_frame(double frame) const
{
  ExportSubset subset;
  subset.transforms = is_xform_frame(frame);
  subset.shapes = is_shape_frame(frame);
  return subset;
}

void ABCArchive::update_bounding_box(const Imath::Box3d &bounds)
{
  abc_archive_bbox_.set(bounds);
}

}  // namespace blender::io::alembic
