#include <algorithm>
#include <iostream>
#include <sstream>

#include "tinyusdz.hh"
#include "tydra/attribute-eval.hh"
#include "tydra/render-data.hh"
#include "tydra/scene-access.hh"
#include "tydra/shader-network.hh"
#include "usdShade.hh"
#include "pprinter.hh"
#include "prim-pprint.hh"
#include "value-pprint.hh"
#include "value-types.hh"

static std::string GetFileExtension(const std::string &filename) {
  if (filename.find_last_of('.') != std::string::npos)
    return filename.substr(filename.find_last_of('.') + 1);
  return "";
}

static std::string str_tolower(std::string s) {
  std::transform(s.begin(), s.end(), s.begin(),
                 // static_cast<int(*)(int)>(std::tolower)         // wrong
                 // [](int c){ return std::tolower(c); }           // wrong
                 // [](char c){ return std::tolower(c); }          // wrong
                 [](unsigned char c) { return std::tolower(c); }  // correct
  );
  return s;
}

// key = Full absolute prim path(e.g. `/bora/dora`)
using XformMap = std::map<std::string, const tinyusdz::Xform *>;
using MeshMap = std::map<std::string, const tinyusdz::GeomMesh *>;
using MaterialMap = std::map<std::string, const tinyusdz::Material *>;
using PreviewSurfaceMap =
    std::map<std::string, std::pair<const tinyusdz::Shader *, const tinyusdz::UsdPreviewSurface *>>;
using UVTextureMap = std::map<std::string, std::pair<const tinyusdz::Shader *, const tinyusdz::UsdUVTexture *>>;
using PrimvarReader_float2Map =
    std::map<std::string, std::pair<const tinyusdz::Shader *, const tinyusdz::UsdPrimvarReader_float2 *>>;

#if 0
template <typename T>
static bool TraverseRec(const std::string &path_prefix,
                        const tinyusdz::Prim &prim, uint32_t depth,
                        std::map<std::string, const T *> &itemmap) {
  if (depth > 1024 * 128) {
    // Too deep
    return false;
  }

  std::string prim_abs_path = path_prefix + "/" + prim.local_path().full_path_name();

  if (prim.is<tinyusdz::Material>()) {
    if (const T *pv = prim.as<T>()) {
      std::cout << "Path : <" << prim_abs_path << "> is "
                << tinyusdz::value::TypeTraits<T>::type_name() << ".\n";
      itemmap[prim_abs_path] = pv;
    }
  }

  for (const auto &child : prim.children()) {
    if (!TraverseRec(prim_abs_path, child, depth + 1, itemmap)) {
      return false;
    }
  }

  return true;
}

template <typename T>
static bool TraverseShaderRec(const std::string &path_prefix,
                        const tinyusdz::Prim &prim, uint32_t depth,
                        std::map<std::string, const T *> &itemmap) {
  if (depth > 1024 * 128) {
    // Too deep
    return false;
  }

  std::string prim_abs_path = path_prefix + "/" + prim.local_path().full_path_name();

  // First test if Shader prim.
  if (const tinyusdz::Shader *ps = prim.as<tinyusdz::Shader>()) {
    // Concrete Shader object(e.g. UsdUVTexture) is stored in .data.
    if (const T *s = ps->value.as<T>()) {
      std::cout << "Path : <" << prim_abs_path << "> is "
                << tinyusdz::value::TypeTraits<T>::type_name() << ".\n";
      itemmap[prim_abs_path] = s;
    }
  }

  for (const auto &child : prim.children()) {
    if (!TraverseShaderRec(prim_abs_path, child, depth + 1, itemmap)) {
      return false;
    }
  }

  return true;
}

static void TraverseMaterial(const tinyusdz::Stage &stage, MaterialMap &m) {
  for (const auto &prim : stage.GetRootPrims()) {
    TraverseRec(/* root */ "", prim, 0, m);
  }
}

static void TraversePreviewSurface(const tinyusdz::Stage &stage,
                                   PreviewSurfaceMap &m) {
  for (const auto &prim : stage.GetRootPrims()) {
    TraverseShaderRec(/* root */ "", prim, 0, m);
  }
}

static void TraverseUVTexture(const tinyusdz::Stage &stage, UVTextureMap &m) {
  for (const auto &prim : stage.GetRootPrims()) {
    TraverseShaderRec(/* root */ "", prim, 0, m);
  }
}

static void TraversePrimvarReader_float2(const tinyusdz::Stage &stage,
                                         PrimvarReader_float2Map &m) {
  for (const auto &prim : stage.GetRootPrims()) {
    TraverseShaderRec(/* root */ "", prim, 0, m);
  }
}
#endif

int main(int argc, char **argv) {
  if (argc < 2) {
    std::cout << "Need USD file with Material/Shader(e.g. `<tinyusdz>/models/texturescube.usda`)\n" << std::endl;
    return EXIT_FAILURE;
  }

  std::string filepath = argv[1];
  std::string warn;
  std::string err;

  std::string ext = str_tolower(GetFileExtension(filepath));

  tinyusdz::Stage stage;

  if (!tinyusdz::IsUSD(filepath)) {
    std::cerr << "File not found or not a USD format: " << filepath << "\n";
  }

  bool ret = tinyusdz::LoadUSDFromFile(filepath, &stage, &warn, &err);
  if (!warn.empty()) {
    std::cerr << "WARN : " << warn << "\n";
  }

  if (!err.empty()) {
    std::cerr << "ERR : " << err << "\n";
  }

  if (!ret) {
    std::cerr << "Failed to load USD file: " << filepath << "\n";
    return EXIT_FAILURE;
  }

  // bool is_usdz = tinyusdz::IsUSDZ(filepath);

  std::string s = stage.ExportToString();
  std::cout << s << "\n";
  std::cout << "--------------------------------------"
            << "\n";

  // Visit all Prims in the Stage.
  auto prim_visit_fun = [](const tinyusdz::Path &abs_path, const tinyusdz::Prim &prim, const int32_t level, void *userdata, std::string *err) -> bool {
    (void)err;
    std::cout << tinyusdz::pprint::Indent(level) << "[" << level << "] (" << prim.data().type_name() << ") " << prim.local_path().prim_part() << " : AbsPath " << tinyusdz::to_string(abs_path) << "\n";

    // Use as() or is() for Prim specific processing.
    if (const tinyusdz::Material *pm = prim.as<tinyusdz::Material>()) {
      (void)pm;
      std::cout << tinyusdz::pprint::Indent(level) << "  Got Material!\n";
      // return false + `err` empty if you want to terminate traversal earlier.
      //return false;
    }

    return true;
  };

  void *userdata = nullptr;

  tinyusdz::tydra::VisitPrims(stage, prim_visit_fun, userdata);


  std::cout << "--------------------------------------"
            << "\n";

  // Compute Xform of each Prim at time t.
  {
    tinyusdz::tydra::XformNode xformnode;
    double t = tinyusdz::value::TimeCode::Default();
    tinyusdz::value::TimeSampleInterpolationType tinterp = tinyusdz::value::TimeSampleInterpolationType::Held; // Held or Linear

    if (!tinyusdz::tydra::BuildXformNodeFromStage(stage, &xformnode, t, tinterp)) {
      std::cerr << "BuildXformNodeFromStage error.\n";
    } else {
      std::cout << tinyusdz::tydra::DumpXformNode(xformnode) << "\n";
    }
  }


  // Mapping hold the pointer to concrete Prim object,
  // So stage content should not be changed(no Prim addition/deletion).
  XformMap xformmap;
  MeshMap meshmap;
  MaterialMap matmap;
  PreviewSurfaceMap surfacemap;
  UVTextureMap texmap;
  PrimvarReader_float2Map preadermap;

  // Collect and make full path <-> Prim/Shader mapping
  tinyusdz::tydra::ListPrims(stage, xformmap);
  tinyusdz::tydra::ListPrims(stage, meshmap);
  tinyusdz::tydra::ListPrims(stage, matmap);
  tinyusdz::tydra::ListShaders(stage, surfacemap);
  tinyusdz::tydra::ListShaders(stage, texmap);
  tinyusdz::tydra::ListShaders(stage, preadermap);

  //
  // Query example
  //
  for (const auto &item : matmap) {
    nonstd::expected<const tinyusdz::Prim*, std::string> mat = stage.GetPrimAtPath(tinyusdz::Path(item.first, /* prop name */""));
    if (mat) {
      std::cout << "Found Material <" << item.first << "> from Stage:\n";
      if (const tinyusdz::Material *mp = mat.value()->as<tinyusdz::Material>()) { // this should be true though.
        std::cout << tinyusdz::to_string(*mp) << "\n";
      }
    } else {
      std::cerr << "Err: " << mat.error() << "\n";
    }
  }

  for (const auto &item : surfacemap) {
    // Returned Prim is Shader class
    nonstd::expected<const tinyusdz::Prim*, std::string> shader = stage.GetPrimAtPath(tinyusdz::Path(item.first, /* prop name */""));
    if (shader) {
      std::cout << "Found Shader(UsdPreviewSurface) <" << item.first << "> from Stage:\n";

      const tinyusdz::Shader *sp = shader.value()->as<tinyusdz::Shader>();
      if (sp) { // this should be true though.

        if (const tinyusdz::UsdPreviewSurface *surf = sp->value.as<tinyusdz::UsdPreviewSurface>()) {
          // Print Shader
          std::cout << tinyusdz::to_string(*sp) << "\n";
        }
      }

    } else {
      std::cerr << "Err: " << shader.error() << "\n";
    }
  }

  for (const auto &item : texmap) {
    // Returned Prim is Shader class
    nonstd::expected<const tinyusdz::Prim*, std::string> shader = stage.GetPrimAtPath(tinyusdz::Path(item.first, /* prop name */""));
    if (shader) {
      std::cout << "Found Shader(UsdUVTexture) <" << item.first << "> from Stage:\n";

      const tinyusdz::Shader *sp = shader.value()->as<tinyusdz::Shader>();
      if (sp) { // this should be true though.
        if (const tinyusdz::UsdUVTexture *tex = sp->value.as<tinyusdz::UsdUVTexture>()) {
          std::cout << tinyusdz::to_string(*sp);
        }
      }

    } else {
      std::cerr << "Err: " << shader.error() << "\n";
    }
  }

  for (const auto &item : preadermap) {
    // Returned Prim is Shader class
    nonstd::expected<const tinyusdz::Prim*, std::string> shader = stage.GetPrimAtPath(tinyusdz::Path(item.first, /* prop name */""));
    if (shader) {
      std::cout << "Found Shader(UsdPrimvarReader_float2) <" << item.first << "> from Stage:\n";

      const tinyusdz::Shader *sp = shader.value()->as<tinyusdz::Shader>();
      if (sp) { // this should be true though.

        if (const tinyusdz::UsdPrimvarReader_float2 *preader = sp->value.as<tinyusdz::UsdPrimvarReader_float2>()) {
          std::cout << tinyusdz::to_string(*sp) << "\n";
        }
      }

    } else {
      std::cerr << "Err: " << shader.error() << "\n";
    }
  }

  //
  // -- Querying Parent Prim
  //
  for (const auto &item : surfacemap) {
    // Usually Parent is Material
    std::string err;
    if (const tinyusdz::Prim *p = tinyusdz::tydra::GetParentPrim(stage, tinyusdz::Path(item.first, /* property */""), &err)) {
      std::cout << "Input path = " << tinyusdz::to_string(item.first) << "\n";
      std::cout << "Parent prim = " << tinyusdz::prim::print_prim(*p) << "\n";
    } else {
      std::cerr << err;
    }
  }

  std::cout << "GetProperty example -------------\n";
  for (const auto &item : xformmap) {
    std::string err;
    tinyusdz::Property prop;
    if (tinyusdz::tydra::GetProperty(*item.second, "xformOp:transform", &prop, &err)) {
      std::cout << "Property value = " << print_prop(prop, "xformOp:transform", 0) << "\n";
    } else {
      std::cerr << err;
    }
  }


  //
  // Get bound Material
  //
  // - GetBoundMaterial:
  //   - Look Directly boundMaterial to the Prim.
  //   - If no material is asigned to the Prim, look into parent's BoundMaterial settings.
  //   - Also considers BindMaterial strength `bindMaterialAs` Prim metadatum
  //   - [ ] TODO: Consider materialBindingCollection
  // - GetDirectlyBoundMaterial: Get BoundMaterial assigned to the Prim being queried.
  //
  // Usually GetBoundMaterial is what you want.
  //
  // See https://openusd.org/release/wp_usdshade.html for more details about Material assignment.
  //
  std::cout << "GetBoundMaterial example -------------\n";
  for (const auto &item : meshmap) {

    tinyusdz::Path matPath;
    const tinyusdz::Material *material{nullptr};
    std::string err;

    bool ret = tinyusdz::tydra::GetDirectlyBoundMaterial(stage, tinyusdz::Path(item.first, ""), /* purpose */"", &matPath, &material, &err);
    std::cout << "Prim : " << item.first << "\n";
    if (ret) {
      std::cout << "has directlyBoundMaterial: " << matPath.full_path_name() << "\n";
    } else if (err.empty()) {
      std::cout << "no directlyBoundMaterial" << "\n";
    } else {
      std::cout << "GetDirectlyBoundMaterial failed: " << err << "\n";
    }

    ret = tinyusdz::tydra::GetBoundMaterial(stage, tinyusdz::Path(item.first, ""), /* purpose */"", &matPath, &material, &err);

    if (ret) {
      std::cout << item.first << " has bound Material. Material Path = " << matPath << "\n";
      std::cout << "mat = " << material << "\n";
      if (material) {
        std::cout << to_string(*material, /* indent */1) << "\n";
      }
    } else {
      std::cout << "Bound material not found for Prim path : " << item.first << "\n";
    }
  }



  //
  // Shader attribute evaluation example.
  //
  std::cout << "EvaluateAttribute example -------------\n";
  for (const auto &item : preadermap) {
    // Returned Prim is Shader class
    nonstd::expected<const tinyusdz::Prim*, std::string> shader = stage.GetPrimAtPath(tinyusdz::Path(item.first, /* prop name */""));
    if (shader) {
      std::cout << "Shader(UsdPrimvarReader_float2) <" << item.first << "> from Stage:\n";

      const tinyusdz::Shader *sp = shader.value()->as<tinyusdz::Shader>();
      if (sp) { // this should be true though.

        if (const tinyusdz::UsdPrimvarReader_float2 *preader = sp->value.as<tinyusdz::UsdPrimvarReader_float2>()) {

          tinyusdz::tydra::TerminalAttributeValue tav;
          std::string err;
          bool ret = tinyusdz::tydra::EvaluateAttribute(stage, *shader.value(), "inputs:varname", &tav, &err);

          if (!ret) {
            std::cout << "Resolving `inputs:varname` failed: " << err << "\n";
          }

          std::cout << "type = " << tav.type_name() << "\n";

          if (auto pv = tav.as<tinyusdz::value::token>()) {
            std::cout << "inputs:varname = " << (*pv) << "\n";
          }
        }
      }

    } else {
      std::cerr << "Err: " << shader.error() << "\n";
    }
  }



  return 0;
}
