/*
 * Software License Agreement (BSD License)
 *
 *  Copyright (c) 2011, Willow Garage, Inc.
 *  All rights reserved.
 *
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions
 *  are met:
 *
 *   * Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above
 *     copyright notice, this list of conditions and the following
 *     disclaimer in the documentation and/or other materials provided
 *     with the distribution.
 *   * Neither the name of the copyright holder(s) nor the names of its
 *     contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 *  FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 *  COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 *  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 *  BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 *  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 *  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 *  LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 *  ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 *  POSSIBILITY OF SUCH DAMAGE.
 */

#pragma once

#include <pcl/pcl_config.h>

// needed for the grabber interface / observers
#include <map>
#include <memory>
#include <iostream>
#include <string>
#include <typeinfo>
#include <tuple>
#include <vector>
#include <sstream>
#include <pcl/pcl_macros.h>
#include <pcl/io/boost.h>
#include <pcl/exceptions.h>

namespace pcl
{

  /** \brief Grabber interface for PCL 1.x device drivers
    * \author Suat Gedikli <gedikli@willowgarage.com>
    * \ingroup io
    */
  class PCL_EXPORTS Grabber
  {
    public:
      /**
       * \brief Default ctor
       */
      Grabber() = default;

      /**
       * \brief No copy ctor since Grabber can't be copied
       */
      Grabber(const Grabber&) = delete;

      /**
       * \brief No copy assign operator since Grabber can't be copied
       */
      Grabber& operator=(const Grabber&) = delete;

      /**
       * \brief Move ctor
       */
      Grabber(Grabber&&) = default;

      /**
       * \brief Move assign operator
       */
      Grabber& operator=(Grabber&&) = default;

      /** \brief virtual destructor. */
      #if defined(_MSC_VER)
        virtual inline ~Grabber () noexcept {}
      #else
        virtual inline ~Grabber () noexcept = default;
      #endif

      /** \brief registers a callback function/method to a signal with the corresponding signature
        * \param[in] callback: the callback function/method
        * \return Connection object, that can be used to disconnect the callback method from the signal again.
        */
      template<typename T> boost::signals2::connection
      registerCallback (const std::function<T>& callback);

      /** \brief registers a callback function/method to a signal with the corresponding signature
        * \param[in] callback: the callback function/method
        * \return Connection object, that can be used to disconnect the callback method from the signal again.
        */
      template<typename T, template<typename> class FunctionT>
      PCL_DEPRECATED (1, 12, "please assign the callback to a std::function.")
      boost::signals2::connection
      registerCallback (const FunctionT<T>& callback)
      {
        return registerCallback (std::function<T> (callback));
      }

      /** \brief indicates whether a signal with given parameter-type exists or not
        * \return true if signal exists, false otherwise
        */
      template<typename T> bool
      providesCallback () const noexcept;

      /** \brief For devices that are streaming, the streams are started by calling this method.
        *        Trigger-based devices, just trigger the device once for each call of start.
        */
      virtual void
      start () = 0;

      /** \brief For devices that are streaming, the streams are stopped.
        *        This method has no effect for triggered devices.
        */
      virtual void
      stop () = 0;

      /** \brief For devices that are streaming, stopped streams are started and running stream are stopped.
        *        For triggered devices, the behavior is not defined.
        * \return true if grabber is running / streaming. False otherwise.
        */
      inline bool
      toggle ();

      /** \brief returns the name of the concrete subclass.
        * \return the name of the concrete driver.
        */
      virtual std::string
      getName () const = 0;

      /** \brief Indicates whether the grabber is streaming or not. This value is not defined for triggered devices.
        * \return true if grabber is running / streaming. False otherwise.
        */
      virtual bool
      isRunning () const = 0;

      /** \brief returns fps. 0 if trigger based. */
      virtual float
      getFramesPerSecond () const = 0;

    protected:

      virtual void
      signalsChanged () { }

      template<typename T> boost::signals2::signal<T>*
      find_signal () const noexcept;

      template<typename T> int
      num_slots () const noexcept;

      template<typename T> void
      disconnect_all_slots ();

      template<typename T> void
      block_signal ();

      template<typename T> void
      unblock_signal ();

      inline void
      block_signals ();

      inline void
      unblock_signals ();

      template<typename T> boost::signals2::signal<T>*
      createSignal ();

      std::map<std::string, std::unique_ptr<boost::signals2::signal_base>> signals_;
      std::map<std::string, std::vector<boost::signals2::connection> > connections_;
      std::map<std::string, std::vector<boost::signals2::shared_connection_block> > shared_connections_;
  } ;

  bool
  Grabber::toggle ()
  {
    if (isRunning ())
    {
      stop ();
    } else
    {
      start ();
    }
    return isRunning ();
  }

  template<typename T> boost::signals2::signal<T>*
  Grabber::find_signal () const noexcept
  {
    using Signal = boost::signals2::signal<T>;

    const auto signal_it = signals_.find (typeid (T).name ());
    if (signal_it != signals_.end ())
    {
      return (static_cast<Signal*> (signal_it->second.get ()));
    }
    return nullptr;
  }

  template<typename T> void
  Grabber::disconnect_all_slots ()
  {
    const auto signal = find_signal<T> ();
    if (signal != nullptr)
    {
      signal->disconnect_all_slots ();
    }
  }

  template<typename T> void
  Grabber::block_signal ()
  {
    if (connections_.find (typeid (T).name ()) != connections_.end ())
      for (auto &connection : shared_connections_[typeid (T).name ()])
        connection.block ();
  }

  template<typename T> void
  Grabber::unblock_signal ()
  {
    if (connections_.find (typeid (T).name ()) != connections_.end ())
      for (auto &connection : shared_connections_[typeid (T).name ()])
        connection.unblock ();
  }

  void
  Grabber::block_signals ()
  {
    for (const auto &signal : signals_)
      for (auto &connection : shared_connections_[signal.first])
        connection.block ();
  }

  void
  Grabber::unblock_signals ()
  {
    for (const auto &signal : signals_)
      for (auto &connection : shared_connections_[signal.first])
        connection.unblock ();
  }

  template<typename T> int
  Grabber::num_slots () const noexcept
  {
    const auto signal = find_signal<T> ();
    if (signal != nullptr)
    {
      return static_cast<int> (signal->num_slots ());
    }
    return 0;
  }

  template<typename T> boost::signals2::signal<T>*
  Grabber::createSignal ()
  {
    using Signal = boost::signals2::signal<T>;
    using Base = boost::signals2::signal_base;
    // DefferedPtr serves 2 purposes:
    // * allows MSVC to copy it around, can't do that with unique_ptr<T>
    // * performs dynamic allocation only when required. If the key is found, this
    //   struct is a no-op, otherwise it allocates when implicit conversion operator
    //   is called inside emplace/try_emplace
    struct DefferedPtr {
      operator std::unique_ptr<Base>() const { return std::make_unique<Signal>(); }
    };
    // TODO: remove later for C++17 features: structured bindings and try_emplace
    #ifdef __cpp_structured_bindings
      const auto [iterator, success] =
    #else
      typename decltype(signals_)::const_iterator iterator;
      bool success;
      std::tie (iterator, success) =
    #endif

    #ifdef __cpp_lib_map_try_emplace
      signals_.try_emplace (
    #else
      signals_.emplace (
    #endif
                         std::string (typeid (T).name ()), DefferedPtr ());
    if (!success)
    {
      return nullptr;
    }
    return static_cast<Signal*> (iterator->second.get ());
  }

  template<typename T> boost::signals2::connection
  Grabber::registerCallback (const std::function<T> & callback)
  {
    const auto signal = find_signal<T> ();
    if (signal == nullptr)
    {
      std::stringstream sstream;

      sstream << "no callback for type:" << typeid (T).name ();

      PCL_THROW_EXCEPTION (pcl::IOException, "[" << getName () << "] " << sstream.str ());
      //return (boost::signals2::connection ());
    }
    boost::signals2::connection ret = signal->connect (callback);

    connections_[typeid (T).name ()].push_back (ret);
    shared_connections_[typeid (T).name ()].push_back (boost::signals2::shared_connection_block (connections_[typeid (T).name ()].back (), false));
    signalsChanged ();
    return (ret);
  }

  template<typename T> bool
  Grabber::providesCallback () const noexcept
  {
    return find_signal<T> ();
  }

} // namespace
