From 35398b6ec2daf83377cc781c41e541902ebbe453 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?=
 <jeanmichael.celerier@gmail.com>
Date: Sun, 22 Sep 2024 15:32:31 -0400
Subject: [PATCH] [pd/max] Many improvements

- Support much more types in the pd inlets
- Add support for update() in more places
- Handle class_attribute in pd too, by not creating an inlet in that case
Thanks @josephlarralde for fixes on max sdk locating and other improvements !
---
 cmake/avendish.examples.cmake                 |   8 +
 cmake/avendish.max.cmake                      |   4 +
 examples/Complete/CompleteMessageExample.hpp  | 111 +++++++++
 examples/Complete/CompleteMessageExample.pd   |  37 +++
 include/avnd/binding/max/outputs.hpp          |   6 +
 include/avnd/binding/pd/helpers.hpp           |  47 ++--
 include/avnd/binding/pd/inputs.hpp            | 221 +++++++++++++++++-
 include/avnd/binding/pd/message_processor.hpp |   8 +
 include/avnd/binding/pd/messages.hpp          |  15 +-
 include/avnd/binding/pd/outputs.hpp           |  18 +-
 include/avnd/wrappers/widgets.hpp             |   2 +
 11 files changed, 442 insertions(+), 35 deletions(-)
 create mode 100644 examples/Complete/CompleteMessageExample.hpp
 create mode 100644 examples/Complete/CompleteMessageExample.pd

diff --git a/cmake/avendish.examples.cmake b/cmake/avendish.examples.cmake
index 0ff9f7ee..cb57be6d 100644
--- a/cmake/avendish.examples.cmake
+++ b/cmake/avendish.examples.cmake
@@ -107,6 +107,14 @@ avnd_make_object(
 )
 
 
+avnd_make_object(
+  TARGET CompleteMessageExample
+  MAIN_FILE examples/Complete/CompleteMessageExample.hpp
+  MAIN_CLASS examples::helpers::CompleteMessageExample
+  C_NAME avnd_complete_message_example
+)
+
+
 
 # This one does not really make sense as a Pd or Max object
 # (Pd has no notion of MIDI port)
diff --git a/cmake/avendish.max.cmake b/cmake/avendish.max.cmake
index ccdf7a86..2426eaf5 100644
--- a/cmake/avendish.max.cmake
+++ b/cmake/avendish.max.cmake
@@ -14,6 +14,10 @@ elseif(EXISTS "${AVND_MAXSDK_PATH}/c74support/max-includes")
   set(MAXSDK_MAX_INCLUDE_DIR "${AVND_MAXSDK_PATH}/c74support/max-includes")
   set(MAXSDK_MSP_INCLUDE_DIR "${AVND_MAXSDK_PATH}/c74support/msp-includes")
   set(MAXSDK_JIT_INCLUDE_DIR "${AVND_MAXSDK_PATH}/c74support/jit-includes")
+elseif(EXISTS "${AVND_MAXSDK_PATH}/source/max-sdk-base/c74support/max-includes")
+  set(MAXSDK_MAX_INCLUDE_DIR "${AVND_MAXSDK_PATH}/source/max-sdk-base/c74support/max-includes")
+  set(MAXSDK_MSP_INCLUDE_DIR "${AVND_MAXSDK_PATH}/source/max-sdk-base/c74support/msp-includes")
+  set(MAXSDK_JIT_INCLUDE_DIR "${AVND_MAXSDK_PATH}/source/max-sdk-base/c74support/jit-includes")
 endif()
 
 if(APPLE)
diff --git a/examples/Complete/CompleteMessageExample.hpp b/examples/Complete/CompleteMessageExample.hpp
new file mode 100644
index 00000000..bdceecf5
--- /dev/null
+++ b/examples/Complete/CompleteMessageExample.hpp
@@ -0,0 +1,111 @@
+#pragma once
+#include <halp/callback.hpp>
+#include <halp/controls.hpp>
+#include <halp/messages.hpp>
+#include <halp/meta.hpp>
+
+#include <iostream>
+#include <vector>
+
+namespace examples::helpers
+{
+struct CompleteMessageExample
+{
+  halp_meta(name, "CompleteMessageExample")
+  halp_meta(c_name, "avnd_complete_message_example")
+  halp_meta(author, "Jean-Michaƫl Celerier")
+  halp_meta(category, "Examples")
+  halp_meta(description, "Test all the Max & Pd features at once")
+  halp_meta(uuid, "ecbf8e41-5596-4d13-8407-6b962a02fa54")
+
+  struct
+  {
+    // Pd:
+    // - first inlet will receive either:
+    //   * Numbers directly
+    //   * The message [ Test1 123 >
+    //   * The message [ test1 45 >
+    struct : halp::val_port<"Test1", int>
+    {
+      // Class attribute: always goes to the first inlet
+      // and does not create a new inlet
+      halp_flag(class_attribute);
+
+      void update(CompleteMessageExample& a)
+      {
+        std::cerr << "Test1: " << value << std::endl;
+        a.outputs.out_msg1(1.f, 10, "message1");
+        a.outputs.out_msg2("heya", "message2");
+      }
+    } test1;
+
+    struct : halp::val_port<"Test2", float>
+    {
+      // Class attribute: always goes to the first inlet
+      // and does not create a new inlet
+      halp_flag(class_attribute);
+
+      void update(CompleteMessageExample& a)
+      {
+        std::cerr << "Test2: " << value << std::endl;
+      }
+    } test2;
+    struct : halp::val_port<"Test3", float>
+    {
+      // Not a class attribute: will create a new inlet
+
+      void update(CompleteMessageExample& a)
+      {
+        std::cerr << "Test3: " << value << std::endl;
+      }
+    } test3;
+    struct : halp::val_port<"Test4", std::string>
+    {
+      // Not a class attribute: will create a new inlet
+
+      void update(CompleteMessageExample& a)
+      {
+        std::cerr << "Test4: " << value << std::endl;
+      }
+    } test4;
+    struct : halp::val_port<"Test5", std::vector<int>>
+    {
+      // Not a class attribute: will create a new inlet
+
+      void update(CompleteMessageExample& a)
+      {
+        std::cerr << "Test5: [ ";
+        for(auto v : value)
+          std::cerr << v << ", ";
+        std::cerr << "]\n";
+      }
+    } test5;
+  } inputs;
+
+  struct messages
+  {
+    struct
+    {
+      halp_meta(name, "m1");
+      void operator()(int a, float b, std::string c)
+      {
+        std::cerr << "m1: " << a << " " << b << " " << c << "\n";
+      }
+    } m1;
+  };
+
+  struct
+  {
+    halp::callback<"some_symbol", float, int, std::string> out_msg1;
+    halp::callback<"other_symbol", std::string, std::string> out_msg2;
+  } outputs;
+
+  void operator()()
+  {
+    std::cerr << "Test1: " << inputs.test1.value << "\n";
+    std::cerr << "Test2: " << inputs.test2.value << "\n";
+    std::cerr << "Test3: " << inputs.test3.value << "\n";
+  }
+};
+
+}
diff --git a/examples/Complete/CompleteMessageExample.pd b/examples/Complete/CompleteMessageExample.pd
new file mode 100644
index 00000000..4f293789
--- /dev/null
+++ b/examples/Complete/CompleteMessageExample.pd
@@ -0,0 +1,37 @@
+#N canvas 1344 0 956 1035 12;
+#X obj 135 472 avnd_complete_message_example;
+#X obj 138 523 print A;
+#X obj 337 524 print B;
+#X floatatom 143 306 5 0 0 0 - - - 0;
+#X msg 135 76 Test1 1;
+#X msg 137 228 m1 1 2.3 foobar;
+#X msg 137 134 Test2 15;
+#X msg 137 183 Test3 123;
+#X floatatom 202 401 4 0 0 0 - - - 0;
+#X obj 140 272 bng 18 250 50 0 empty empty empty 0 -9 0 12 #fcfcfc #000000 #000000;
+#X text 182 306 Set Test1 \, update();
+#X text 193 78 Set Test1 \, update();
+#X text 162 273 operator()();
+#X text 201 135 Set Test2 \, update();
+#X text 208 184 Set Test3 \, update();
+#X text 251 230 call m1();
+#X symbolatom 268 438 10 0 0 0 - - - 0;
+#X msg 361 445 list 1 32 17 14 6 145;
+#X text 197 382 Set Test3 \, update();
+#X msg 145 347 Test5 1 3 5 17 12;
+#X msg 266 408 symbol hello;
+#X text 367 407 Set Test4 \, update();
+#X text 519 446 Set Test5 \, update();
+#X connect 0 0 1 0;
+#X connect 0 1 2 0;
+#X connect 3 0 0 0;
+#X connect 4 0 0 0;
+#X connect 5 0 0 0;
+#X connect 6 0 0 0;
+#X connect 7 0 0 0;
+#X connect 8 0 0 1;
+#X connect 9 0 0 0;
+#X connect 16 0 0 2;
+#X connect 17 0 0 3;
+#X connect 19 0 0 0;
+#X connect 20 0 16 0;
diff --git a/include/avnd/binding/max/outputs.hpp b/include/avnd/binding/max/outputs.hpp
index 386408b1..9c0f04c8 100644
--- a/include/avnd/binding/max/outputs.hpp
+++ b/include/avnd/binding/max/outputs.hpp
@@ -69,6 +69,12 @@ struct do_process_outlet
     outlet_list(p, nullptr, l.atoms.size(), l.atoms.data());
   }
 
+  void operator()(const avnd::variant_ish auto& v) const noexcept
+  {
+    using namespace std;
+    visit([](const auto& val) { (*this)(val); }, v);
+  }
+
   void operator()(const avnd::map_ish auto& v) const noexcept
   {
     /*
diff --git a/include/avnd/binding/pd/helpers.hpp b/include/avnd/binding/pd/helpers.hpp
index 3bc00116..c865cbbc 100644
--- a/include/avnd/binding/pd/helpers.hpp
+++ b/include/avnd/binding/pd/helpers.hpp
@@ -91,10 +91,10 @@ static void process_generic_message(T& implementation, t_symbol* s)
 template <typename Arg>
 static constexpr bool compatible(t_atomtype type)
 {
-  if constexpr(requires(Arg arg) { arg = 0.f; })
-    return type == t_atomtype::A_FLOAT;
-  else if constexpr(requires(Arg arg) { arg = "str"; })
+  if constexpr(requires(Arg arg) { arg = "str"; })
     return type == t_atomtype::A_SYMBOL;
+  else if constexpr(requires(Arg arg) { arg = 0.f; })
+    return type == t_atomtype::A_FLOAT;
 
   return false;
 }
@@ -102,24 +102,14 @@ static constexpr bool compatible(t_atomtype type)
 template <typename Arg>
 static Arg convert(t_atom& atom)
 {
-  if constexpr(requires(Arg arg) { arg = 0.f; })
-    return atom.a_w.w_float;
-  else if constexpr(requires(Arg arg) { arg = "str"; })
+  if constexpr(requires(Arg arg) { arg = "str"; })
     return atom.a_w.w_symbol->s_name;
+  else if constexpr(requires(Arg arg) { arg = 0.f; })
+    return atom.a_w.w_float;
   else
     static_assert(std::is_same_v<void, Arg>, "Argument type not handled yet");
 }
 
-t_symbol* symbol_for_port(avnd::float_parameter auto& port)
-{
-  return &s_float;
-}
-
-t_symbol* symbol_for_port(avnd::string_parameter auto& port)
-{
-  return &s_symbol;
-}
-
 t_symbol* symbol_for_port(avnd::mono_audio_port auto& port)
 {
   return &s_signal;
@@ -130,9 +120,32 @@ t_symbol* symbol_for_port(avnd::poly_audio_port auto& port)
   return &s_signal;
 }
 
-t_symbol* symbol_for_port(auto& port)
+t_symbol* symbol_for_port(avnd::parameter auto& port)
 {
+  using type = std::remove_cvref_t<decltype(port.value)>;
+  if constexpr(avnd::vector_ish<type>)
+    return &s_list;
+  else if constexpr(avnd::set_ish<type>)
+    return &s_list;
+  else if constexpr(avnd::map_ish<type>)
+    return &s_list;
+  else if constexpr(avnd::pair_ish<type>)
+    return &s_list;
+  else if constexpr(avnd::tuple_ish<type>)
+    return &s_list;
+  else if constexpr(avnd::span_ish<type>)
+    return &s_list;
+  else if constexpr(std::floating_point<type>)
+    return &s_float;
+  else if constexpr(std::integral<type>)
+    return &s_float;
+  else if constexpr(avnd::string_ish<type>)
+    return &s_symbol;
   return &s_anything; // TODO is that correct ?
 }
 
+t_symbol* symbol_for_port(auto& port)
+{
+  return &s_anything; // TODO is that correct ?
+}
 }
diff --git a/include/avnd/binding/pd/inputs.hpp b/include/avnd/binding/pd/inputs.hpp
index 5125b177..ef52b804 100644
--- a/include/avnd/binding/pd/inputs.hpp
+++ b/include/avnd/binding/pd/inputs.hpp
@@ -1,9 +1,170 @@
 #pragma once
 #include <avnd/binding/pd/helpers.hpp>
 
+#if !defined(__cpp_lib_to_chars)
+#include <boost/lexical_cast.hpp>
+#else
+#include <charconv>
+#endif
+
+#include <cstring>
 namespace pd
 {
 
+// Note: this assumes that ac >=
+struct from_atom
+{
+  t_atom& av;
+  template <typename T>
+    requires std::integral<T> || std::floating_point<T>
+  bool operator()(T& v) const noexcept
+  {
+    switch(av.a_type)
+    {
+      case A_FLOAT: {
+        v = atom_getfloat(&av);
+        return true;
+      }
+      case A_SYMBOL: {
+        auto sym = av.a_w.w_symbol;
+        if(sym && sym->s_name)
+        {
+          double vv{};
+#if defined(__cpp_lib_to_chars)
+          auto [_, ec]
+              = std::from_chars(sym->s_name, sym->s_name + strlen(sym->s_name), vv);
+          if(ec == std::errc{})
+          {
+            v = vv;
+            return true;
+          }
+#else
+          std::string_view str{sym->s_name, strlen(sym->s_name)};
+          if(boost::conversion::try_lexical_convert(str, vv))
+          {
+            v = vv;
+            return true;
+          }
+#endif
+        }
+        return false;
+      }
+    }
+
+    return false;
+  }
+
+  bool operator()(std::string& v) const noexcept
+  {
+    if(av.a_type == A_SYMBOL)
+    {
+      if(auto sym = av.a_w.w_symbol; sym && sym->s_name)
+      {
+        v = sym->s_name;
+      }
+    }
+    return true;
+  }
+};
+
+struct from_atoms
+{
+  long ac{};
+  t_atom* av{};
+
+  bool operator()(std::integral auto& v) const noexcept { return from_atom{av[0]}(v); }
+
+  bool operator()(std::floating_point auto& v) const noexcept
+  {
+    return from_atom{av[0]}(v);
+  }
+
+  bool operator()(avnd::vector_ish auto& v) const noexcept
+  {
+    v.clear();
+    v.resize(ac);
+
+    for(int i = 0; i < ac; i++)
+      from_atom{av[i]}(v[i]);
+    return true;
+  }
+
+  template <std::size_t N>
+  bool operator()(avnd::array_ish<N> auto& v) const noexcept
+  {
+    for(int i = 0; i < ac; i++)
+      from_atom{av[i]}(v[i]);
+    return true;
+  }
+
+  bool operator()(avnd::pair_ish auto& v) const noexcept
+  {
+    if(ac < 2)
+      return false;
+
+    from_atom{av[0]}(v.first);
+    from_atom{av[1]}(v.second);
+    return true;
+  }
+
+  bool operator()(avnd::set_ish auto& v) const noexcept
+  {
+    v.clear();
+
+    using value_type = std::remove_cvref_t<typename decltype(v)::value_type>;
+    for(int i = 0; i < ac; i++)
+    {
+      value_type val;
+      from_atom{av[i]}(val);
+      v.insert(std::move(val));
+    }
+    return true;
+  }
+
+  bool operator()(avnd::map_ish auto& v) const noexcept
+  {
+    v.clear();
+    if(ac <= 1)
+      return false;
+
+    using value_type = std::remove_cvref_t<typename decltype(v)::value_type>;
+    using key_type = std::remove_cvref_t<typename decltype(v)::key_type>;
+    using mapped_type = std::remove_cvref_t<typename decltype(v)::mapped_type>;
+    for(int i = 0; i < ac / 2; i += 2)
+    {
+      value_type val;
+      from_atom{av[i]}(val.first);
+      from_atom{av[i + 1]}(val.second);
+      v.insert(std::move(val));
+    }
+    return true;
+  }
+
+  template <typename T>
+    requires std::is_enum_v<T>
+  bool operator()(T& v) const noexcept
+  {
+    auto r = static_cast<std::underlying_type_t<T>>(v);
+    auto res = from_atom{av[0]}(r);
+    if(res)
+      v = static_cast<T>(r);
+    return res;
+  }
+
+  template <typename T>
+    requires(
+        std::is_class_v<T> && avnd::pfr::tuple_size_v<T> == 0
+        && std::is_trivial_v<T> && std::is_standard_layout_v<T>)
+  bool operator()(T& v) const noexcept
+  {
+    // Impulse case, nothing to do
+    // FIXME unless we have an optional parameter !
+    return true;
+  }
+
+  bool operator()(std::string& v) const noexcept { return from_atom{av[0]}(v); }
+};
+
 template <typename T>
 struct inputs
 {
@@ -11,16 +172,56 @@ struct inputs
   {
     int k = 0;
     avnd::input_introspection<T>::for_all(
-        avnd::get_inputs<T>(implementation), [&x_obj, &k](auto& ctl) {
-          if(k++)
-          { // Skip the first port
-            if_possible(floatinlet_new(&x_obj, &ctl.value))
-
-            // TODO
-            //else if_possible(symbolinlet_new(&x_obj, &ctl.value));
-            // => we must allocate a t_symbol* and copy the s_name before execution
-          }
-        });
+        avnd::get_inputs<T>(implementation), [&x_obj, &k]<typename M>(M& ctl) {
+      // Skip the first port
+      if(k++)
+      {
+        // Do not create a port for attributes
+        if constexpr(!avnd::attribute_port<M>)
+        {
+          static constexpr auto name = avnd::get_name<M>();
+
+          inlet_new(&x_obj, &x_obj.ob_pd, pd::symbol_for_port(ctl), gensym(name.data()));
+        }
+      }
+    });
+  }
+
+  template <typename Field>
+  bool
+  process_inlet_control(T& obj, Field& field, std::string_view s, int argc, t_atom* argv)
+  {
+    if(from_atoms{argc, argv}(field.value))
+    {
+      if constexpr(requires { field.update(obj); })
+      {
+        field.update(obj);
+      }
+      return true;
+    }
+    return false;
+  }
+
+  bool process_inputs(
+      avnd::effect_container<T>& implementation, t_symbol* s, int argc, t_atom* argv)
+  {
+    // FIXME create static pointer tables instead
+    if constexpr(avnd::parameter_input_introspection<T>::size > 0)
+    {
+      bool ok = false;
+      std::string_view symname = s->s_name;
+      avnd::parameter_input_introspection<T>::for_all(
+          avnd::get_inputs(implementation), [&]<typename M>(M& field) {
+        if(ok)
+          return;
+        if(symname == avnd::get_name<M>())
+        {
+          ok = process_inlet_control(implementation.effect, field, symname, argc, argv);
+        }
+      });
+      return ok;
+    }
+    return false;
   }
 };
 
diff --git a/include/avnd/binding/pd/message_processor.hpp b/include/avnd/binding/pd/message_processor.hpp
index 0da4ed36..e24af0e0 100644
--- a/include/avnd/binding/pd/message_processor.hpp
+++ b/include/avnd/binding/pd/message_processor.hpp
@@ -128,13 +128,19 @@ struct message_processor
       case A_FLOAT: {
         // This is the float that is supposed to go inside the first inlet if any ?
         if constexpr(requires { port.value = 0.f; })
+        {
           avnd::apply_control(port, arg.a_w.w_float);
+          if_possible(port.update(implementation.effect));
+        }
         break;
       }
 
       case A_SYMBOL: {
         if constexpr(requires { port.value = "string"; })
+        {
           avnd::apply_control(port, arg.a_w.w_symbol->s_name);
+          if_possible(port.update(implementation.effect));
+        }
         break;
       }
 
@@ -190,6 +196,8 @@ struct message_processor
     // First try to process messages handled explicitely in the object
     if(messages_setup.process_messages(implementation, s, argc, argv))
       return;
+    if(input_setup.process_inputs(implementation, s, argc, argv))
+      return;
 
     // Then some default behaviour
     switch(argc)
diff --git a/include/avnd/binding/pd/messages.hpp b/include/avnd/binding/pd/messages.hpp
index f1a48a05..203b4bac 100644
--- a/include/avnd/binding/pd/messages.hpp
+++ b/include/avnd/binding/pd/messages.hpp
@@ -175,19 +175,20 @@ struct messages
   static bool process_messages(
       avnd::effect_container<T>& implementation, t_symbol* s, int argc, t_atom* argv)
   {
+    // FIXME create static pointer tables instead
     if constexpr(avnd::has_messages<T>)
     {
       bool ok = false;
       std::string_view symname = s->s_name;
       avnd::messages_introspection<T>::for_all(
           avnd::get_messages(implementation), [&]<typename M>(M& field) {
-            if(ok)
-              return;
-            if(symname == M::name())
-            {
-              ok = process_message(implementation.effect, field, symname, argc, argv);
-            }
-          });
+        if(ok)
+          return;
+        if(symname == avnd::get_name<M>())
+        {
+          ok = process_message(implementation.effect, field, symname, argc, argv);
+        }
+      });
       return ok;
     }
     return false;
diff --git a/include/avnd/binding/pd/outputs.hpp b/include/avnd/binding/pd/outputs.hpp
index c4ca7fba..c0be9605 100644
--- a/include/avnd/binding/pd/outputs.hpp
+++ b/include/avnd/binding/pd/outputs.hpp
@@ -192,6 +192,22 @@ inline void value_to_pd(t_outlet* outlet, const std::string& v) noexcept
   outlet_symbol(outlet, gensym(v.c_str()));
 }
 
+template <typename... Args>
+  requires(sizeof...(Args) > 1)
+inline void value_to_pd(t_outlet* outlet, Args&&... v) noexcept
+{
+  std::array<t_atom, sizeof...(Args)> atoms;
+  static constexpr int N = sizeof...(Args);
+
+  int i = 0;
+
+  [&]<std::size_t... I>(std::index_sequence<I...>) {
+    (value_to_pd(atoms[I], v), ...);
+  }(std::make_index_sequence<N>{});
+
+  outlet_list(outlet, &s_list, N, atoms.data());
+}
+
 template <typename T>
 struct value_writer
 {
@@ -213,7 +229,7 @@ struct value_writer
     using processor_type = typename T::processor_type;
     using lin_out = avnd::linear_timed_parameter_output_introspection<processor_type>;
     using indices = typename lin_out::indices_n;
-    constexpr int storage_index = avnd::index_of_element<Idx>(indices{});
+    static constexpr int storage_index = avnd::index_of_element<Idx>(indices{});
 
     auto& buffer = get<storage_index>(buffers);
 
diff --git a/include/avnd/wrappers/widgets.hpp b/include/avnd/wrappers/widgets.hpp
index d788fd90..c4dd977e 100644
--- a/include/avnd/wrappers/widgets.hpp
+++ b/include/avnd/wrappers/widgets.hpp
@@ -94,6 +94,8 @@ struct widget_reflection
         return "range_spinbox";
       case widget_type::multi_slider:
         return "multi_slider";
+      default:
+        return "unknown";
     }
   }
   using value_type = T;