// This software is in the public domain. Where that dedication is not
// recognized, you are granted a perpetual, irrevocable license to copy,
// distribute, and modify this file as you see fit.
// Authored in 2015 by Dimitri Diakopoulos (http://www.dimitridiakopoulos.com)
// https://github.com/ddiakopoulos/tinyply

#ifndef RESOURCES_COMMON_TINYPLY_TINYPLY_H_
#define RESOURCES_COMMON_TINYPLY_TINYPLY_H_

#include <algorithm>
#include <cstring>
#include <functional>
#include <iostream>
#include <map>
#include <memory>
#include <sstream>
#include <stdint.h>
#include <string>
#include <type_traits>
#include <vector>

#include <glog/logging.h>

namespace tinyply {

struct DataCursor {
  void* vector;
  uint8_t* data;
  size_t offset;
  bool realloc = false;
};

class PlyProperty {
  void parse_internal(std::istream& is);

 public:
  enum class Type : uint8_t {
    INVALID,
    INT8,
    UINT8,
    INT16,
    UINT16,
    INT32,
    UINT32,
    FLOAT32,
    FLOAT64
  };

  PlyProperty(std::istream& is);
  PlyProperty(Type type, const std::string& name)
      : propertyType(type), isList(false), name(name) {}
  PlyProperty(
      Type list_type, Type prop_type, const std::string& name, int listCount)
      : listType(list_type),
        propertyType(prop_type),
        isList(true),
        listCount(listCount),
        name(name) {}

  Type listType, propertyType;
  bool isList;
  int listCount = 0;
  std::string name;
};

inline std::string make_key(const std::string& a, const std::string& b) {
  return (a + "-" + b);
}

template <typename T>
void ply_cast(void* dest, const char* src) {
  *(static_cast<T*>(dest)) = *(reinterpret_cast<const T*>(src));
}

template <typename T>
void ply_cast_float(void* dest, const char* src) {
  *(static_cast<T*>(dest)) = *(reinterpret_cast<const T*>(src));
}

template <typename T>
void ply_cast_double(void* dest, const char* src) {
  *(static_cast<T*>(dest)) = *(reinterpret_cast<const T*>(src));
}

template <typename T>
T ply_read_ascii(std::istream& is) {
  T data;
  is >> data;
  return data;
}

template <typename T>
void ply_cast_ascii(void* dest, std::istream& is) {
  *(static_cast<T*>(dest)) = ply_read_ascii<T>(is);
}

struct PropertyInfo {
  int stride;
  std::string str;
};
static std::map<PlyProperty::Type, PropertyInfo> PropertyTable{
    {PlyProperty::Type::INT8, {1, "char"}},
    {PlyProperty::Type::UINT8, {1, "uchar"}},
    {PlyProperty::Type::INT16, {2, "short"}},
    {PlyProperty::Type::UINT16, {2, "ushort"}},
    {PlyProperty::Type::INT32, {4, "int"}},
    {PlyProperty::Type::UINT32, {4, "uint"}},
    {PlyProperty::Type::FLOAT32, {4, "float"}},
    {PlyProperty::Type::FLOAT64, {8, "double"}},
    {PlyProperty::Type::INVALID, {0, "INVALID"}}};

inline PlyProperty::Type property_type_from_string(const std::string& t) {
  if (t == "int8" || t == "char")
    return PlyProperty::Type::INT8;
  else if (t == "uint8" || t == "uchar")
    return PlyProperty::Type::UINT8;
  else if (t == "int16" || t == "short")
    return PlyProperty::Type::INT16;
  else if (t == "uint16" || t == "ushort")
    return PlyProperty::Type::UINT16;
  else if (t == "int32" || t == "int")
    return PlyProperty::Type::INT32;
  else if (t == "uint32" || t == "uint")
    return PlyProperty::Type::UINT32;
  else if (t == "float32" || t == "float")
    return PlyProperty::Type::FLOAT32;
  else if (t == "float64" || t == "double")
    return PlyProperty::Type::FLOAT64;
  return PlyProperty::Type::INVALID;
}

template <typename T>
inline uint8_t* resize(void* v, size_t newSize) {
  auto vec = static_cast<std::vector<T>*>(v);
  vec->resize(newSize);
  return reinterpret_cast<uint8_t*>(vec->data());
}

inline void resize_vector(
    const PlyProperty::Type t, void* v, size_t newSize, uint8_t*& ptr) {
  switch (t) {
    case PlyProperty::Type::INT8:
      ptr = resize<int8_t>(v, newSize);
      break;
    case PlyProperty::Type::UINT8:
      ptr = resize<uint8_t>(v, newSize);
      break;
    case PlyProperty::Type::INT16:
      ptr = resize<int16_t>(v, newSize);
      break;
    case PlyProperty::Type::UINT16:
      ptr = resize<uint16_t>(v, newSize);
      break;
    case PlyProperty::Type::INT32:
      ptr = resize<int32_t>(v, newSize);
      break;
    case PlyProperty::Type::UINT32:
      ptr = resize<uint32_t>(v, newSize);
      break;
    case PlyProperty::Type::FLOAT32:
      ptr = resize<float>(v, newSize);
      break;
    case PlyProperty::Type::FLOAT64:
      ptr = resize<double>(v, newSize);
      break;
    case PlyProperty::Type::INVALID:
      throw std::invalid_argument("invalid ply property");
  }
}

template <typename T>
inline PlyProperty::Type property_type_for_type(std::vector<T>& /*theType*/) {
  if (std::is_same<T, int8_t>::value)
    return PlyProperty::Type::INT8;
  else if (std::is_same<T, uint8_t>::value)
    return PlyProperty::Type::UINT8;
  else if (std::is_same<T, int16_t>::value)
    return PlyProperty::Type::INT16;
  else if (std::is_same<T, uint16_t>::value)
    return PlyProperty::Type::UINT16;
  else if (std::is_same<T, int32_t>::value)
    return PlyProperty::Type::INT32;
  else if (std::is_same<T, uint32_t>::value)
    return PlyProperty::Type::UINT32;
  else if (std::is_same<T, float>::value)
    return PlyProperty::Type::FLOAT32;
  else if (std::is_same<T, double>::value)
    return PlyProperty::Type::FLOAT64;
  else
    return PlyProperty::Type::INVALID;
}

class PlyElement {
  void parse_internal(std::istream& is);

 public:
  PlyElement(std::istream& istream);
  PlyElement(const std::string& name, size_t count) : name(name), size(count) {}
  std::string name;
  size_t size;
  std::vector<PlyProperty> properties;
};

inline int find_element(const std::string key, std::vector<PlyElement>& list) {
  for (size_t i = 0; i < list.size(); ++i) {
    if (list[i].name == key) {
      return i;
    }
  }
  return -1;
}

class PlyFile {
 public:
  PlyFile() {}
  PlyFile(std::istream& is);

  void read(std::istream& is);
  void write(std::ostream& os, bool isBinary);

  std::vector<PlyElement>& get_elements() {
    return elements;
  }

  std::vector<std::string> comments;
  std::vector<std::string> objInfo;

  template <typename T>
  size_t request_properties_from_element(
      const std::string& elementKey, std::vector<std::string> propertyKeys,
      std::vector<T>& source, const int listCount = 1) {
    if (get_elements().size() == 0)
      return 0;

    if (find_element(elementKey, get_elements()) >= 0) {
      if (std::find(
              requestedElements.begin(), requestedElements.end(), elementKey) ==
          requestedElements.end())
        requestedElements.push_back(elementKey);
    } else
      return 0;

    // count and verify large enough
    auto instance_counter = [&](
        const std::string& elementKey, const std::string& propertyKey) {
      for (auto e : get_elements()) {
        if (e.name != elementKey)
          continue;
        for (auto p : e.properties) {
          if (p.name == propertyKey) {
            if (PropertyTable[property_type_for_type(source)].stride !=
                PropertyTable[p.propertyType].stride)
              throw std::runtime_error(
                  "destination vector is wrongly typed to hold this property");
            return e.size;
          }
        }
      }
      return size_t(0);
    };

    // Check if requested key is in the parsed header
    std::vector<std::string> unusedKeys;
    for (auto key : propertyKeys) {
      for (auto e : get_elements()) {
        if (e.name != elementKey)
          continue;
        std::vector<std::string> headerKeys;
        for (auto p : e.properties) {
          headerKeys.push_back(p.name);
        }

        if (std::find(headerKeys.begin(), headerKeys.end(), key) ==
            headerKeys.end()) {
          unusedKeys.push_back(key);
        }
      }
    }

    // Not using them? Don't let them affect the propertyKeys count used for
    // calculating array sizes
    for (auto k : unusedKeys) {
      propertyKeys.erase(
          std::remove(propertyKeys.begin(), propertyKeys.end(), k),
          propertyKeys.end());
    }
    if (!propertyKeys.size())
      return 0;

    // All requested properties in the userDataTable share the same cursor
    // (thrown into the same flat array)
    auto cursor = std::make_shared<DataCursor>();

    std::vector<size_t> instanceCounts;

    for (auto key : propertyKeys) {
      if (int instanceCount = instance_counter(elementKey, key)) {
        instanceCounts.push_back(instanceCount);
        auto result = userDataTable.insert(
            std::pair<std::string, std::shared_ptr<DataCursor>>(
                make_key(elementKey, key), cursor));
        if (result.second == false)
          throw std::invalid_argument(
              "property has already been requested: " + key);
      } else
        continue;
    }

    size_t totalInstanceSize = [&]() {
      size_t t = 0;
      for (auto c : instanceCounts) {
        t += c;
      }
      return t;
    }() * listCount;
    source.resize(totalInstanceSize);  // this satisfies regular properties;
                                       // `cursor->realloc` is for list types
                                       // since tinyply uses single-pass parsing
    cursor->offset = 0;
    cursor->vector = &source;
    cursor->data = reinterpret_cast<uint8_t*>(source.data());

    if (listCount > 1) {
      cursor->realloc = true;
      return (totalInstanceSize / propertyKeys.size()) / listCount;
    }

    return totalInstanceSize / propertyKeys.size();
  }

  template <typename T>
  void add_properties_to_element(
      const std::string& elementKey,
      const std::vector<std::string>& propertyKeys, std::vector<T>& source,
      const int listCount = 1,
      const PlyProperty::Type listType = PlyProperty::Type::INVALID) {
    auto cursor = std::make_shared<DataCursor>();
    cursor->offset = 0;
    cursor->vector = &source;
    cursor->data = reinterpret_cast<uint8_t*>(source.data());

    auto create_property_on_element = [&](PlyElement& e) {
      for (auto key : propertyKeys) {
        PlyProperty::Type t = property_type_for_type(source);
        PlyProperty newProp = (listType == PlyProperty::Type::INVALID)
                                  ? PlyProperty(t, key)
                                  : PlyProperty(listType, t, key, listCount);
        userDataTable.insert(
            std::pair<std::string, std::shared_ptr<DataCursor>>(
                make_key(e.name, key), cursor));
        e.properties.push_back(newProp);
      }
    };

    int idx = find_element(elementKey, elements);
    if (idx >= 0) {
      PlyElement& e = elements[idx];
      create_property_on_element(e);
    } else {
      PlyElement newElement =
          (listCount == 1)
              ? PlyElement(elementKey, source.size() / propertyKeys.size())
              : PlyElement(elementKey, source.size() / listCount);
      create_property_on_element(newElement);
      elements.push_back(newElement);
    }
  }

 private:
  size_t skip_property_binary(const PlyProperty& property, std::istream& is);
  void skip_property_ascii(const PlyProperty& property, std::istream& is);

  void read_property_binary(
      PlyProperty::Type t, void* dest, size_t& destOffset, std::istream& is);
  void read_property_ascii(
      PlyProperty::Type t, void* dest, size_t& destOffset, std::istream& is);
  void write_property_ascii(
      PlyProperty::Type t, std::ostream& os, uint8_t* src, size_t& srcOffset);
  void write_property_binary(
      PlyProperty::Type t, std::ostream& os, uint8_t* src, size_t& srcOffset);

  bool parse_header(std::istream& is);
  void write_header(std::ostream& os);

  void read_header_format(std::istream& is);
  void read_header_element(std::istream& is);
  void read_header_property(std::istream& is);
  void read_header_text(
      std::string line, std::istream& is, std::vector<std::string>& place,
      int erase = 0);

  void read_internal(std::istream& is);

  void write_ascii_internal(std::ostream& os);
  void write_binary_internal(std::ostream& os);

  bool isBinary = false;
  bool isBigEndian = false;

  std::map<std::string, std::shared_ptr<DataCursor>> userDataTable;

  std::vector<PlyElement> elements;
  std::vector<std::string> requestedElements;
};

}  // namesapce tinyply

#endif  // RESOURCES_COMMON_TINYPLY_TINYPLY_H_
