//
// Created by jonathan.hanks on 4/23/18.
//

#ifndef NDS2_CLIENT_NDS_SWIG_HH
#define NDS2_CLIENT_NDS_SWIG_HH

#include <algorithm>
#include <memory>
#include <ostream>
#include <stdexcept>
#include <type_traits>
#include <vector>

#include "nds_standalone.hh"

/**
 * \if Developer
 * \brief Client layers needed to build SWIG bindings
 *
 * The NDS_swig namespace contains client layers needed to build
 * the SWIG bindings.  These are not considered for general use
 * by the users.
 * \endif
 */
namespace NDS_swig
{
    class simple_segment_list_type;
    struct availability;
    class simple_availability_list_type;
    class availability_list_type;

    typedef NDS::buffer                              buffer;
    typedef std::vector< std::shared_ptr< buffer > > buffers_type;

    typedef NDS::channel                              channel;
    typedef std::vector< std::shared_ptr< channel > > channels_type;

    typedef NDS::epoch                              epoch;
    typedef std::vector< std::shared_ptr< epoch > > epochs_type;

    typedef NDS::parameters parameters;

    namespace detail
    {
        template < typename T, typename U >
        void
        destructive_translate_to_shared_ptr( T& input, U& output )
        {
            U intermediate; // use this to maintain our invariants on output,
            // don't
            intermediate.reserve( input.size( ) );
            for ( auto& cur : input )
            {
                std::shared_ptr< typename T::value_type > tmp(
                    new typename T::value_type( ) );
                tmp->swap( cur );
                intermediate.emplace_back( std::move( tmp ) );
            }
            output.swap( intermediate );
        }

        template < typename T, typename U >
        void
        destructive_translate_to_shared_ptr_swp( T& input, U& output )
        {
            U intermediate; // use this to maintain our invariants on output,
            // don't
            intermediate.reserve( input.size( ) );
            for ( typename T::iterator cur = input.begin( );
                  cur != input.end( );
                  ++cur )
            {
                std::shared_ptr< typename T::value_type > tmp(
                    new typename T::value_type( ) );
                std::swap( *tmp, *cur );
                intermediate.push_back( tmp );
            }
            output.swap( intermediate );
        }

        std::shared_ptr< NDS_swig::simple_segment_list_type >
        simplify_availability( const NDS_swig::availability& avail );

        NDS_swig::simple_availability_list_type simplify_availability_list(
            const NDS_swig::availability_list_type& avails );

        inline NDS::parameters
        default_parameters( )
        {
            return NDS::parameters( );
        }
    }

    class simple_segment_list_type
        : public std::vector< std::shared_ptr< NDS::simple_segment > >
    {
    public:
    };

    inline std::ostream&
    operator<<( std::ostream& os, const simple_segment_list_type& obj )
    {
        os << "( ";
        simple_segment_list_type::const_iterator cur = obj.begin( );
        if ( cur != obj.end( ) )
        {
            for ( simple_segment_list_type::const_iterator
                      cur = obj.begin( ),
                      last = obj.end( ) - 1;
                  cur != last;
                  ++cur )
            {
                os << ( *( *cur ) ) << ", ";
            }
            os << ( *( obj.back( ) ) ) << " ";
        }
        os << ")";
        return os;
    }

    class simple_availability_list_type
        : public std::vector< std::shared_ptr< simple_segment_list_type > >
    {
    public:
    };

    inline std::ostream&
    operator<<( std::ostream& os, const simple_availability_list_type& obj )
    {
        os << "( ";
        if ( obj.begin( ) != obj.end( ) )
        {
            for ( simple_availability_list_type::const_iterator
                      cur = obj.begin( ),
                      last = obj.end( ) - 1;
                  cur != last;
                  ++cur )
            {
                os << ( *cur ) << ", ";
            }
            os << ( *( obj.back( ) ) );
        }
        os << " )";
        return os;
    }

    class segment_list_type
        : public std::vector< std::shared_ptr< NDS::segment > >
    {
    public:
    };

    inline std::ostream&
    operator<<( std::ostream& os, const segment_list_type& obj )
    {
        os << "(";
        segment_list_type::const_iterator cur = obj.begin( );
        for ( ; cur != obj.end( ); ++cur )
        {
            os << " " << ( *cur )->frame_type << ":";
            os << ( *cur )->gps_start << "-" << ( *cur )->gps_stop;
        }
        os << " )";
        return os;
    }

    struct availability
    {
        std::string       name;
        segment_list_type data;

        std::shared_ptr< simple_segment_list_type >
        simple_list( ) const
        {
            return detail::simplify_availability( *this );
        }
    };

    inline std::ostream&
    operator<<( std::ostream& os, const availability& obj )
    {
        os << "<" << obj.name << " " << obj.data << " >";
        return os;
    }

    class availability_list_type
        : public std::vector< std::shared_ptr< availability > >
    {
    public:
        simple_availability_list_type
        simple_list( ) const
        {
            return detail::simplify_availability_list( *this );
        }
    };

    inline std::ostream&
    operator<<( std::ostream& os, const availability_list_type& obj )
    {
        os << "( ";
        if ( obj.begin( ) != obj.end( ) )
        {
            for ( availability_list_type::const_iterator cur = obj.begin( ),
                                                         last = obj.end( ) - 1;
                  cur != last;
                  ++cur )
            {
                os << ( *( *cur ) ) << ", ";
            }
            os << ( *( obj.back( ) ) ) << " ";
        }
        os << ")";
        return os;
    }

    namespace detail
    {
        inline bool
        availability_comp( segment_list_type::value_type a,
                           segment_list_type::value_type b )
        {
            if ( a->gps_start < b->gps_start )
            {
                return true;
            }
            if ( a->gps_start == b->gps_start )
            {
                return ( a->gps_stop > b->gps_stop );
            }
            return false;
        }

        inline std::shared_ptr< NDS_swig::simple_segment_list_type >
        simplify_availability( const NDS_swig::availability& avail )
        {
            NDS_swig::availability sortlist( avail );
            std::sort( sortlist.data.begin( ),
                       sortlist.data.end( ),
                       availability_comp );
            std::shared_ptr< simple_segment_list_type > retval(
                new simple_segment_list_type( ) );

            buffer::gps_second_type gps_cur = 0;
            for ( segment_list_type::iterator cur = sortlist.data.begin( );
                  cur != sortlist.data.end( );
                  ++cur )
            {
                buffer::gps_second_type start = ( *cur )->gps_start,
                                        stop = ( *cur )->gps_stop;
                // NDS::dout << "ex: cur = " << gps_cur << " " <<
                // (*cur)->frame_type
                // << " " << start << " " << stop << std::endl;
                if ( gps_cur >= stop )
                {
                    continue;
                }

                if ( gps_cur > start )
                {
                    start = gps_cur;
                }

                if ( gps_cur == start && retval->size( ) > 0 )
                {
                    retval->back( )->gps_stop = stop;
                }
                else
                {
                    retval->push_back(
                        NDS_swig::simple_segment_list_type::value_type(
                            new NDS::simple_segment( start, stop ) ) );
                }
                gps_cur = stop;
            }
            return retval;
        }

        inline std::shared_ptr< simple_segment_list_type >
        simplify_availability( std::shared_ptr< availability > avail )
        {
            return simplify_availability( *avail );
        }

        inline std::shared_ptr< NDS_swig::simple_segment_list_type >
        simplify_availability_shared_ptr(
            std::shared_ptr< NDS_swig::availability > avail )
        {
            return NDS_swig::detail::simplify_availability( *avail );
        }

        inline NDS_swig::simple_availability_list_type
        simplify_availability_list(
            const NDS_swig::availability_list_type& avails )
        {
            NDS_swig::simple_availability_list_type results;
            results.resize( avails.size( ) );
            std::transform(
                avails.begin( ),
                avails.end( ),
                results.begin( ),
                NDS_swig::detail::simplify_availability_shared_ptr );
            return results;
        }
    }

    namespace standalone
    {

        inline buffers_type
        fetch( const parameters&                          params,
               buffer::gps_second_type                    gps_start,
               buffer::gps_second_type                    gps_stop,
               const NDS::connection::channel_names_type& channel_names )
        {
            auto intermediate =
                NDS::fetch( params, gps_start, gps_stop, channel_names );
            buffers_type results;
            NDS_swig::detail::destructive_translate_to_shared_ptr( intermediate,
                                                                   results );
            return results;
        }

        inline buffers_type
        fetch( buffer::gps_second_type                    gps_start,
               buffer::gps_second_type                    gps_stop,
               const NDS::connection::channel_names_type& channel_names )
        {
            auto intermediate =
                NDS::fetch( gps_start, gps_stop, channel_names );
            buffers_type results;
            NDS_swig::detail::destructive_translate_to_shared_ptr( intermediate,
                                                                   results );
            return results;
        }

        inline channels_type
        find_channels(
            const parameters&     params,
            std::string           channel_glob = "*",
            channel::channel_type channel_type_mask =
                channel::DEFAULT_CHANNEL_MASK,
            channel::data_type data_type_mask = channel::DEFAULT_DATA_MASK,
            channel::sample_rate_type min_sample_rate =
                channel::MIN_SAMPLE_RATE,
            channel::sample_rate_type max_sample_rate =
                channel::MAX_SAMPLE_RATE,
            epoch timespan = epoch( "", 0, buffer::GPS_INF ) )
        {
            auto pred = NDS::channel_predicate(
                channel_glob,
                channel_type_mask,
                data_type_mask,
                NDS::frequency_range( min_sample_rate, max_sample_rate ),
                timespan );
            auto          intermediate = NDS::find_channels( params, pred );
            channels_type result;
            detail::destructive_translate_to_shared_ptr( intermediate, result );
            return result;
        }

        inline channels_type
        find_channels(
            std::string           channel_glob = "*",
            channel::channel_type channel_type_mask =
                channel::DEFAULT_CHANNEL_MASK,
            channel::data_type data_type_mask = channel::DEFAULT_DATA_MASK,
            channel::sample_rate_type min_sample_rate =
                channel::MIN_SAMPLE_RATE,
            channel::sample_rate_type max_sample_rate =
                channel::MAX_SAMPLE_RATE,
            epoch timespan = epoch( "", 0, buffer::GPS_INF ) )
        {
            auto pred = NDS::channel_predicate(
                channel_glob,
                channel_type_mask,
                data_type_mask,
                NDS::frequency_range( min_sample_rate, max_sample_rate ),
                timespan );
            auto          intermediate = NDS::find_channels( pred );
            channels_type result;
            detail::destructive_translate_to_shared_ptr( intermediate, result );
            return result;
        }
    }

    template < typename TYPES, typename IMPL >
    class connection;

    template < typename TYPES, typename IMPL >
    std::ostream& operator<<( std::ostream&, const connection< TYPES, IMPL >& );

    /**
     * The connection type as exported for SWIG.
     * @tparam TYPES A type to pull typdefs/... from typically it will be
     * NDS::connection
     * @tparam IMPL A type to pull implementation from.
     * @note When used by swig it is expected that both TYPES and IMPL will
     * be NDS::connection.  They should differ only when doing testing.
     */
    template < typename TYPES, typename IMPL >
    class connection
    {
        friend std::ostream&
        operator<<< TYPES, IMPL >( std::ostream&,
                                   const connection< TYPES, IMPL >& );

    public:
        typedef typename TYPES::protocol_type      protocol_type;
        typedef typename TYPES::host_type          host_type;
        typedef typename TYPES::port_type          port_type;
        typedef typename TYPES::count_type         count_type;
        typedef typename TYPES::channel_names_type channel_names_type;
        typedef typename TYPES::parameters_type    parameters_type;

        static const protocol_type PROTOCOL_INVALID = TYPES::PROTOCOL_INVALID;
        static const protocol_type PROTOCOL_ONE = TYPES::PROTOCOL_ONE;
        static const protocol_type PROTOCOL_TWO = TYPES::PROTOCOL_TWO;
        static const protocol_type PROTOCOL_TRY = TYPES::PROTOCOL_TRY;

        static const NDS::buffer::gps_second_type FAST_STRIDE =
            NDS::request_period::FAST_STRIDE;
        static const NDS::buffer::gps_second_type AUTO_STRIDE =
            NDS::request_period::AUTO_STRIDE;

        static const port_type DEFAULT_PORT = TYPES::DEFAULT_PORT;

        struct iterate_object
        {
            explicit iterate_object( NDS::data_iterable&& stream_ )
                : stream( std::move( stream_ ) ), cur( stream.begin( ) ),
                  end( stream.end( ) )
            {
            }

            NDS::data_iterable                stream;
            NDS::data_iterable::iterator_type cur;
            NDS::data_iterable::iterator_type end;
        };

        connection( const host_type& host,
                    port_type        port = DEFAULT_PORT,
                    protocol_type    protocol = PROTOCOL_TRY )
            : _conn( new IMPL( host, port, protocol ) ),
              _cur_iterate_obj( nullptr ),
              _current_epoch( "", 0, NDS::buffer::GPS_INF )
        {
        }

        connection( const parameters& params )
            : _conn( new IMPL( params ) ), _cur_iterate_obj( nullptr ),
              _current_epoch( "", 0, NDS::buffer::GPS_INF )
        {
        }

        connection( )
            : _conn( new IMPL( detail::default_parameters( ) ) ),
              _cur_iterate_obj( nullptr ),
              _current_epoch( "", 0, NDS::buffer::GPS_INF )
        {
        }

        ~connection( )
        {
        }
        void
        close( )
        {
            _conn->close( );
        }

        count_type
        count_channels(
            std::string           channel_glob = "*",
            channel::channel_type channel_type_mask =
                channel::DEFAULT_CHANNEL_MASK,
            channel::data_type data_type_mask = channel::DEFAULT_DATA_MASK,
            channel::sample_rate_type min_sample_rate =
                channel::MIN_SAMPLE_RATE,
            channel::sample_rate_type max_sample_rate =
                channel::MAX_SAMPLE_RATE )
        {
            return _conn->count_channels( NDS::channel_predicate(
                channel_glob,
                channel_type_mask,
                data_type_mask,
                NDS::frequency_range( min_sample_rate, max_sample_rate ) ) );
        }

        channels_type
        find_channels(
            std::string           channel_glob = "*",
            channel::channel_type channel_type_mask =
                channel::DEFAULT_CHANNEL_MASK,
            channel::data_type data_type_mask = channel::DEFAULT_DATA_MASK,
            channel::sample_rate_type min_sample_rate =
                channel::MIN_SAMPLE_RATE,
            channel::sample_rate_type max_sample_rate =
                channel::MAX_SAMPLE_RATE )
        {
            NDS::channels_type intermediate =
                _conn->find_channels( NDS::channel_predicate(
                    _current_epoch,
                    channel_glob,
                    channel_type_mask,
                    data_type_mask,
                    NDS::frequency_range( min_sample_rate,
                                          max_sample_rate ) ) );
            channels_type result;
            detail::destructive_translate_to_shared_ptr( intermediate, result );
            return result;
        }

        epochs_type
        get_epochs( )
        {
            NDS::epochs_type intermediate = _conn->get_epochs( );
            epochs_type      result;
            detail::destructive_translate_to_shared_ptr_swp( intermediate,
                                                             result );
            return result;
        }

        bool
        set_epoch( std::string epoch )
        {
            NDS::epochs_type available_epochs = _conn->get_epochs( );
            auto             match = std::find_if( available_epochs.begin( ),
                                       available_epochs.end( ),
                                       [&epoch]( NDS::epoch& cur ) -> bool {
                                           return cur.name == epoch;
                                       } );
            if ( match == available_epochs.end( ) )
            {
                throw NDS::connection::daq_error(
                    1, "Could not find the request epoch" );
            }
            return set_epoch( match->gps_start, match->gps_stop );
        }

        bool
        set_epoch( buffer::gps_second_type gps_start,
                   buffer::gps_second_type gps_stop )
        {
            if ( gps_stop < gps_start )
            {
                throw std::invalid_argument(
                    "GPS start time is greater than GPS stop time" );
            }
            _current_epoch.name = "";
            _current_epoch.gps_start = gps_start;
            _current_epoch.gps_stop = gps_stop;
            return true;
        }

        epoch
        current_epoch( ) const
        {
            return _current_epoch;
        }

        bool
        check( buffer::gps_second_type   gps_start,
               buffer::gps_second_type   gps_stop,
               const channel_names_type& channel_names )
        {
            return _conn->check( gps_start, gps_stop, channel_names );
        }

        buffers_type
        fetch( buffer::gps_second_type   gps_start,
               buffer::gps_second_type   gps_stop,
               const channel_names_type& channel_names )
        {
            NDS::buffers_type intermediate =
                _conn->fetch( gps_start, gps_stop, channel_names );
            buffers_type result;
            detail::destructive_translate_to_shared_ptr( intermediate, result );
            return result;
        }

        host_type
        get_host( ) const
        {
            return _conn->parameters( ).host( );
        }

        port_type
        get_port( ) const
        {
            return _conn->parameters( ).port( );
        }

        protocol_type
        get_protocol( ) const
        {
            return _conn->parameters( ).protocol( );
        }

        bool
        set_parameter( const std::string& parameter, const std::string& value )
        {
            return _conn->parameters( ).set( parameter, value );
        }

        std::string
        get_parameter( const std::string& parameter ) const
        {
            return _conn->parameters( ).get( parameter );
        }

        parameters_type
        get_parameters( ) const
        {
            return _conn->parameters( ).parameter_list( );
        }

        NDS_swig::parameters
        parameters( ) const
        {
            return _conn->parameters( );
        }

        const channel::hash_type&
        hash( ) const
        {
            return _conn->hash( );
        }

        connection&
        iterate( buffer::gps_second_type   gps_start,
                 buffer::gps_second_type   gps_stop,
                 buffer::gps_second_type   stride,
                 const channel_names_type& channel_names )
        {
            start_iterate( gps_start, gps_stop, stride, channel_names );
            return *this;
        }

        connection&
        iterate( const channel_names_type& channel_names )
        {
            start_iterate( 0, 0, 0, channel_names );
            return *this;
        }

        connection&
        iterate( buffer::gps_second_type   stride,
                 const channel_names_type& channel_names )
        {
            start_iterate( 0, 0, stride, channel_names );
            return *this;
        }

        connection&
        iterate( buffer::gps_second_type   gps_start,
                 buffer::gps_second_type   gps_stop,
                 const channel_names_type& channel_names )
        {
            start_iterate( gps_start, gps_stop, 0, channel_names );
            return *this;
        }

        buffers_type
        next( )
        {
            if ( !_cur_iterate_obj ||
                 ( _cur_iterate_obj->cur == _cur_iterate_obj->end ) )
            {
                throw std::out_of_range( "No Next" );
            }
            std::shared_ptr< NDS::buffers_type > intermediate =
                *( _cur_iterate_obj->cur );
            ++( _cur_iterate_obj->cur );
            if ( _cur_iterate_obj->cur == _cur_iterate_obj->end )
            {
                _cur_iterate_obj.reset( nullptr );
            }
            buffers_type result;
            detail::destructive_translate_to_shared_ptr( *intermediate,
                                                         result );
            return result;
        }

        bool
        request_in_progress( ) const
        {
            return (bool)_cur_iterate_obj;
        }

        void
        shutdown( )
        {
            close( );
        }

        availability_list_type
        get_availability( const channel_names_type& channel_names )
        {
            //            return _conn->get_availability(channel_names);
            availability_list_type      result;
            NDS::availability_list_type input =
                _conn->get_availability( current_epoch( ), channel_names );
            result.reserve( input.size( ) );

            for ( auto& cur : input )
            {
                std::shared_ptr< NDS_swig::availability > avail{
                    new NDS_swig::availability
                };
                avail->name = cur.name;
                avail->data.reserve( cur.data.size( ) );
                detail::destructive_translate_to_shared_ptr_swp( cur.data,
                                                                 avail->data );
                result.emplace_back( std::move( avail ) );
            }
            return result;
        }

    private:
        void
        start_iterate( buffer::gps_second_type   gps_start,
                       buffer::gps_second_type   gps_stop,
                       buffer::gps_second_type   stride,
                       const channel_names_type& channel_names )
        {
            if ( _cur_iterate_obj )
            {
                throw NDS::connection::transfer_busy_error( );
            }
            if ( gps_stop < gps_start )
            {
                throw std::invalid_argument(
                    "GPS start time is greater than GPS stop time" );
            }

            _cur_iterate_obj.reset( new iterate_object( _conn->iterate(
                NDS::request_period( gps_start, gps_stop, stride ),
                channel_names ) ) );
        }

        std::shared_ptr< IMPL >           _conn;
        std::unique_ptr< iterate_object > _cur_iterate_obj;
        NDS::epoch                        _current_epoch;
    };

    template < typename TYPES, typename IMPL >
    std::ostream&
    operator<<( std::ostream& os, const connection< TYPES, IMPL >& conn )
    {
        return os << *conn._conn;
    }
}

namespace
{
    std::ostream&
    operator<<( std::ostream& os, const NDS_swig::buffers_type& obj )
    {
        os << "(";
        for ( const auto& cur : obj )
        {
            os << "'" << *cur << "',";
        }
        os << ")";
        return os;
    }

    std::ostream&
    operator<<( std::ostream& os, const NDS_swig::epochs_type& obj )
    {
        os << "(";
        for ( const auto& cur : obj )
        {
            os << " " << *cur << ",";
        }
        os << " )";
        return os;
    }
}

#endif // NDS2_CLIENT_NDS_SWIG_HH
