// ======================================================================== //
// Copyright 2019 Ingo Wald                                                 //
//                                                                          //
// Licensed under the Apache License, Version 2.0 (the "License");          //
// you may not use this file except in compliance with the License.         //
// You may obtain a copy of the License at                                  //
//                                                                          //
//     http://www.apache.org/licenses/LICENSE-2.0                           //
//                                                                          //
// Unless required by applicable law or agreed to in writing, software      //
// distributed under the License is distributed on an "AS IS" BASIS,        //
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. //
// See the License for the specific language governing permissions and      //
// limitations under the License.                                           //
// ======================================================================== //

#pragma once

#include "pbrtParser/Scene.h"
#include "../syntactic/Scene.h"
// std
#include <map>
#include <sstream>

namespace pbrt {

  using PBRTScene = pbrt::syntactic::Scene;
  using pbrt::syntactic::ParamArray;

  /*! helper function - return whether file ends with given extension */
  inline bool endsWith(const std::string &s, const std::string &suffix)
  {
    return s.substr(s.size()-suffix.size(),suffix.size()) == suffix;
  }

  /*! The class that "semantically" parses a syntactic::Scene into a
      semantic::Scene. In a syntactic scene we know only the
      high-level types of objects (e.g., that something is a array of
      floats, or a "parameter" of type "float" with name "radius" and
      value "1.0", or a "shape" with a given "type" string and some
      set of parameters), but do not know what those data actually
      mean (e.g., that that array of floats is an array or vertices in
      a triangle mesh, that this "float radius 1.0" is a
      SphereShape::radius, or that the given shape is a
      TriangleMeshShape etc. The semantic parser si the class that
      translates from this syntactic thingy to actual C++ classes */
  struct SemanticParser
  {
    /*! the resulting _semantic_ scene we're building */
    Scene::SP result;
    
    /*! the _syntatic_ scnee we're parsing (our _input_) */
    PBRTScene::SP pbrtScene;

    /*! constructor that also perfoms all the work - converts the
      input 'pbrtScene' to a naivescenelayout, and assings that to
      'result' */
    SemanticParser(PBRTScene::SP pbrtScene)
      : pbrtScene(pbrtScene)
    {
      result        = std::make_shared<Scene>();
      result->world  = findOrEmitObject(pbrtScene->world);

      if (!unhandledShapeTypeCounter.empty()) {
        std::cerr << "WARNING: scene contained some un-handled shapes!" << std::endl;
        for (auto type : unhandledShapeTypeCounter)
          std::cerr << " - " << type.first << " : " << type.second << " occurrances" << std::endl;
      }
    }

  private:
    // ==================================================================
    // Textures
    // ==================================================================
    std::map<pbrt::syntactic::Texture::SP,Texture::SP> textureMapping;

    /*! do create a track representation of given texture, _without_
      checking whether that was already created */
    Texture::SP createTextureFrom(pbrt::syntactic::Texture::SP in);
    /*! @{ specializing parsing function for given texture type */
    Texture::SP createTexture_mix(pbrt::syntactic::Texture::SP in);
    Texture::SP createTexture_scale(pbrt::syntactic::Texture::SP in);
    Texture::SP createTexture_ptex(pbrt::syntactic::Texture::SP in);
    Texture::SP createTexture_constant(pbrt::syntactic::Texture::SP in);
    Texture::SP createTexture_checker(pbrt::syntactic::Texture::SP in);
    /*! @} */
    Texture::SP findOrCreateTexture(pbrt::syntactic::Texture::SP in);
    
    // ==================================================================
    // LigthSources
    // ==================================================================
    std::map<pbrt::syntactic::LightSource::SP,LightSource::SP> lightSourceMapping;
    
    /*! do create a track representation of given light, _without_
      checking whether that was already created */
    LightSource::SP createLightSourceFrom(pbrt::syntactic::LightSource::SP in);

    /*! check if this light has already been imported, and if so,
      find what we imported, and reutrn this. otherwise import and
      store for later use.
      
      important: it is perfectly OK for this light to be a null
      object - the area ligths in moana have this features, for
      example */
    LightSource::SP findOrCreateLightSource(pbrt::syntactic::LightSource::SP in);
    LightSource::SP createLightSource_infinite(pbrt::syntactic::LightSource::SP in);
    LightSource::SP createLightSource_distant(pbrt::syntactic::LightSource::SP in);
    LightSource::SP createLightSource_point(pbrt::syntactic::LightSource::SP in);
    LightSource::SP createLightSource_spot(pbrt::syntactic::LightSource::SP in);

    // ==================================================================
    // Materials
    // ==================================================================
    std::map<pbrt::syntactic::Material::SP,Material::SP> materialMapping;

    /*! @{ type-specific extraction routines (ie, we already know the
        type, and only have to extract the potential/expected
        parameters) */
    Material::SP createMaterial_hair(pbrt::syntactic::Material::SP in);
    Material::SP createMaterial_uber(pbrt::syntactic::Material::SP in);
    Material::SP createMaterial_metal(pbrt::syntactic::Material::SP in);
    Material::SP createMaterial_matte(pbrt::syntactic::Material::SP in);
    Material::SP createMaterial_fourier(pbrt::syntactic::Material::SP in);
    Material::SP createMaterial_mirror(pbrt::syntactic::Material::SP in);
    Material::SP createMaterial_substrate(pbrt::syntactic::Material::SP in);
    Material::SP createMaterial_disney(pbrt::syntactic::Material::SP in);
    Material::SP createMaterial_mix(pbrt::syntactic::Material::SP in);
    Material::SP createMaterial_plastic(pbrt::syntactic::Material::SP in);
    Material::SP createMaterial_translucent(pbrt::syntactic::Material::SP in);
    Material::SP createMaterial_glass(pbrt::syntactic::Material::SP in);
    /*! @} */
    
    /*! do create a track representation of given material, _without_
      checking whether that was already created */
    Material::SP createMaterialFrom(pbrt::syntactic::Material::SP in);

    /*! check if this material has already been imported, and if so,
      find what we imported, and reutrn this. otherwise import and
      store for later use.
      
      important: it is perfectly OK for this material to be a null
      object - the area ligths in moana have this features, for
      example */
    Material::SP findOrCreateMaterial(pbrt::syntactic::Material::SP in);

    /*! counter that tracks, for each un-handled shape type, how many
      "instances" of that shape type it could not create (note those
      are not _real_ instances in the ray tracing sense, just
      "occurrances" of that shape type in the scene graph,
      _before_ object instantiation */
    std::map<std::string,size_t> unhandledShapeTypeCounter;
    
    // ==================================================================
    // Geometry: shapes, instances, objects
    // ==================================================================

    std::map<pbrt::syntactic::Object::SP,Object::SP> emittedObjects;
    std::map<pbrt::syntactic::Shape::SP,Shape::SP>   emittedShapes;
    
    AreaLight::SP parseAreaLight(pbrt::syntactic::AreaLightSource::SP in);
    
    /*! check if object has already been emitted, and return reference
        is so; else emit new and return reference */
    Object::SP findOrEmitObject(pbrt::syntactic::Object::SP pbrtObject);
    
    /*! check if shapehas already been emitted, and return reference
        is so; else emit new and return reference */
    Shape::SP findOrCreateShape(pbrt::syntactic::Shape::SP pbrtShape);

    /*! emit given instance */
    Instance::SP emitInstance(pbrt::syntactic::Object::Instance::SP pbrtInstance);
    
    /*! emit given shape */
    Shape::SP emitShape(pbrt::syntactic::Shape::SP shape);

    /*! @{ type specific extraction routines for very specific shape type */
    Shape::SP emitPlyMesh(pbrt::syntactic::Shape::SP shape);
    Shape::SP emitDisk(pbrt::syntactic::Shape::SP shape);
    Shape::SP emitSphere(pbrt::syntactic::Shape::SP shape);
    Shape::SP emitCurve(pbrt::syntactic::Shape::SP shape);
    Shape::SP emitTriangleMesh(pbrt::syntactic::Shape::SP shape);
    /*! @} */


    
    // ==================================================================
    // General helper stuff
    // ==================================================================

    /*! extract 'texture' parameters from shape, and assign to shape */
    void extractTextures(Shape::SP geom, pbrt::syntactic::Shape::SP shape);

    template<typename T>
    std::vector<T> extractVector(pbrt::syntactic::Shape::SP shape, const std::string &name)
    {
      std::vector<T> result;
      typename ParamArray<typename T::scalar_t>::SP param = shape->findParam<typename T::scalar_t>(name);
      if (param) {

        int dims = sizeof(T)/sizeof(typename T::scalar_t);
        size_t num = param->size() / dims;// / T::dims;
        const T *const data = (const T*)param->data();
        
        result.resize(num);
        std::copy(data,data+num,result.begin());
      }
      return result;
    }
    
  };

  /*! iterate through syntactic parameters and find value fov field */
  float findCameraFov(pbrt::syntactic::Camera::SP camera);

  /*! parse syntactic camera's 'film' value */
  void createFilm(Scene::SP ours, pbrt::syntactic::Scene::SP pbrt);
  
  /*! create a scene->camera from the pbrt model, if specified, or
    leave unchanged if not */
  void createCamera(Scene::SP scene, pbrt::syntactic::Scene::SP pbrt);

  /*! create semantic camera from syntactic one */
  Camera::SP createCamera(pbrt::syntactic::Camera::SP camera);

} // ::pbrt
