// Copyright (c) 2018-2020 Dr. Colin Hirsch and Daniel Frey
// Please see LICENSE for license or visit https://github.com/taocpp/json/

#ifndef TAO_JSON_BINDING_FACTORY_HPP
#define TAO_JSON_BINDING_FACTORY_HPP

#include <bitset>
#include <map>
#include <sstream>
#include <stdexcept>
#include <string>

#include "../forward.hpp"

#include "../basic_value.hpp"
#include "../external/pegtl/demangle.hpp"
#include "../external/pegtl/internal/pegtl_string.hpp"
#include "../internal/escape.hpp"
#include "../internal/format.hpp"
#include "../internal/string_t.hpp"

#include "internal/type_key.hpp"

namespace tao::json::binding
{
   namespace internal
   {
      template< typename... Ts >
      void list_all_types( std::ostream& oss, const std::map< std::string, Ts... >& m )
      {
         for( const auto& i : m ) {
            json::internal::format_to( oss, " \"", i.first, '\"' );
         }
      }

      template< typename... Ts >
      void list_all_types( std::ostream& oss, const std::map< const std::type_info*, Ts... >& m )
      {
         for( const auto& i : m ) {
            json::internal::format_to( oss, ' ', i.first->name() );
         }
      }

      template< typename K, typename T, typename Base, template< typename... > class Pointer >
      struct factory_type
         : type_key< K, T >
      {
         [[nodiscard]] static const std::type_info* type()
         {
            return &typeid( T );
         }

         template< template< typename... > class Traits, typename... With >
         [[nodiscard]] static Pointer< Base > as( const basic_value< Traits >& v, With&... with )
         {
            using R = typename Traits< Pointer< T > >::template with_base< Base >;
            return R::as( v, with... );
         }

         template< template< typename... > class Traits >
         static void assign( basic_value< Traits >& v, const Pointer< Base >& p )
         {
            using R = typename Traits< Pointer< T > >::template with_base< Base >;
            R::assign( v, p );
         }

         template< template< typename... > class Traits, typename Producer >
         [[nodiscard]] static Pointer< Base > consume( Producer& parser )
         {
            using R = typename Traits< Pointer< T > >::template with_base< Base >;
            return R::template consume< Traits >( parser );
         }

         template< template< typename... > class Traits, typename Consumer >
         static void produce( Consumer& c, const Pointer< Base >& p )
         {
            using R = typename Traits< Pointer< T > >::template with_base< Base >;
            R::template produce< Traits >( c, p );
         }
      };

      template< typename K, typename T >
      struct factory_temp
      {
         template< typename Base, template< typename... > class Pointer >
         using bind = factory_type< K, T, Base, Pointer >;
      };

      template< template< typename... > class Traits, template< typename... > class Pointer, typename Base, typename... With >
      using as_func_t = Pointer< Base > ( * )( const basic_value< Traits >&, With&... );

   }  // namespace internal

   template< typename... Ts >
   struct factory
   {
      template< typename F >
      struct entry1
      {
         explicit entry1( F c )
            : function( c )
         {}

         F function;
      };

      template< typename F >
      struct entry2
      {
         entry2( F c, std::string&& n )
            : function( c ),
              name( std::move( n ) )
         {}

         F function;
         std::string name;
      };

      template< typename V, template< typename... > class Traits, template< typename... > class Pointer, typename Base, typename... With >
      static void emplace_as( std::map< std::string, entry1< internal::as_func_t< Traits, Pointer, Base, With... > >, std::less<> >& m )
      {
         using W = typename V::template bind< Base, Pointer >;
         m.try_emplace( W::template key< Traits >(), entry1< internal::as_func_t< Traits, Pointer, Base, With... > >( &W::template as< Traits, With... > ) );
      }

      template< template< typename... > class Traits, template< typename... > class Pointer, typename Base, typename... With >
      static void to( const basic_value< Traits >& v, Pointer< Base >& r, With&... with )
      {
         static const std::map< std::string, entry1< internal::as_func_t< Traits, Pointer, Base, With... > >, std::less<> > m = []() {
            std::map< std::string, entry1< internal::as_func_t< Traits, Pointer, Base, With... > >, std::less<> > t;
            ( emplace_as< Ts >( t ), ... );
            assert( t.size() == sizeof...( Ts ) );
            return t;
         }();

         const auto& a = v.get_object();
         if( a.size() != 1 ) {
            throw std::runtime_error( json::internal::format( "polymorphic factory requires object of size one for base class ", demangle< Base >(), json::message_extension( v ) ) );
         }
         const auto b = a.begin();
         const auto i = m.find( b->first );
         if( i == m.end() ) {
            std::ostringstream oss;
            json::internal::format_to( oss, "unknown factory type \"", json::internal::escape( b->first ), "\" -- known are" );
            internal::list_all_types( oss, m );
            json::internal::format_to( oss, " for base class ", demangle< Base >(), json::message_extension( v ) );
            throw std::runtime_error( oss.str() );
         }
         r = i->second.function( b->second, with... );
      }

      template< typename V, template< typename... > class Traits, template< typename... > class Pointer, typename Base, typename F >
      static void emplace_assign( std::map< const std::type_info*, entry2< F >, json::internal::type_info_less >& m )
      {
         using W = typename V::template bind< Base, Pointer >;
         m.try_emplace( W::type(), entry2< F >( &W::template assign< Traits >, W::template key< Traits >() ) );
      }

      template< template< typename... > class Traits, template< typename... > class Pointer, typename Base >
      static void assign( basic_value< Traits >& v, const Pointer< Base >& p )
      {
         using F = void ( * )( basic_value< Traits >&, const Pointer< Base >& );
         static const std::map< const std::type_info*, entry2< F >, json::internal::type_info_less > m = []() {
            std::map< const std::type_info*, entry2< F >, json::internal::type_info_less > t;
            ( emplace_assign< Ts, Traits, Pointer, Base >( t ), ... );
            assert( t.size() == sizeof...( Ts ) );
            return t;
         }();

         const auto i = m.find( &typeid( *p ) );
         if( i == m.end() ) {
            std::ostringstream oss;
            json::internal::format_to( oss, "unknown factory type ", demangle< decltype( *p ) >(), " -- known are" );
            internal::list_all_types( oss, m );
            json::internal::format_to( oss, " for base class ", demangle< Base >() );
            throw std::runtime_error( oss.str() );
         }
         i->second.function( v, p );
         v = {
            { i->second.name, std::move( v ) }
         };
      }

      template< typename V, template< typename... > class Traits, template< typename... > class Pointer, typename Base, typename Producer, typename F >
      static void emplace_consume( std::map< std::string, entry1< F >, std::less<> >& m )
      {
         using W = typename V::template bind< Base, Pointer >;
         m.try_emplace( W::template key< Traits >(), entry1< F >( &W::template consume< Traits, Producer > ) );
      }

      template< template< typename... > class Traits, template< typename... > class Pointer, typename Base, typename Producer >
      static void consume( Producer& parser, Pointer< Base >& r )
      {
         using F = Pointer< Base > ( * )( Producer& );
         static const std::map< std::string, entry1< F >, std::less<> > m = []() {
            std::map< std::string, entry1< F >, std::less<> > t;
            ( emplace_consume< Ts, Traits, Pointer, Base, Producer >( t ), ... );
            return t;
         }();

         auto s = parser.begin_object();
         const auto k = parser.key();
         const auto i = m.find( k );
         if( i == m.end() ) {
            std::ostringstream oss;
            json::internal::format_to( oss, "unknown factory type \"", json::internal::escape( k ), "\" -- known are" );
            internal::list_all_types( oss, m );
            json::internal::format_to( oss, " for base class ", demangle< Base >() );
            throw std::runtime_error( oss.str() );
         }
         r = i->second.function( parser );
         parser.end_object( s );
      }

      template< typename V, template< typename... > class Traits, template< typename... > class Pointer, typename Base, typename Consumer, typename F >
      static void emplace_produce( std::map< const std::type_info*, entry2< F >, json::internal::type_info_less >& m )
      {
         using W = typename V::template bind< Base, Pointer >;
         m.try_emplace( W::type(), entry2< F >( &W::template produce< Traits, Consumer >, W::template key< Traits >() ) );
      }

      template< template< typename... > class Traits, template< typename... > class Pointer, typename Base, typename Consumer >
      static void produce( Consumer& consumer, const Pointer< Base >& p )
      {
         using F = void ( * )( Consumer&, const Pointer< Base >& );
         static const std::map< const std::type_info*, entry2< F >, json::internal::type_info_less > m = []() {
            std::map< const std::type_info*, entry2< F >, json::internal::type_info_less > t;
            ( emplace_produce< Ts, Traits, Pointer, Base, Consumer >( t ), ... );
            assert( t.size() == sizeof...( Ts ) );
            return t;
         }();

         const auto i = m.find( &typeid( *p ) );
         if( i == m.end() ) {
            std::ostringstream oss;
            json::internal::format_to( oss, "unknown factory type ", demangle< decltype( *p ) >(), " -- known are" );
            internal::list_all_types( oss, m );
            json::internal::format_to( oss, " for base class ", demangle< Base >() );
            throw std::runtime_error( oss.str() );
         }
         consumer.begin_object( 1 );
         consumer.key( i->second.name );
         i->second.function( consumer, p );
         consumer.member();
         consumer.end_object( 1 );
      }
   };

}  // namespace tao::json::binding

#endif
