////////////////////////////////////////////////////////
//
// GEM - Graphics Environment for Multimedia
//
// Implementation file
//
// SPDX-FileCopyrightText: © 2019, IOhannes m zmölnig and the GEM contributors
// SPDX-License-Identifier: GPL-2.0-or-later
//
////////////////////////////////////////////////////////

#include "modelfiler.h"
#include "plugins/modelloader.h"
#include <algorithm> // std::min
#include "RTE/Array.h"

CPPEXTERN_NEW(modelfiler);


namespace
{
std::string checkArrays(const std::string*tablenames, size_t count)
{
  gem::RTE::Array a;
  for(size_t i=0; i<count; i++) {
    if(!a.name(tablenames[i])) {
      return tablenames[i];
    }
  }
  return std::string();
}
};

/////////////////////////////////////////////////////////
//
// modelfiler
//
/////////////////////////////////////////////////////////
// Constructor
//
/////////////////////////////////////////////////////////
modelfiler :: modelfiler(void)
  : m_loader(gem::plugins::modelloader::getInstance())
  , m_infoOut(gem::RTE::Outlet(this))
{

}

/////////////////////////////////////////////////////////
// Destructor
//
/////////////////////////////////////////////////////////
modelfiler :: ~modelfiler(void)
{
  if(m_loader) {
    delete m_loader;
    m_loader=NULL;
  }
}

static gem::any atom2any(t_atom*ap)
{
  gem::any result;
  if(ap) {
    switch(ap->a_type) {
    case A_FLOAT:
      result=atom_getfloat(ap);
      break;
    case A_SYMBOL:
      result=std::string(atom_getsymbol(ap)->s_name);
      break;
    default:
      result=ap->a_w.w_gpointer;
    }
  }
  return result;
}
static void addProperties(CPPExtern*obj, gem::Properties&props, int argc, t_atom*argv)
{
  if(!argc) {
    return;
  }

  if(argv->a_type != A_SYMBOL) {
    pd_error(obj, "no key given...");
    return;
  }
  std::string key=std::string(atom_getsymbol(argv)->s_name);
  std::vector<gem::any> values;
  argc--;
  argv++;
  while(argc-->0) {
    values.push_back(atom2any(argv++));
  }
  switch(values.size()) {
  default:
    props.set(key, values);
    break;
  case 1:
    props.set(key, values[0]);
    break;
  case 0: {
    gem::any dummy;
    props.set(key, dummy);
  }
  break;
  }
}

void modelfiler :: setPropertyMess(t_symbol*, int argc, t_atom*argv)
{
  if(!argc) {
    error("no property specified!");
    return;
  }
  addProperties(this, m_readprops, argc, argv);

  if(m_loader) {
    m_loader->setProperties(m_readprops);
  }
}

void modelfiler :: getPropertyMess(t_symbol*, int argc, t_atom*argv)
{
  if(argc) {
    int i=0;
    m_readprops.clear();

    for(i=0; i<argc; i++) {
      addProperties(this, m_readprops, 1, argv+i);
    }

  } else {
    /* LATER: read all properties */
  }

  if(m_loader) {
    m_loader->getProperties(m_readprops);
    std::vector<std::string>keys=m_readprops.keys();
    unsigned int i=0;
    for(i=0; i<keys.size(); i++) {
      std::vector<gem::any>atoms;
      gem::any value;
      std::string key=keys[i];
      atoms.push_back(value=key);
      switch(m_readprops.type(key)) {
      default:
      case gem::Properties::UNSET:
        post("oops: %s", key.c_str());
        continue;
      case gem::Properties::NONE:
        break;
      case gem::Properties::DOUBLE:
        do {
          double d=0;
          if(m_readprops.get(key, d)) {
            atoms.push_back(value=d);
          }
        } while(0);
        break;
      case gem::Properties::STRING:
        do {
          std::string s;
          if(m_readprops.get(key, s)) {
            atoms.push_back(value=s);
          }
        } while(0);
        break;
      }
      m_infoOut.send("prop", atoms);
    }
  } else {
    verbose(1, "no open model loader...remembering properties...");
  }
}

void modelfiler :: enumPropertyMess()
{
  if(m_loader) {
    gem::Properties readable, writeable;
    std::vector<std::string>readkeys, writekeys;
    std::vector<gem::any>data;
    gem::any value;

    m_loader->enumProperties(readable, writeable);

    readkeys=readable.keys();

    data.clear();
    data.push_back(value=std::string("numread"));
    data.push_back(value=(int)readkeys.size());
    m_infoOut.send("proplist", data);

    unsigned int i=0;
    for(i=0; i<readkeys.size(); i++) {
      std::string key=readkeys[i];
      data.clear();
      data.push_back(value=std::string("read"));
      data.push_back(key);
      switch(readable.type(key)) {
      case gem::Properties::NONE:
        data.push_back(value=std::string("bang"));
        break;
      case gem::Properties::DOUBLE: {
        double d=-1;
        data.push_back(value=std::string("float"));
        /* LATER: get and show ranges */
        if(readable.get(key, d)) {
          data.push_back(value=d);
        }
      }
      break;
      case gem::Properties::STRING: {
        data.push_back(value=std::string("symbol"));
        std::string s;
        if(readable.get(key, s)) {
          data.push_back(value=s);
        }
      }
      break;
      default:
        data.push_back(value=std::string("unknown"));
        break;
      }
      m_infoOut.send("proplist", data);
    }

    writekeys=writeable.keys();

    data.clear();
    data.push_back(value=std::string("numwrite"));
    data.push_back(value=(int)writekeys.size());
    m_infoOut.send("proplist", data);

    for(i=0; i<writekeys.size(); i++) {
      data.clear();
      data.push_back(value=std::string("write"));
      std::string key=writekeys[i];
      data.push_back(value=key);
      switch(writeable.type(key)) {
      case gem::Properties::NONE:
        data.push_back(value=std::string("bang"));
        break;
      case gem::Properties::DOUBLE: {
        double d=-1;
        data.push_back(value=std::string("float"));
        /* LATER: get and show ranges */
        if(writeable.get(key, d)) {
          data.push_back(value=d);
        }
      }
      break;
      case gem::Properties::STRING: {
        data.push_back(value=std::string("symbol"));
        std::string s;
        if(writeable.get(key, s)) {
          data.push_back(value=s);
        }
      }
      break;
      default:
        data.push_back(value=std::string("unknown"));
        break;
      }
      m_infoOut.send("proplist", data);
    }
  } else {
    error("cannot enumerate properties without a valid model loader");
  }
}

void modelfiler :: setPropertiesMess(t_symbol*, int argc, t_atom*argv)
{
  addProperties(this, m_readprops, argc, argv);
}

void modelfiler :: applyProperties()
{
  if(m_loader) {
    m_loader->setProperties(m_readprops);
  } else {
    verbose(1, "no open model loader...remembering properties...");
  }
}
void modelfiler :: clearPropertiesMess()
{
  m_readprops.clear();
}

/////////////////////////////////////////////////////////
// backendMess
//
/////////////////////////////////////////////////////////
void modelfiler :: backendMess(t_symbol*s, int argc, t_atom*argv)
{
  m_backends.clear();
  if(argc) {
    for(int i=0; i<argc; i++) {
      if(A_SYMBOL == argv->a_type) {
        t_symbol* b=atom_getsymbol(argv+i);
        m_backends.push_back(b->s_name);
      } else {
        error("%s must be symbolic", s->s_name);
      }
    }
  } else {
    /* no backend requested, just enumerate them */
    if(m_loader) {
      const std::string sel = s->s_name;
      std::vector<gem::any>atoms;
      gem::any value;
      gem::Properties props;
      std::vector<std::string> backends;
      props.set("_backends", value);
      m_loader->getProperties(props);
      if(props.type("_backends")!=gem::Properties::UNSET) {
        props.get("_backends", backends);
      }
      atoms.clear();
      atoms.push_back(value=(int)(backends.size()));
      m_infoOut.send(sel + "s", atoms);
      if(!backends.empty()) {
        for(int i=0; i<backends.size(); i++) {
          atoms.clear();
          atoms.push_back(value=backends[i]);
          post("loader[%d] %s", i, backends[i].c_str());
          m_infoOut.send(sel, atoms);
        }
      } else {
        post("no model-loading backends found!");
      }
    }
  }
}

/////////////////////////////////////////////////////////
// readMess
//
/////////////////////////////////////////////////////////
void modelfiler :: readMess(const std::string&filename)
{
  gem::Properties wantProps = m_readprops;

  if(!m_loader) {
    error("no model loader backends found");
    return;
  }
  if(!m_backends.empty()) {
    wantProps.set("_backends", m_backends);
  }

  char buf[MAXPDSTRING];
  canvas_makefilename(const_cast<t_canvas*>(getCanvas()),
                      const_cast<char*>(filename.c_str()), buf, MAXPDSTRING);
  if(!m_loader->open(buf, wantProps)) {
    error("unable to read model '%s'", buf);
    return;
  }

#define COPYARRAY(name, tables, count) int name = copyArrays(#name, tables, count)
#define PRINTSUCCESS(name) if(name) do {        \
    alist.clear();                              \
    gem::any v = name;                   \
    alist.push_back(v);                         \
    m_infoOut.send(std::string(#name), alist); \
  } while(0)

  COPYARRAY(vertices, m_position, 3);
  COPYARRAY(colors, m_color, 4);
  COPYARRAY(texcoords, m_texture, 2);
  COPYARRAY(normals, m_normal, 3);

  m_loader->close();
  std::vector<gem::any> alist;
  PRINTSUCCESS(vertices);
  PRINTSUCCESS(colors);
  PRINTSUCCESS(texcoords);
  PRINTSUCCESS(normals);
}

namespace {
  float*getVector(gem::plugins::modelloader*loader
                  , size_t group, const std::string name
                  , size_t &dimen, size_t &size
    ) {
    gem::plugins::modelloader::mesh *m=loader->getMesh(group);
    if(!m)
      return nullptr;
    if("vertices" == name) {
      size=m->size;
      dimen=3;
      return m->vertices;
    }
    if("normals" == name) {
      size=m->size;
      dimen=3;
      return m->normals;
    }
    if("colors" == name) {
      size=m->size;
      dimen=4;
      return m->colors;
    }
    if("texcoords" == name) {
      size=m->size;
      dimen=2;
      return m->texcoords;
    }
    return 0;
  }
};

size_t modelfiler :: copyArrays(const std::string&name, const std::string*tablenames, size_t count)
{
  if(tablenames[0].empty()) return 0;
  const bool interleaved = tablenames[1].empty();
  const std::string* tables = interleaved ? &tablenames[0] : tablenames;
  const size_t ntables = interleaved ? 1 : count;

  if(ntables > 0 && tables[0].empty()) {
    return 0;
  }
  std::string failed = checkArrays(tables, ntables);
  if(!failed.empty()) {
    error("no such array '%s' for %s", failed.c_str(), name.c_str());
    return 0;
  }

  size_t totalsize=0;
  size_t meshes = m_loader->getNumMeshes();
  for(size_t m=0; m<meshes; m++) {
    size_t dimen=0, meshsize=0;
    float*data = getVector(m_loader, m, name, dimen, meshsize);
    if(!data || !meshsize || (dimen!=count))
      continue;
    totalsize += meshsize;
  }

  if(!totalsize) {
    return 0;
  }


  gem::RTE::Array tab;
  std::vector<gem::RTE::Array> tabs;
  if(interleaved) {
    tab = gem::RTE::Array(tables[0]);
    tab.resize(totalsize * count);
  } else {
    for(size_t i=0; i<ntables; i++) {
      gem::RTE::Array a(tables[i]);
      a.resize(totalsize);
      tabs.push_back(a);
    }
  }


  size_t offset=0;

  for(size_t m=0; m<meshes; m++) {
    size_t dimen=0, meshsize=0;
    float*data = getVector(m_loader, m, name, dimen, meshsize);
    if(!data || !meshsize || (dimen!=count))
      continue;
    for(size_t i=0; i<meshsize; i++) {
      for(size_t j=0; j<count; j++) {
        float val = data[i * count + j];
        if(interleaved)
          tab[(offset + i) * count + j] = val;
        else
          tabs[j][offset + i] = val;
      }
    }
    offset += meshsize;
  }
  return totalsize;
}


void modelfiler :: tableMess(t_symbol*s, int argc, t_atom*argv)
{
  const std::string tabletype = s->s_name;
  int ncomponents = 0;
  std::string*names = 0;

  if     (tabletype == "position") { ncomponents = 3; names = m_position; }
  else if(tabletype == "normal")   { ncomponents = 3; names = m_normal; }
  else if(tabletype == "texture")  { ncomponents = 2; names = m_texture; }
  else if(tabletype == "color")    { ncomponents = 4; names = m_color; }

  if (!names) {
    error("invalid tabletype '%s'", s->s_name);
    return;
  }

  if (argc > 1 && argc != ncomponents) {
    error("'%s' requires 0, 1 or %d array names", s->s_name, ncomponents);
    return;
  }
  for (int i = 0; i < ncomponents; i++) {
    /* clear unused table names */
    if (argc == 0 || (argc == 1 && i > 0))
      names[i].clear();
    else
      names[i] = atom_getsymbol(argv + (argc == 1 ? 0 : i))->s_name;
  }
}



/////////////////////////////////////////////////////////
// static member function
//
/////////////////////////////////////////////////////////
void modelfiler :: obj_setupCallback(t_class *classPtr)
{
  CPPEXTERN_MSG1(classPtr, "read", readMess, std::string);
  CPPEXTERN_MSG1(classPtr, "open", readMess, std::string); /* legacy alias */
  CPPEXTERN_MSG (classPtr, "backend", backendMess);
  CPPEXTERN_MSG (classPtr, "loader", backendMess);

  CPPEXTERN_MSG (classPtr, "set", setPropertyMess);
  CPPEXTERN_MSG (classPtr, "get", getPropertyMess);
  CPPEXTERN_MSG (classPtr, "setProps", setPropertiesMess);
  CPPEXTERN_MSG0(classPtr, "enumProps", enumPropertyMess);
  CPPEXTERN_MSG0(classPtr, "clearProps", clearPropertiesMess);

  CPPEXTERN_MSG(classPtr, "position", tableMess);
  CPPEXTERN_MSG(classPtr, "color", tableMess);
  CPPEXTERN_MSG(classPtr, "texture", tableMess);
  CPPEXTERN_MSG(classPtr, "normal", tableMess);
}
