#pragma once
#include <Loader.hpp>
#include <Camera.hpp>
#include <Renderer.h>
#include <string>
#include <memory>

class SceneLoader {
public:
    ~SceneLoader() {
        camera.reset();
        volume.reset();
        importanceSDF.reset();
        renderers.clear();
        lights.clear();
    }

    void loadSceneFile(const std::string& filename) {
        std::string line;
        std::ifstream file(filename);
        if (file.is_open()) {
            auto pos = filename.find_last_of('/');
            pathToFile = filename.substr(0,pos + 1);
            renderers = std::vector<std::unique_ptr<Renderer>>(0);
            lights = std::vector<CUDA::PointLight>(0);
            while (getline(file, line)) {
                evaluateLine(line);
            }
            file.close();
        } else {
            throw std::runtime_error("File not found!");
        }
    }

    std::unique_ptr<Renderer> getFirstRenderer() {
        return std::move(renderers[0]);
    }

    std::vector<std::unique_ptr<Renderer>> getAllRenderers() {
        return std::move(renderers);
    }

    std::unique_ptr<Camera> getCamera() {
        return std::move(camera);
    }

    std::unique_ptr<Volume> getVolume() {
        return std::move(volume);
    }

    std::unique_ptr<Volume> getImportanceSDF() {
        return std::move(importanceSDF);
    }

    std::unique_ptr<TransferFunction> getTransferFunction() {
        return std::move(transferFunction);
    }

    std::vector<CUDA::PointLight> getLight() {
        return lights;
    }

    [[nodiscard]] int getWidth() const {
        return width;
    }

    [[nodiscard]] int getHeight() const {
        return height;
    }

    CUDA::OptimizationParameters getOptParams() {
        return optParams;
    }

    float4 getVolumeProperties() {
        return volumeProperties;
    }

private:
    std::string pathToFile;

    std::unique_ptr<Camera> camera = nullptr;
    std::unique_ptr<Volume> volume = nullptr;
    std::unique_ptr<Volume> importanceSDF = nullptr;
    std::vector<std::unique_ptr<Renderer>> renderers = std::vector<std::unique_ptr<Renderer>>(0);
    std::unique_ptr<TransferFunction> transferFunction = nullptr;

    std::vector<CUDA::PointLight> lights = std::vector<CUDA::PointLight>(0);

    float4 volumeProperties = make_float4(30, 0.3, 0.7, 0);

    int width = -1;
    int height = -1;
    CUDA::OptimizationParameters optParams = {1, 70, 30, 1, 0.3, 1, false, false, false, 20, 1};

    void evaluateLine(const std::string& line) {
        if (line.empty()) {
            return;
        }
        if (line[0] == '#') {
            // comment
            return;
        }
        std::stringstream ss(line);
        std::string word;
        std::string equals;
        ss >> word;
        ss >> equals;
        if (word == "Camera") {
            evaluateCamera(ss);
            return;
        }
        if (word == "Light") {
            evaluateLight(ss);
            return;
        }
        if (word == "Volume") {
            evaluateVolume(ss);
            return;
        }
        if (word == "ImportanceSDF") {
            evaluateImportanceSDF(ss);
            return;
        }
        if (word == "VolumeProperties") {
            evaluateVolumeProperties(ss);
            return;
        }
        if (word == "TransferFunction") {
            evaluateTransferFunction(ss);
            return;
        }
        if (word == "OptParams") {
            evaluateOptimizationParams(ss);
            return;
        }
        if (word == "Film") {
            evaluateFilm(ss);
            return;
        }
        if (word == "Renderer") {
            evaluateRenderer(ss);
            return;
        }

        throw std::runtime_error("Undefined attribute");
    }

    void evaluateCamera(std::stringstream& ss) {
        if (ss.eof()) {
            camera = std::make_unique<Camera>();
            return;
        }
        std::string temp;

        bool fps = false;
        float3 pos;
        float3 front;
        float3 up;

        while (!ss.eof()) {
            ss >> temp;
            if (temp == "\"Pos\"") {

                if (ss.eof()) {
                    throw std::runtime_error("Position defined wrong");
                }
                ss >> temp;
                float x = std::stof(temp);
                if (ss.eof()) {
                    throw std::runtime_error("Position defined wrong");
                }
                ss >> temp;
                float y = std::stof(temp);
                if (ss.eof()) {
                    throw std::runtime_error("Position defined wrong");
                }
                ss >> temp;
                float z = std::stof(temp);
                pos = make_float3(x, y, z);
                continue;
            }
            if (temp == "\"Up\"") {

                if (ss.eof()) {
                    throw std::runtime_error("Position defined wrong");
                }
                ss >> temp;
                float x = std::stof(temp);
                if (ss.eof()) {
                    throw std::runtime_error("Position defined wrong");
                }
                ss >> temp;
                float y = std::stof(temp);
                if (ss.eof()) {
                    throw std::runtime_error("Position defined wrong");
                }
                ss >> temp;
                float z = std::stof(temp);
                up = make_float3(x, y, z);
                continue;
            }
            if (temp == "\"Front\"") {

                if (ss.eof()) {
                    throw std::runtime_error("Position defined wrong");
                }
                ss >> temp;
                float x = std::stof(temp);
                if (ss.eof()) {
                    throw std::runtime_error("Position defined wrong");
                }
                ss >> temp;
                float y = std::stof(temp);
                if (ss.eof()) {
                    throw std::runtime_error("Position defined wrong");
                }
                ss >> temp;
                float z = std::stof(temp);
                front = make_float3(x, y, z);
                continue;
            }
            if (temp == "\"FPS\"") {
                ss >> temp;
                fps = (temp == "TRUE");
                continue;
            }
            throw std::runtime_error("Camera: Unknown argument");
        }
        if (fps) {
            camera = std::make_unique<Camera>(pos, up, front);
        } else {
            camera = std::make_unique<Camera>(pos);
        }
    }

    void evaluateLight(std::stringstream& ss) {
        float3 pos = make_float3(0, 2, 0);
        float scale = 1;
        if (ss.eof()) {
            lights.push_back({scale, pos});
            return;
        }
        std::string temp;
        while (!ss.eof()) {
            ss >> temp;
            if (temp == "\"Pos\"") {
                if (ss.eof()) {
                    throw std::runtime_error("Position defined wrong");
                }
                ss >> temp;
                float x = std::stof(temp);
                if (ss.eof()) {
                    throw std::runtime_error("Position defined wrong");
                }
                ss >> temp;
                float y = std::stof(temp);
                if (ss.eof()) {
                    throw std::runtime_error("Position defined wrong");
                }
                ss >> temp;
                float z = std::stof(temp);
                pos = make_float3(x, y, z);
                continue;
            }
            if (temp == "\"Scale\"") {
                ss >> temp;
                try {
                    scale = std::stof(temp);
                    continue;
                } catch (std::invalid_argument const&) {
                    throw std::runtime_error("Scale defined wrong");
                }
            }
            throw std::runtime_error("Light: Unknown argument");
        }
        lights.push_back({scale, pos});
    }

    void evaluateVolume(std::stringstream& ss) {
        std::string temp;
        ss >> temp;

        auto pos = temp.find_last_of('.');
        auto loaderName = temp.substr(pos + 1, temp.size());

        std::unique_ptr<Loader> loader;
        if (loaderName == "mhd") {
            loader = std::make_unique<MHDLoader>();
        } else {
            throw std::runtime_error("Filetype not recognised");
        }

        volume = loader->loadFile(pathToFile + temp);
        loader.reset();
        if (volume == nullptr) {
            throw std::runtime_error("Error in volume creation");
        }
    }

    void evaluateImportanceSDF(std::stringstream& ss) {
        std::string temp;
        ss >> temp;

        auto pos = temp.find_last_of('.');
        auto loaderName = temp.substr(pos + 1, temp.size());

        std::unique_ptr<Loader> loader;
        if (loaderName == "mhd") {
            loader = std::make_unique<MHDLoader>();
        } else {
            throw std::runtime_error("Filetype not recognised");
        }

        importanceSDF = loader->loadFile(pathToFile + temp);
        loader.reset();
        if (importanceSDF == nullptr) {
            throw std::runtime_error("Error in SDF creation");
        }
    }

    void evaluateVolumeProperties(std::stringstream& ss) {
        if (ss.eof()) {
            return;
        }

        float density = 0;
        float absorption = 0;
        float scattering = 0;
        float g = 0;
        std::string temp;
        std::string value;
        while (!ss.eof()) {
            ss >> temp;
            ss >> value;
            if (temp == "\"Density\"") {
                try {
                    density = std::stof(value);
                    continue;
                } catch (std::invalid_argument const&) {
                    throw std::runtime_error("Density defined wrong");
                }
            }
            if (temp == "\"Absorption\"") {
                try {
                    absorption = std::stof(value);
                    continue;
                } catch (std::invalid_argument const&) {
                    throw std::runtime_error("Density defined wrong");
                }
            }
            if (temp == "\"Scattering\"") {
                try {
                    scattering = std::stof(value);
                    continue;
                } catch (std::invalid_argument const&) {
                    throw std::runtime_error("Density defined wrong");
                }
            }
            if (temp == "\"G\"") {
                try {
                    g = std::stof(value);
                    continue;
                } catch (std::invalid_argument const&) {
                    throw std::runtime_error("Density defined wrong");
                }
            }
            throw std::runtime_error("VolumeProperties: Unknown argument");
        }
        volumeProperties = make_float4(density, absorption, scattering, g);
    }

    void evaluateOptimizationParams(std::stringstream& ss) {
        std::string temp;
        std::string value;

        float p = -1;
        float q = -1;
        float r = -1;
        float lambda = -1;
        float min = -1;
        float max = -1;
        bool view = false;
        bool light = false;
        bool phong = false;
        float shiny = -1;
        float scale = -1;

        while (!ss.eof()) {
            ss >> temp;
            ss >> value;

            if (temp == "\"P\"") {
                try {
                    p = std::stof(value);
                    continue;
                } catch (std::invalid_argument const&) {
                    throw std::runtime_error("P defined wrong");
                }
            }
            if (temp == "\"Q\"") {
                try {
                    q = std::stof(value);
                    continue;
                } catch (std::invalid_argument const&) {
                    throw std::runtime_error("Q defined wrong");
                }
            }
            if (temp == "\"R\"") {
                try {
                    r = std::stof(value);
                    continue;
                } catch (std::invalid_argument const&) {
                    throw std::runtime_error("R defined wrong");
                }
            }
            if (temp == "\"L\"") {
                try {
                    lambda = std::stof(value);
                    continue;
                } catch (std::invalid_argument const&) {
                    throw std::runtime_error("L defined wrong");
                }
            }
            if (temp == "\"Min\"") {
                try {
                    min = std::stof(value);
                    continue;
                } catch (std::invalid_argument const&) {
                    throw std::runtime_error("Min defined wrong");
                }
            }
            if (temp == "\"Max\"") {
                try {
                    max = std::stof(value);
                    continue;
                } catch (std::invalid_argument const&) {
                    throw std::runtime_error("Max defined wrong");
                }
            }
            if (temp == "\"View\"") {
                view = (value == "TRUE");
                continue;
            }
            if (temp == "\"Light\"") {
                light = (value == "TRUE");
                continue;
            }
            if (temp == "\"Phong\"") {
                phong = (value == "TRUE");
                continue;
            }
            if (temp == "\"Shiny\"") {
                try {
                    shiny = std::stof(value);
                    continue;
                } catch (std::invalid_argument const&) {
                    throw std::runtime_error("Shiny defined wrong");
                }
            }
            if (temp == "\"Scale\"") {
                try {
                    scale = std::stof(value);
                    continue;
                } catch (std::invalid_argument const&) {
                    throw std::runtime_error("Scale defined wrong");
                }
            }
            throw std::runtime_error("OptParams: Unknown argument");
        }
        optParams = CUDA::OptimizationParameters(p, q, r, lambda, min, max, view, light, phong, shiny, scale);
    }

    void evaluateTransferFunction(std::stringstream& ss) {
        std::string temp;
        std::string value;
        float min = -1;
        float max = -1;
        std::string map;
        while (!ss.eof()) {
            ss >> temp;
            ss >> value;
            if (temp == "\"Colormap\"") {
                map = value;
                continue;
            }
            if (temp == "\"Min\"") {
                try {
                    min = std::stof(value);
                    continue;
                } catch (std::invalid_argument const&) {
                    throw std::runtime_error("Min defined wrong");
                }
            }
            if (temp == "\"Max\"") {
                try {
                    max = std::stof(value);
                    continue;
                } catch (std::invalid_argument const&) {
                    throw std::runtime_error("Max defined wrong");
                }
            }
            throw std::runtime_error("TransferFunction: Unknown argument");
        }
        transferFunction = std::make_unique<TransferFunction>(min, max, map);
    }

    void evaluateFilm(std::stringstream& ss) {
        std::string temp;
        std::string value;
        while (!ss.eof()) {
            ss >> temp;
            ss >> value;
            if (temp == "\"Width\"") {
                width = std::stoi(value);
                continue;
            }
            if (temp == "\"Height\"") {
                height = std::stoi(value);
                continue;
            }
            throw std::runtime_error("Film: Unknown argument");
        }
    }

    void evaluateRenderer(std::stringstream& ss) {
        if (width <= 0 || height <= 0) {
            throw std::runtime_error("Film not defined");
        }
        std::string render;
        ss >> render;

        std::string temp;
        std::string value;

        int samples = 5000;
        int samplesPerFrame = 1;

        RenderParameters params = {false, false, false, false, false, 500, false, false, 0.05, 0.05, ""};

        while (!ss.eof()) {
            ss >> temp;
            ss >> value;
            if (temp == "\"Samples\"") {
                try {
                    samples = std::stoi(value);
                    continue;
                } catch (std::invalid_argument const&) {
                    throw std::runtime_error("Samples defined wrong");
                }
            }
            if (temp == "\"SamplesPerFrame\"") {
                try {
                    samplesPerFrame = std::stoi(value);
                    continue;
                } catch (std::invalid_argument const&) {
                    throw std::runtime_error("SamplesPerFrame defined wrong");
                }
            }
            if (temp == "\"LightSamples\"") {
                try {
                    params.lightSamples = std::stoi(value);
                    continue;
                } catch (std::invalid_argument const&) {
                    throw std::runtime_error("LightSamples defined wrong");
                }
            }
            if (temp == "\"Multiscatter\"") {
                params.multiscatter = (value == "TRUE");
                continue;
            }
            if (temp == "\"Full\"") {
                params.fullIllumination = (value == "TRUE");
                continue;
            }
            if (temp == "\"Bootstrapped\"") {
                params.bootstrap = (value == "TRUE");
                continue;
            }
            if (temp == "\"Debug\"") {
                params.debug = (value == "TRUE");
                continue;
            }
            if (temp == "\"ViewStep\"") {
                try {
                    params.stepSizeView = std::stof(value);
                    continue;
                } catch (std::invalid_argument const&) {
                    throw std::runtime_error("ViewStep defined wrong");
                }
            }
            if (temp == "\"LightStep\"") {
                try {
                    params.stepSizeLight = std::stof(value);
                    continue;
                } catch (std::invalid_argument const&) {
                    throw std::runtime_error("LightStep defined wrong");
                }
            }
            throw std::runtime_error("Renderer: Unknown argument");
        }

        try {
            std::unique_ptr<Renderer> renderer =
                    Renderer::get_registry()[render](width, height, samples, samplesPerFrame, lights);
            renderer->setParameters(params);
            renderers.push_back(std::move(renderer));
        } catch (std::bad_function_call const&) {
            throw std::runtime_error("Renderer: Unknown algorithm");
        }
    }
};
