// Copyright 2019 Google Inc.
//
// 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.
//
///////////////////////////////////////////////////////////////////////////////

// Type conversion utilities for pybind11 and util::Status/StatusOr.
//
// Usage: Just include this file in the .cc file with your bindings and add the
// appropriate dependency. Any functions which take or return the supported
// types will have those types automatically converted.
//
// Supported types:
// - util::Status- converts a non-ok return status into a python exception.
//   Can be passed as an argument too if you import the status pybind module.
// - util::StatusOr- converts a non-ok return status into a python exception,
//   otherwise converts/returns the payload. Can only be used as a return value.
//
// Author: Ken Oslund (kenoslund@)
// Adapted for tink: rwgk@
// TODO(b/146426040): Remove this code when OSS absl::Status, StatusOr
//                    are available.
#ifndef TINK_PYTHON_CC_PYBIND_STATUS_CASTERS_H_
#define TINK_PYTHON_CC_PYBIND_STATUS_CASTERS_H_

#include <pybind11/cast.h>
#include <pybind11/pybind11.h>

#include <stdexcept>
#include <utility>

#include "tink/util/status.h"
#include "tink/util/statusor.h"
#include "tink/cc/pybind/status_utils.h"

namespace pybind11 {
namespace detail {

namespace util = crypto::tink::util;

// Convert NoThrowStatus by dispatching to a caster for StatusType with the
// argument throw_exception = false. StatusType should be a util::Status
// (rvalue, lvalue, reference, or pointer), or a util::StatusOr value.
// Only return values trigger exceptions, so NoThrowStatus has no meaning for
// input values. Therefore only C++->Python casting is supported.
template <typename StatusType>
struct type_caster<google_tink::NoThrowStatus<StatusType>> {
  using InputType = google_tink::NoThrowStatus<StatusType>;
  using StatusCaster = make_caster<StatusType>;
  static constexpr auto name = StatusCaster::name;

  // Convert C++->Python.
  static handle cast(const InputType& src, return_value_policy policy,
                     handle parent) {
    // pybind11::cast applies a const qualifier, so this takes a const reference
    // argument. The qualifiers we care about are in StatusType, and we will
    // forward those, but to do so, we must strip the cost off the InputType.
    return StatusCaster::cast(
        std::forward<StatusType>(const_cast<InputType&>(src).status), policy,
        parent, false);
  }
};

// Convert util::Status.
template <>
struct type_caster<util::Status> : public type_caster_base<util::Status> {
 public:
  // Conversion part 1 (Python->C++) handled by built in caster.
  bool load(handle src, bool convert) {
    google_tink::ImportStatusModule();
    return type_caster_base<util::Status>::load(src, convert);
  }

  // Conversion part 2 (C++ -> Python)
  static handle cast(const util::Status* src, return_value_policy policy,
                     handle parent, bool throw_exception = true) {
    if (!src) return none().release();
    return cast_impl<const util::Status&>(*src, policy, parent,
                                          throw_exception);
  }

  static handle cast(const util::Status& src, return_value_policy policy,
                     handle parent, bool throw_exception = true) {
    return cast_impl<const util::Status&>(src, policy, parent, throw_exception);
  }

  static handle cast(util::Status&& src, return_value_policy policy,
                     handle parent, bool throw_exception = true) {
    return cast_impl<util::Status&&>(std::move(src), policy, parent,
                                     throw_exception);
  }

 private:
  template <typename CType>
  static handle cast_impl(CType src, return_value_policy policy, handle parent,
                          bool throw_exception) {
    google_tink::ImportStatusModule();
    if (!throw_exception) {
      // Use the built-in/standard pybind11 caster.
      return type_caster_base<util::Status>::cast(std::forward<CType>(src),
                                                  policy, parent);
    } else if (!src.ok()) {
      // Convert a non-ok status into an exception.
      throw google_tink::StatusNotOk(std::forward<CType>(src));
    } else {
      // Return none for an ok status.
      return none().release();
    }
  }
};

// Convert a util::StatusOr.
template <typename PayloadType>
struct type_caster<util::StatusOr<PayloadType>> {
 public:
  using PayloadCaster = make_caster<PayloadType>;
  using StatusCaster = make_caster<util::Status>;
  static constexpr auto name = _("StatusOr[") + PayloadCaster::name + _("]");

  // Conversion part 2 (C++ -> Python).
  static handle cast(util::StatusOr<PayloadType>&& src,
                     return_value_policy policy, handle parent,
                     bool throw_exception = true) {
    if (src.ok()) {
      // Convert and return the payload.
      return PayloadCaster::cast(std::forward<PayloadType>(src.ValueOrDie()),
                                 policy, parent);
    } else {
      // Convert and return the error.
      return StatusCaster::cast(std::move(src.status()),
                                return_value_policy::move, parent,
                                throw_exception);
    }
  }
};

}  // namespace detail
}  // namespace pybind11

#endif  // TINK_PYTHON_CC_PYBIND_STATUS_CASTERS_H_
