/**
 * @file
 * Declares an iterator interface for examining the AIX object data manager
 */

#include <odmi.h>

#include <cstdint>
#include <memory>
#include <string>
#include <utility>
#include <leatherman/locale/locale.hpp>

namespace facter { namespace util { namespace aix {

    /**
     * Singleton representing the ODM subsystem.
     * The ODM subsystem needs to be explicitly initialized and terminated when
     * it is used. This wraps that initialization/cleanup such that ODM is
     * initialized when it is first used, and cleaned up when all consumers
     * have released their references to it.
     *
     * some helper methods for interacting with the ODM are also defined here,
     * since we can't have a `namespace odm` here.
     */
    class odm {
    public:
         /// shared_ptr to the odm reference
         using ptr = std::shared_ptr<odm>;

         /**
          * Grab a reference to the ODM subsystem to keep it open.
          * If no references currently exist, this will initialize the ODM
          * subsystem.
          * @return 
          */
         static ptr open() {
             static std::weak_ptr<odm> self;

             auto result = self.lock();
             if (!result) {
                 result = ptr(new odm);
                 self = result;
             }
             return result;
         }

         /// deleted copy constructor
         odm(const odm&) = delete;

         /// deleted assignment operator
         /// @return nothing
         odm& operator=(const odm&) = delete;

         /**
          * Clean up the odm library when there are no more users
          */
         ~odm() {
             odm_terminate();
         }

         /**
          * Get the error string for an ODM error state.
          * @return an error string owned by the ODM subsystem
          */
         static std::string error_string() {
             char* msg;
             int result = odm_err_msg(odmerrno, &msg);
             if (result < 0) {
                 return leatherman::locale::format("failed to retrieve ODM error message");
             } else {
                 return std::string(msg);
             }
         }

    private:
         /**
          * Initialize the ODM library
          */
         odm() {
             if (odm_initialize() < 0) {
                 throw std::runtime_error(odm::error_string());
             }
         }
    };

    /**
     * This represents an ODM class as an iterable thing.  An ODM
     * class can be throught of as a table in a typical database.
     * Each templatized version of this has a singleton instance,
     * which represents a process' handle to that table. Each process
     * can open a single ODM class only once, and concurrent
     * operations are not supported.
     *
     * @tparam T the struct that is stored in the ODM class.
     */
    template <typename T>
    class odm_class : public std::enable_shared_from_this<odm_class<T>> {
    public:
        /// shared_ptr to an odm_class
        using ptr = std::shared_ptr<odm_class<T>>;

        /**
         * Implements the standard C++ iterator interface for an
         * odm_class. Iterators can be incremented and compared for
         * inequality. This is the minimum to make range-based for
         * loops work.
         *
         * Additional members may need to be added to make other
         * algorithms play nice.
         */
        class iterator {
        public:
             /**
              * inequality comparison.
              * @param rhs the other iterator to compare to
              * @return true if the iterators are not equal
              */
             bool operator != (const iterator& rhs) {
                 return _data != rhs._data && _owner != rhs._owner;
             }

             /**
              * pre-increment operator. This invalidates any
              * references held to the current data of this iterator.
              * @return the new value of the iterator
              */
             iterator& operator++() {
                 if (!_data || !_owner) {
                     return *this;
                 }
                 free(_data);
                 _data = static_cast<T*>(odm_get_next(_owner->_class, nullptr));
                 // If data == nullptr, we have reached the end of this query
                 if (!_data) {
                     _owner->_locked = false;
                     _owner = ptr(nullptr);
                 }
                 if ((intptr_t)_data < 0) {
                     throw std::runtime_error(odm::error_string());
                 }
                 return *this;
             }

             /**
              * dereference operator
              * @return a reference to the held ODM data structure
              */
             const T& operator*() const{
                 return *_data;
             }

             /**
              * Destructor. Frees any held ODM data.
              */
             ~iterator() {
                 if (_data) {
                     free(_data);
                 }
             }

        protected:  // Constructor is protected so iterators must come from an odm_class<T> or its associated query_proxy
             /**
              * Construct an iterator from an odm_class ptr and the first ODM data pointer
              * @param data the ODM data we point to
              * @param owner the odm_class object that owns this iterator
              */
             iterator(T* data, ptr owner) : _data(data), _owner(owner) {
                 if (_data) {
                     if (!_owner) {
                         throw std::logic_error(leatherman::locale::format("Tried to construct an iterator with valid data but no owner. Naughty naughty."));
                     }
                     _owner->_locked = true;
                 } else {
                     // In theory nobody should be constructing us with
                     // null data and valid owner, but why take the risk?
                     _owner = ptr(nullptr);
                 }
             }

        private:
             T *_data;
             ptr _owner;

             friend class odm_class::query_proxy;
             friend class odm_class;
        };
        friend class iterator;  // iterator is our friend so it can lock/unlock us.

        /**
         * A query_proxy instance represents a query of an ODM
         * class. The proxy has begin and end methods to allow it to
         * be used in context of a range-based for loop or other
         * algorithm.
         */
        class query_proxy {
        public:
             /**
              * Begin the actual ODM query. This locks the odm_class until all valid iterators from this query are destroyed.
              * @return first iterator of the query
              */
             iterator begin() {
                 if (_owner->_locked) {
                     throw std::logic_error(leatherman::locale::format("Cannot iterate over the same ODM class concurrently"));
                 }
                 auto data = static_cast<T*>(odm_get_first(_owner->_class, const_cast<char*>(_query.c_str()), nullptr));
                 if ((intptr_t)data < 0) {
                     throw std::runtime_error(odm::error_string());
                 }
                 return iterator(data, _owner);
             }

             /// @return an end iterator
             iterator end() {
                 return iterator(nullptr, nullptr);
             }

        protected:
             /**
              * Construct a query_proxy for an odm_class query
              * @param query the query string
              * @param owner the odm_class that owns this query
              */
             template <typename Arg>
             query_proxy(Arg&& query, ptr owner) : _query(std::forward<Arg>(query)), _owner(owner) {}

        private:
             std::string _query;
             ptr _owner;

             friend class odm_class;
        };
        friend class query_proxy;  // query proxy needs to know if we're locked so it can begin() properly.

        /**
         * This class exists purely to allow nicer syntax when
         * iterating over an odm_class. Using the proxy, it's possible
         * to use `.begin()` and `.end()`, isntead of `->begin()`.
         */
        class proxy {
        public:
             /**
              * Begin iterating over the entire odm_class. This could
              * potentially be MANY values, so use wisely. You
              * probably want query(). This will lock the odm_class
              * until iteration is complete or all valid iterators are
              * destructed.
              * @return an iterator
              */
             iterator begin() {
                 return _self->begin();
             }

             /**
              * Get the end iterator. All end iterators are identical
              * @return the end iterator
              */
             iterator end() {
                 return _self->end();
             }

             /**
              * Begin a query. This does not look the odm_class until
              * query_proxy::begin() is called.
              * @param query the query string
              * @return a query_proxy representing this query
              */
             template <typename Arg>
             query_proxy query(Arg&& query) {
                 return _self->query(std::forward<Arg>(query));
             }

        protected:
             /**
              * Construct a proxy for an odm_class
              * @param self The odm_class that we proxy for
              */
             proxy(ptr self) : _self(self) {}

        private:
             ptr _self;

             friend class odm_class;
        };
        friend class proxy;  // proxy needs to, well, proxy to us.

        /**
         * Get a reference to an odm_class named "name". This will
         * open the class if it is not yet open. The class will be
         * closed when all existing references are released.
         * @param name The name of the ODM class. Usually the string form of T
         * @return an odm_class::proxy object that represents the requested class
         */
        static proxy open(std::string name) {
            static std::weak_ptr<odm_class<T>> self;

            auto result = self.lock();
            if (!result) {
                result = ptr(new odm_class<T>(name));
                self = result;
            }
            return proxy { result };
        }

        /// deleted default constructor
        odm_class() = delete;

        /// deleted copy constructor
        odm_class(const odm_class&) = delete;

        /// deleted assignment operator
        /// @return nothing
        odm& operator=(const odm&) = delete;

        /**
         * Releases an ODM class when there are no more users of it.
         */
        ~odm_class() {
            odm_close_class(_class);
        }

    protected:
        /**
         * @see proxy::query
         * @param query the query string
         * @return a query_proxy representing the provided query
         */
        template <typename Arg>
        query_proxy query(Arg&& query) {
            return query_proxy(std::forward<Arg>(query), this->shared_from_this());
        }

        /**
         * @see proxy::begin
         * @return an iterator
         */
        iterator begin() {
            return query("").begin();
        }

        /**
         * @see proxy::end
         * @return the end/invalid iterator
         */
        iterator end() {
            return iterator(nullptr, nullptr);
        }

    private:
        odm_class(std::string name) : _locked(false) {
            _the_odm = odm::open();
            _class = odm_mount_class(const_cast<char*>(name.c_str()));
            if (reinterpret_cast<intptr_t>(_class) < 0) {
                throw std::runtime_error(odm::error_string());
            }
            _class = odm_open_class_rdonly(_class);
            if (reinterpret_cast<intptr_t>(_class) < 0) {
                throw std::runtime_error(odm::error_string());
            }
        }

    protected:
        /// The ODM class that we are wrapping
        CLASS_SYMBOL _class;

        /// whether we are currently locked for iteration
        bool _locked;

    private:
        odm::ptr _the_odm;
    };

}}}  // namespace facter::util::aix
