// Copyright 2020 Intel Corporation
// SPDX-License-Identifier: Apache-2.0

#pragma once

#include "ISurfaceVolumeField.h"
#include "Field.h"
#include "FieldStatistics.h"

#define FIELD_FILE_HEADER_STRING "OPENPGL_" OPENPGL_VERSION_STRING "_FIELD"

namespace openpgl
{


template<int Vecsize, class TDirectionalDistributionFactory, template<typename, typename> class TSpatialStructureBuilder, typename TSurfaceSamplingDistribution, typename TVolumeSamplingDistribution>
struct SurfaceVolumeField: public ISurfaceVolumeField
{

private:

    using FieldType = Field<Vecsize, TDirectionalDistributionFactory, TSpatialStructureBuilder>;
    using SampleContainer = SampleDataStorage::SampleDataContainer;
    using SampleContainerIntern = std::vector<SampleData>;
public:

    using Settings = typename FieldType::Settings;
    using RegionType = typename FieldType::RegionType;
    using DirectionalDistribution = typename FieldType::DirectionalDistribution;
public:

    SurfaceVolumeField() = default;

    SurfaceVolumeField(const Settings &settings):
        m_surfaceField(settings),
        m_volumeField(settings)
    { 
        m_surfaceField.setIsSurface(true);
        m_volumeField.setIsSurface(false);
    }

    ~SurfaceVolumeField() override
    {}

    ISurfaceSamplingDistribution* newSurfaceSamplingDistribution() const override
    {
        return new TSurfaceSamplingDistribution();
    }

    bool initSurfaceSamplingDistribution(ISurfaceSamplingDistribution* surfaceSamplingDistribution, const Point3& position, float* sample1D) const override
    {
        TSurfaceSamplingDistribution* _surfaceSamplingDistribution = (TSurfaceSamplingDistribution*)surfaceSamplingDistribution;
        uint32_t id = -1;
        const RegionType* region = m_surfaceField.getRegion(position, sample1D, id);
        if(!region || !region->valid)
        {
            return false;
        }
        const DirectionalDistribution* distribution = &region->distribution;
        _surfaceSamplingDistribution->init(distribution, position);
        _surfaceSamplingDistribution->setId(id);
        _surfaceSamplingDistribution->setRegion(region);
        return true;
    }

    IVolumeSamplingDistribution* newVolumeSamplingDistribution() const override
    {
        return new TVolumeSamplingDistribution();
    }

    bool initVolumeSamplingDistribution(IVolumeSamplingDistribution* volumeSamplingDistribution, const Point3& position, float* sample1D) const override
    {
        TVolumeSamplingDistribution* _volumeSamplingDistribution = (TVolumeSamplingDistribution*)volumeSamplingDistribution;
        uint32_t id = -1;
        const RegionType* region = m_volumeField.getRegion(position, sample1D, id);
        if(!region || !region->valid)
        {
            return false;
        }
        const DirectionalDistribution* distribution = region->getDistribution(position);
        _volumeSamplingDistribution->init(distribution, position);
        _volumeSamplingDistribution->setId(id);
        _volumeSamplingDistribution->setRegion(region);
        return true;
    }

    void setSceneBounds(const openpgl::BBox &sceneBounds) override
    {
        openpgl::BBox scaledSceneBounds = sceneBounds;
        scaledSceneBounds.enlarge_by(1.01f);
        m_surfaceField.setSceneBounds(scaledSceneBounds);
        m_volumeField.setSceneBounds(scaledSceneBounds);
    }

    openpgl::BBox getSceneBounds() const override
    {
        openpgl::BBox sceneBounds = m_surfaceField.getSceneBounds();
        sceneBounds.extend(m_volumeField.getSceneBounds());
        return sceneBounds;
    }

    void updateField(SampleContainer& samplesSurface, SampleContainer& samplesVolume) override
    {
    #if TBB_INTERFACE_VERSION < 12010
        // we need to initialize the task_scheduler in the context to avoid
        // asyncronous deconsrution of the implicit initialized tbb::arenas and tbb::streams
        tbb::task_scheduler_init anonymous;
    #endif
        if(samplesSurface.size() > 0) {
            if(!m_surfaceField.isInitialized()) {
                m_surfaceField.buildField(samplesSurface);
            } else {
                m_surfaceField.updateField(samplesSurface);
            }
        }
        if(samplesVolume.size() > 0) {
            if(!m_volumeField.isInitialized()) {
                m_volumeField.buildField(samplesVolume);
            } else {
                m_volumeField.updateField(samplesVolume);
            }
        }
        m_iteration++;
    }

    void updateFieldSurface(SampleContainer& samplesSurface) override
    {
        if(samplesSurface.size() > 0) {
            if(!m_surfaceField.isInitialized()) {
                m_surfaceField.buildField(samplesSurface);
            } else {
                m_surfaceField.updateField(samplesSurface);
            }
        }
        m_iteration++;
    }

    void updateFieldVolume(SampleContainer& samplesVolume) override
    {
        if(samplesVolume.size() > 0) {
            if(!m_volumeField.isInitialized()) { 
                m_volumeField.buildField(samplesVolume);
            } else {
                m_volumeField.updateField(samplesVolume);
            }
        }
        m_iteration++;
    }

    void resetField() override
    {
        m_iteration = 0;
        m_totalSPP = 0;
        m_surfaceField.resetField();
        m_volumeField.resetField();
    }

    PGL_SPATIAL_STRUCTURE_TYPE getSpatialStructureType() const override {
        return FieldType::SpatialStructureBuilder::SPATIAL_STRUCTURE_TYPE;
    }

    PGL_DIRECTIONAL_DISTRIBUTION_TYPE getDirectionalDistributionType() const override {
        return FieldType::DirectionalDistributionFactory::DIRECTIONAL_DISTRIBUTION_TYPE;
    }

    size_t getIteration() const override
    {
        return m_iteration;
    }

    void serialize(std::ostream &os) const override
    {
        os.write(reinterpret_cast<const char*>(&m_iteration), sizeof(m_iteration));
        os.write(reinterpret_cast<const char*>(&m_totalSPP), sizeof(m_totalSPP));
        m_surfaceField.serialize(os);
        m_volumeField.serialize(os);
    }

    void deserialize(std::istream &is) override
    {
        is.read(reinterpret_cast<char*>(&m_iteration), sizeof(m_iteration));
        is.read(reinterpret_cast<char*>(&m_totalSPP), sizeof(m_totalSPP));
        m_surfaceField.deserialize(is);
        m_volumeField.deserialize(is);
    }

    virtual bool validate(const bool checkSurface, const bool checkVolume) const override
    {
        bool valid = true;
        if(m_surfaceField.isInitialized())
            valid = valid & m_surfaceField.isValid();
        if(m_volumeField.isInitialized())
            valid = valid & m_volumeField.isValid();
        return valid;
    }

    void storeToFile(const std::string fieldFileName) const override {
        std::filebuf fb;
        fb.open (fieldFileName, std::ios::out | std::ios::binary);
        if (!fb.is_open()) throw std::runtime_error("error: couldn't open file!");
        std::ostream os(&fb);

        os.write(FIELD_FILE_HEADER_STRING, strlen(FIELD_FILE_HEADER_STRING) + 1);

        auto spatialStructureType = FieldType::SpatialStructureBuilder::SPATIAL_STRUCTURE_TYPE;
        os.write(reinterpret_cast<const char*>(&spatialStructureType), sizeof(spatialStructureType));
        auto directionalDistributionType = FieldType::DirectionalDistributionFactory::DIRECTIONAL_DISTRIBUTION_TYPE;
        os.write(reinterpret_cast<const char*>(&directionalDistributionType), sizeof(directionalDistributionType));

        serialize(os);

        os.flush();
        fb.close();
    }

    virtual bool operator==(const ISurfaceVolumeField* b) const override {
        bool equal = true;
        const SurfaceVolumeField* fieldB = dynamic_cast<const SurfaceVolumeField*>(b);
        if (!fieldB || m_iteration != fieldB->m_iteration ||
            m_totalSPP != fieldB->m_totalSPP ||
            !m_surfaceField.operator==(fieldB->m_surfaceField) ||
            !m_volumeField.operator==(fieldB->m_volumeField)) {
                equal = false;
            }
        return equal;
    }

    FieldStatistics* getSurfaceStatistics() const override
    {
        FieldStatistics* stats =  m_surfaceField.getStatistics();
        return stats;
    }

	FieldStatistics* getVolumeStatistics() const override
    {
        FieldStatistics* stats =  m_volumeField.getStatistics();
        return stats;
    }
    
private:
    size_t m_iteration {0};
    size_t m_totalSPP  {0};
    
    FieldType m_surfaceField;
    FieldType m_volumeField;
};

}
