//
// Copyright Contributors to the MaterialX Project
// SPDX-License-Identifier: Apache-2.0
//

#ifndef MATERIALXVIEW_VIEWER_H
#define MATERIALXVIEW_VIEWER_H

#include <MaterialXView/Editor.h>
#include <MaterialXView/RenderPipeline.h>

#include <MaterialXRender/ShaderMaterial.h>
#include <MaterialXRender/Camera.h>
#include <MaterialXRender/GeometryHandler.h>
#include <MaterialXRender/LightHandler.h>
#include <MaterialXRender/ImageHandler.h>
#include <MaterialXRender/Timer.h>

#include <MaterialXCore/Unit.h>

namespace mx = MaterialX;
namespace ng = nanogui;

class DocumentModifiers
{
  public:
    mx::StringMap remapElements;
    mx::StringSet skipElements;
    std::string filePrefixTerminator;
};

class Viewer : public ng::Screen
{
    friend class RenderPipeline;
    friend class GLRenderPipeline;
    friend class MetalRenderPipeline;
  public:
    Viewer(const std::string& materialFilename,
           const std::string& meshFilename,
           const std::string& envRadianceFilename,
           const mx::FileSearchPath& searchPath,
           const mx::FilePathVec& libraryFolders,
           int screenWidth,
           int screenHeight,
           const mx::Color3& screenColor);
    ~Viewer() { }

    // Initialize the viewer for rendering.
    void initialize();

    // Set the rotation of the current mesh as Euler angles.
    void setMeshRotation(const mx::Vector3& rotation)
    {
        _meshRotation = rotation;
    }

    // Set the scale of the current mesh.
    void setMeshScale(float scale)
    {
        _meshScale = scale;
    }

    // Set whether turntable rendering is enabled.
    void setTurntableEnabled(bool val)
    {
        _turntableEnabled = val;
    }

    // Set the total number of steps for one 360 degree rotation.
    void setTurntableSteps(int steps)
    {
        _turntableSteps = steps;
    }

    // Set the world-space position of the camera.
    void setCameraPosition(const mx::Vector3& position)
    {
        _cameraPosition = position;
    }

    // Set the world-space target of the camera.
    void setCameraTarget(const mx::Vector3& target)
    {
        _cameraTarget = target;
    }

    // Set the view angle of the camera.
    void setCameraViewAngle(float angle)
    {
        _cameraViewAngle = angle;
    }

    // Set the zoom scale of the camera.
    void setCameraZoom(float zoom)
    {
        _cameraZoom = zoom;
    }

    // Set the method for specular environment rendering.
    void setSpecularEnvironmentMethod(mx::HwSpecularEnvironmentMethod method)
    {
        _genContext.getOptions().hwSpecularEnvironmentMethod = method;
    }

    // Set the number of environment samples.
    void setEnvSampleCount(int count)
    {
        _lightHandler->setEnvSampleCount(count);
    }

    // Set the environment light intensity.
    void setEnvLightIntensity(float intensity)
    {
        _lightHandler->setEnvLightIntensity(intensity);
    }

    // Set the rotation of the lighting environment about the Y axis.
    void setLightRotation(float rotation)
    {
        _lightRotation = rotation;
    }

    // Enable or disable shadow maps.
    void setShadowMapEnable(bool enable)
    {
        _genContext.getOptions().hwShadowMap = enable;
    }

    // Enable or disable direct lighting.
    void setDirectLightEnable(bool enable)
    {
        _lightHandler->setDirectLighting(enable);
    }

    // Enable or disable drawing environment as the background.
    void setDrawEnvironment(bool enable)
    {
        _drawEnvironment = enable;
    }

    // Set the modifiers to be applied to loaded documents.
    void setDocumentModifiers(const DocumentModifiers& modifiers)
    {
        _modifiers = modifiers;
    }

    // Set the target width for texture baking.
    void setBakeWidth(unsigned int bakeWidth)
    {
        _bakeWidth = bakeWidth;
    }

    // Set the target height for texture baking.
    void setBakeHeight(unsigned int bakeHeight)
    {
        _bakeHeight = bakeHeight;
    }

    // Set the output document filename for texture baking.
    void setBakeFilename(const mx::FilePath& bakeFilename)
    {
        _bakeFilename = bakeFilename;
    }

    // Enable or disable frame timing.
    void setFrameTiming(bool enable)
    {
        _frameTiming = enable;
    }

    // Reset frame timing after a blocking event.
    void resetFrameTiming()
    {
        if (_frameTiming)
        {
            _frameTimer.startTimer();
            _avgFrameTime = 0.0;
        }
    }

    // Return true if all inputs should be shown in the property editor.
    bool getShowAllInputs() const
    {
        return _showAllInputs;
    }

    // Return the underlying NanoGUI window.
    ng::ref<ng::Window> getWindow() const
    {
        return _window;
    }

    // Return the active image handler.
    mx::ImageHandlerPtr getImageHandler() const
    {
        return _imageHandler;
    }

    // Return the selected material.
    mx::MaterialPtr getSelectedMaterial() const
    {
        if (_selectedMaterial < _materials.size())
        {
            return _materials[_selectedMaterial];
        }
        return nullptr;
    }

    // Return the selected mesh partition.
    mx::MeshPartitionPtr getSelectedGeometry() const
    {
        if (_selectedGeom < _geometryList.size())
        {
            return _geometryList[_selectedGeom];
        }
        return nullptr;
    }

    // Request a capture of the current frame, writing it to the given filename.
    void requestFrameCapture(const mx::FilePath& filename)
    {
        _captureRequested = true;
        _captureFilename = filename;
    }

    // Request that the viewer be closed after the next frame is rendered.
    void requestExit()
    {
        _exitRequested = true;
    }

    // Bake textures to disk using the current render pipeline.
    void bakeTextures()
    {
        _renderPipeline->bakeTextures();
    }

  private:
    void draw_contents() override;
    bool keyboard_event(int key, int scancode, int action, int modifiers) override;
    bool scroll_event(const ng::Vector2i& p, const ng::Vector2f& rel) override;
    bool mouse_motion_event(const ng::Vector2i& p, const ng::Vector2i& rel, int button, int modifiers) override;
    bool mouse_button_event(const ng::Vector2i& p, int button, bool down, int modifiers) override;
    
    void initContext(mx::GenContext& context);
    void loadMesh(const mx::FilePath& filename);
    void loadEnvironmentLight();
    void applyDirectLights(mx::DocumentPtr doc);
    void loadDocument(const mx::FilePath& filename, mx::DocumentPtr libraries);
    void reloadShaders();
    void loadStandardLibraries();
    void saveShaderSource(mx::GenContext& context);
    void loadShaderSource();
    void saveDotFiles();

    // Compute the resolution for texture baking.
    mx::UnsignedIntPair computeBakingResolution(mx::ConstDocumentPtr doc);
    
    // Translate the current material to the target shading model.
    mx::DocumentPtr translateMaterial();

    // Assign the given material to the given geometry, or remove any
    // existing assignment if the given material is nullptr.
    void assignMaterial(mx::MeshPartitionPtr geometry, mx::MaterialPtr material);

    // Mark the given material as currently selected in the viewer.
    void setSelectedMaterial(mx::MaterialPtr material)
    {
        for (size_t i = 0; i < _materials.size(); i++)
        {
            if (material == _materials[i])
            {
                _selectedMaterial = i;
                break;
            }
        }
    }

    // Generate a base output filepath for data derived from the current material.
    mx::FilePath getBaseOutputPath();

    // Return an element predicate for documents written from the viewer.
    mx::ElementPredicate getElementPredicate();

    void initCamera();
    void updateCameras();
    void updateGeometrySelections();
    void updateMaterialSelections();
    void updateMaterialSelectionUI();
    void updateDisplayedProperties();

    void createLoadMeshInterface(ng::ref<Widget> parent, const std::string& label);
    void createLoadMaterialsInterface(ng::ref<Widget> parent, const std::string& label);
    void createLoadEnvironmentInterface(ng::ref<Widget> parent, const std::string& label);
    void createSaveMaterialsInterface(ng::ref<Widget> parent, const std::string& label);
    void createPropertyEditorInterface(ng::ref<Widget> parent, const std::string& label);
    void createAdvancedSettings(ng::ref<Widget> parent);
    void createDocumentationInterface(ng::ref<Widget> parent);

    // Return the ambient occlusion image, if any, associated with the given material.
    mx::ImagePtr getAmbientOcclusionImage(mx::MaterialPtr material);
    
    // Split the given radiance map into indirect and direct components,
    // returning a new indirect map and directional light document.
    void splitDirectLight(mx::ImagePtr envRadianceMap, mx::ImagePtr& indirectMap, mx::DocumentPtr& dirLightDoc);

    mx::MaterialPtr getEnvironmentMaterial();
    mx::MaterialPtr getWireframeMaterial();

    mx::ImagePtr getShadowMap();
    void invalidateShadowMap();

    mx::ImagePtr renderWedge();
    void renderTurnable();
    void renderScreenSpaceQuad(mx::MaterialPtr material);

    // Update the directional albedo table.
    void updateAlbedoTable();

    // Toggle turntable
    void toggleTurntable(bool enable);

    // Set shader interface type
    void setShaderInterfaceType(mx::ShaderInterfaceType interfaceType);

  private:
    ng::ref<ng::Window> _window;
    RenderPipelinePtr _renderPipeline;

    mx::FilePath _materialFilename;
    mx::FileSearchPath _materialSearchPath;
    mx::FilePath _meshFilename;
    mx::FilePath _envRadianceFilename;

    mx::FileSearchPath _searchPath;
    mx::FilePathVec _libraryFolders;

    mx::Vector3 _meshTranslation;
    mx::Vector3 _meshRotation;
    float _meshScale;

    bool _turntableEnabled;
    int _turntableSteps;
    int _turntableStep;
    mx::ScopedTimer _turntableTimer;

    mx::Vector3 _cameraPosition;
    mx::Vector3 _cameraTarget;
    mx::Vector3 _cameraUp;
    float _cameraViewAngle;
    float _cameraNearDist;
    float _cameraFarDist;
    float _cameraZoom;

    bool _userCameraEnabled;
    mx::Vector3 _userTranslation;
    mx::Vector3 _userTranslationStart;
    bool _userTranslationActive;
    mx::Vector2 _userTranslationPixel;

    // Document management
    mx::DocumentPtr _stdLib;
    DocumentModifiers _modifiers;
    mx::StringSet _xincludeFiles;

    // Lighting information
    mx::FilePath _lightRigFilename;
    mx::DocumentPtr _lightRigDoc;
    float _lightRotation;

    // Light processing options
    bool _normalizeEnvironment;
    bool _splitDirectLight;
    bool _generateReferenceIrradiance;
    bool _saveGeneratedLights;

    // Shadow mapping
    mx::MaterialPtr _shadowMaterial;
    mx::MaterialPtr _shadowBlurMaterial;
    mx::ImagePtr _shadowMap;
    unsigned int _shadowSoftness;

    // Ambient occlusion
    float _ambientOcclusionGain;

    // Geometry selections
    std::vector<mx::MeshPartitionPtr> _geometryList;
    size_t _selectedGeom;
    ng::ref<ng::Label> _geomLabel;
    ng::ref<ng::ComboBox> _geometrySelectionBox;

    // Material selections
    std::vector<mx::MaterialPtr> _materials;
    mx::MaterialPtr _wireMaterial;
    size_t _selectedMaterial;
    ng::ref<ng::Label> _materialLabel;
    ng::ref<ng::ComboBox> _materialSelectionBox;
    PropertyEditor _propertyEditor;

    // Material assignments
    std::map<mx::MeshPartitionPtr, mx::MaterialPtr> _materialAssignments;

    // Cameras
    mx::CameraPtr _identityCamera;
    mx::CameraPtr _viewCamera;
    mx::CameraPtr _envCamera;
    mx::CameraPtr _shadowCamera;

    // Resource handlers
    mx::GeometryHandlerPtr _geometryHandler;
    mx::ImageHandlerPtr _imageHandler;
    mx::LightHandlerPtr _lightHandler;

    // Supporting materials and geometry.
    mx::GeometryHandlerPtr _envGeometryHandler;
    mx::MaterialPtr _envMaterial;
    mx::MeshPtr _quadMesh;

    // Type system
    mx::TypeSystemPtr _typeSystem;

    // Shader generator contexts
    mx::GenContext _genContext;
#ifndef MATERIALXVIEW_METAL_BACKEND
    mx::GenContext _genContextEssl;
#endif
#if MATERIALX_BUILD_GEN_OSL
    mx::GenContext _genContextOsl;
#endif
#if MATERIALX_BUILD_GEN_MDL
    mx::GenContext _genContextMdl;
#endif
    // Unit registry
    mx::UnitConverterRegistryPtr _unitRegistry;

    // Viewing options
    bool _drawEnvironment;
    bool _outlineSelection;

    // Render options
    bool _renderTransparency;
    bool _renderDoubleSided;
    
    // Framebuffer Color Texture
    void* _colorTexture;

    // Scene options
    mx::StringVec _distanceUnitOptions;
    mx::LinearUnitConverterPtr _distanceUnitConverter;

    // Mesh loading options
    bool _splitByUdims;

    // Material loading options
    bool _mergeMaterials;
    bool _showAllInputs;
    bool _flattenSubgraphs;

    // Shader translation
    std::string _targetShader;

    // Frame capture
    bool _captureRequested;
    mx::FilePath _captureFilename;
    bool _exitRequested;

    // Wedge rendering
    bool _wedgeRequested;
    mx::FilePath _wedgeFilename;
    std::string _wedgePropertyName;
    float _wedgePropertyMin;
    float _wedgePropertyMax;
    unsigned int _wedgeImageCount;

    // Texture baking
    bool _bakeHdr;
    bool _bakeAverage;
    bool _bakeOptimize;
    bool _bakeRequested;
    unsigned int _bakeWidth;
    unsigned int _bakeHeight;
    bool _bakeDocumentPerMaterial;
    mx::FilePath _bakeFilename;

    // Frame timing
    bool _frameTiming;
    ng::ref<ng::Label> _timingLabel;
    ng::ref<ng::Widget> _timingPanel;
    ng::ref<ng::TextBox> _timingText;
    mx::ScopedTimer _frameTimer;
    double _avgFrameTime;

    // Documentation UI
    ng::ref<ng::Button> _shortcutsButton;
    ng::ref<ng::Widget> _shortcutsTable;
};

extern const mx::Vector3 DEFAULT_CAMERA_POSITION;
extern const float DEFAULT_CAMERA_VIEW_ANGLE;
extern const float DEFAULT_CAMERA_ZOOM;

#endif // MATERIALXVIEW_VIEWER_H
