#include "bella_sdk/bella_engine.h"
#include <thread>

using namespace dl;
using namespace dl::bella_sdk;

// This is the global dl log callback, where all messages will be received, and you can print
// them as you see fit.
//
static int s_logCtx = 0;
static void log(void* /*ctx*/, LogType type, const char* msg)
{
    switch (type)
    {
    case LogType_Info:
        DL_PRINT("[INFO] %s\n", msg);
        break;
    case LogType_Warning:
        DL_PRINT("[WARN] %s\n", msg);
        break;
    case LogType_Error:
        DL_PRINT("[ERROR] %s\n", msg);
        break;
    case LogType_Custom:
        DL_PRINT("%s\n", msg);
        break;
    case LogType_Progress:
        {
            if (dl::logIsProgressBegin(msg))    { /* Open a progress dialog. */ }
            else if (dl::logIsProgressEnd(msg)) { /* Close a progress dialog. */ }
            else
            {
                auto val = dl::logProgressValue(msg);
                auto text = dl::logProgressText(msg);
                DL_PRINT("[%f] %s\n", val, text.buf());
            }
        }
        break;
    }
}

// EngineObserver subscribes to an Engine instance and receives various messages with status,
// images as they become available, and so forth. You only need to implement the overrides you
// wish, most of which come with a "pass" argument, indicating the render pass to which they
// pertain.
//
// Note that these are called from a non-GUI thread, so if you are running in a GUI you will
// need to marshal these calls to the GUI thread.
//
struct MyEngineObserver : public EngineObserver
{
    void onStarted(String pass) override
    {
        logInfo("Started pass %s", pass.buf());
    }
    void onStatus(String pass, String status) override
    {
        logInfo("%s [%s]", status.buf(), pass.buf());
    }
    void onProgress(String pass, Progress progress) override
    {
        logInfo("%s [%s]", progress.toString().buf(), pass.buf());
    }
    void onImage(String pass, Image image) override
    {
        logInfo("We got an image %d x %d.", (int)image.width(), (int)image.height());
    }
    void onError(String pass, String msg) override
    {
        logError("%s [%s]", msg.buf(), pass.buf());
    }
    void onStopped(String pass) override
    {
        logInfo("Stopped %s", pass.buf());
    }
};

// Similar to EngineObserver, SceneObserver is called when various events occur in the scene. There
// is also a NodeObserver to capture events from a single node, which can be useful when writing a
// user interface.
//
struct MySceneObserver : public SceneObserver
{
    bool inEventGroup = false;

    void onNodeAdded(Node node) override
    {
        logInfo("%sNode added: %s", inEventGroup ? "  " : "", node.name().buf());
    }
    void onNodeRemoved(Node node) override
    {
        logInfo("%sNode removed: %s", inEventGroup ? "  " : "", node.name().buf());
    }
    void onInputChanged(Input input) override
    {
        logInfo("%sInput changed: %s", inEventGroup ? "  " : "", input.path().buf());
    }
    void onInputConnected(Input input) override
    {
        logInfo("%sInput connected: %s", inEventGroup ? "  " : "", input.path().buf());
    }
    void onBeginEventGroup() override
    {
        inEventGroup = true;
        logInfo("Event group begin.");
    }
    void onEndEventGroup() override
    {
        inEventGroup = false;
        logInfo("Event group end.");
    }
};

// Helpers to dump Inputs & Outputs.
//
static void printInput(Input input, String indent = "    ")
{
    logInfo("%s%s %s", indent.buf(), input.typeString().buf(), input.name().buf());

    // Array/Object inputs have child inputs, so print those.
    //
    for (UInt j = 0; j < input.inputCount(); ++j)
        printInput(input.input(j), indent + "  ");
};

static void printOutput(Output output, String indent = "    ")
{
    logInfo("%s%s %s", indent.buf(), output.typeString().buf(), output.name().buf());
};

// Virtual class to override in order to be called to add GUI controls while processing layouts.
//
struct GuiWriter
{
    virtual void addHeader(String text, String indent) { }
    virtual void addSeparator(String indent) { }
    virtual void addControl(Input input, String indent) { }
    virtual void addInputPath(Node inputPath, String indent) { }
};

// Handle an input found while processing layouts.
//
static void addInput(GuiWriter& gui, Input input, String indent)
{
    switch (input.type())
    {
    case AttrType_Null: // nop
        break;

    case AttrType_Bool:
    case AttrType_Int:
    case AttrType_UInt:
    case AttrType_Real:
    case AttrType_Vec2:
    case AttrType_Pos2:
    case AttrType_Vec3:
    case AttrType_Pos3:
    case AttrType_Vec4:
    case AttrType_Quat:
    case AttrType_Rgba:
    case AttrType_Mat3:
    case AttrType_Mat4:
    case AttrType_Node:
    case AttrType_String:
    case AttrType_Buffer:
        gui.addControl(input, indent);
        break;

    case AttrType_Array:
        {
            if (input.arrayType() != AttrType_Node)
                gui.addHeader(input.name(), indent);

            for (auto i: input.inputs())
                addInput(gui, i, indent + "  ");
        }
        break;

    case AttrType_Object:
        {
            if (input.inputCount() > 0)
            {
                gui.addHeader(input.parent().name() + " " + input.name(), indent);

                for (auto i: input.inputs())
                    addInput(gui, i, indent + "  ");
            }
        }
        break;
    }
}

// Process a layout, its sub-layouts, and so forth.
//
static void addLayout(GuiWriter& gui, Node node, Input input, Layout layout, String indent)
{
    switch (layout.type())
    {
    case LayoutType_Invalid:
        logError("Invalid layout type.");
        break;

    case LayoutType_Separator:
        gui.addSeparator(indent);
        break;

    case LayoutType_TopLevel:
        {
            if (node.isTypeOf("quickMaterial"))
            {
                // Need to generalize this somehow. What we are doing here is find out the current
                // "type" of the quick material, in order to only show parameters from its matching
                // sub-object.
                //
                // But anyway, this serves also as an example of interpreting a layout in a special
                // way, for any case you may wish to do that.

                auto type = node["type"].asString();

                for (auto l: layout.layouts())
                {
                    auto isCommon = l.isHeader() && l.name().compareNoCase("Common") == 0;
                    auto isQuick = l.isHeader() && l.name().compareNoCase("Quick") == 0;
                    auto isType = l.isHeader() && l.name().compareNoCase(type) == 0;

                    if (isCommon || isQuick || isType)
                        addLayout(gui, node, node.parseInputPath(l.path()), l, indent);
                }
            }
            else
            {
                for (auto l: layout.layouts())
                    addLayout(gui, node, l.isSeparator() ? nullptr : node.parseInputPath(l.path()), l, indent);
            }
        }
        break;

    case LayoutType_Header:
        {
            gui.addHeader(layout.label(), indent);

            for (auto l: layout.layouts())
                addLayout(gui, node, l.isSeparator() ? nullptr : node.parseInputPath(l.path()), l, indent + "  ");
        }
        break;

    case LayoutType_Input:

        switch (input.type())
        {
        case AttrType_Array:
            {
                if (input.arrayType() != AttrType_Node)
                    gui.addHeader(input.label(), indent);

                for (auto i: input.inputs())
                    addLayout(gui, node,i, layout, indent + "  ");
            }
            break;
        case AttrType_Object:
            {
                gui.addHeader(input.parent().label() + " " + input.label(), indent);

                for (auto i: input.inputs())
                    addLayout(gui, node, i, layout, indent + "  ");
            }
            break;
        default:
            {
                if (node.isTypeOf("inputPath"))
                {
                    // For path nodes we have this special case, where we present the whole
                    // path, instead of controls for the ["dir"], ["file"], and ["ext"]. The
                    // node provides asPath() and setPath() methods for helping here.
                    //
                    if (input.name() == "ext")
                    {
                        gui.addInputPath(node, indent);
                    }
                    else if (input.name() == "dir" || input.name() == "file")
                    {
                        // Skip these, as we already handled them just above.
                    }
                    else
                        addInput(gui, input, indent);
                }
                else
                    addInput(gui, input, indent);
            }
            break;
        }
        break;

    case LayoutType_Object:
        {
            gui.addHeader(layout.label(), indent);

            for (auto l: layout.layouts())
                addLayout(gui, node, l.isSeparator() ? nullptr : input[l.name()], l, indent + "  ");
        }
        break;

    case LayoutType_ObjectArray:
        {
            for (auto i: input.inputs())
            {
                gui.addHeader(layout.label() + i.label(), indent);

                for (auto l: layout.layouts())
                    addLayout(gui, node, l.isSeparator() ? nullptr : i[l.name()], l, indent + "  ");
            }

            // Create an "add new element" button here.
        }
        break;
    }
};

// Here is our implementation of the GuiWriter. In reality it would be creating GUI controls.
//
struct MyGuiWriter : public GuiWriter
{
    void addHeader(String text, String indent) override
    {
        logCustom("%s%s", indent.buf(), text.buf());
    }

    void addSeparator(String indent) override
    {
        logCustom("%s-------------------", indent.buf());
    }

    void addControl(Input input, String indent) override
    {
        // In reality you would specialize this for the actual type, creating a checkbox,
        // spinbox, or whatever else is appropriate for the specific input type, using the
        // value & default value for the input, its soft/hard min/max, step, units, etc.
        //
        logCustom("%s%s: %s", indent.buf(), input.label().buf(), input.valueToString().buf());
    }

    void addInputPath(Node inputPath, String indent) override
    {
        logCustom("%s%s: %s", indent.buf(), "Path", inputPath.asPath().buf());
    }
};

// We will use the dl_core main helper here. This gives us a helpful Args instance to use, and
// also hides the confusing details of dealing with main vs. WinMain on windows, and gives us
// utf8-encoded args when the application is unicode.
//
#include "dl_core/dl_main.inl"
int DL_main(Args& args)
{
    // Very early on, we will subscribe to the global bella logging callback, and ask to flush
    // any messages that may have accumulated prior to this point.
    //
    subscribeLog(&s_logCtx, log);
    flushStartupMessages();

    logBanner("Bella Engine SDK (version: %s, build date: %llu)",
        bellaSdkVersion().toString().buf(),
        bellaSdkBuildDate()
   );

    // The Args class is very easy to use; we just add various arguments we would like to check
    // for, and then ask if we have them. We do not have to add ones for input & output files,
    // as Args handles that internally already, along with associated -i: and -o: arguments.
    //
    args.add("ld",  "listDefs", "",   "List node definitions.");
    args.add("lo",  "layout",   "",   "Print a layout for a node type, e.g. --layout:mesh");
    args.add("bt",  "bakeTex",  "no", "Bake a tga texture.");

    // If version requested, printf it directly with no extraneous printing and exit. This is
    // also something automatically supported by Args.
    //
    if (args.versionReqested())
    {
        printf("%s", bellaSdkVersion().toString().buf());
        return 0;
    }

    // Similarly, the -h switch is built into the Args class, so we can check for that one too.
    // To make -h automatic, all you need to do is ask the Args to generate help for currently-
    // defined switches & arguments, via its help() method.
    //
    if (args.helpRequested())
    {
        printf("%s", args.help("SDK Test", fs::exePath(), bellaSdkVersion().toString()).buf());
        return 0;
    }

    // Create an engine to use for our command line function, and for rendering. We will load our
    // definitions, though it is also possible to define nodes at runtime. When we call loadDefs
    // this way, with no argument, the scene loads bella's internal built-in node definitions.
    //
    Engine engine;
    engine.scene().loadDefs();

    // See if the arguments have our --list argument, and if so, go through and list all the node
    // definitions in the scene.
    //
    if (args.have("--listDefs"))
    {
        logInfo("--listDefs argument given, listing node types:");
        
        for (auto def: engine.scene().defs())
        {
            if (def.isAbstract() || def.isInternal())
                continue;

            logInfo("%s [bases: %s]", def.type().buf(), def.bases().join(", ").buf());
            logInfo("  has eval: %s", def.hasNodeEval() ? "yes" : "no");

            logInfo("  inputs: %d", (int)def.inputCount());
            for (UInt i = 0; i < def.inputCount(); ++i)
                printInput(def.input(i));

            logInfo("  outputs: %d", (int)def.outputCount());
            for (UInt i = 0; i < def.outputCount(); ++i)
                printOutput(def.output(i));
        }

        logInfo("Done.");
        return 0;
    }

    // Demo of using layouts, or laying out a node that has no layout. Maybe we will create an
    // abstraction for this, where you just implement a class and have overrides called to add the
    // different UI elements.
    //
    if (!args.value("--layout", "").isEmpty())
    {
        MyGuiWriter gui;

        auto type = args.value("--layout");
        auto node = engine.scene().createNode(type);

        if (node.layout())
        {
            addLayout(gui, node, nullptr, node.layout(), "");
        }
        else // No layout for this node, so the best we can do is just add controls for its inputs.
        {
            for (UInt i = 0; i < node.inputCount(); ++i)
                addInput(gui, node[i], "");
        }
        
        logInfo("Done.");
        return 0;
    }

    // Demo of observing scene events and baking a procedural texture. During the execution of this
    // code you will see messages being printed about nodes being created. We also show here how to
    // read a value given to a command line argument that takes one; here we are checking whether
    // the application has been called with something like -t:yes or --tga:yes.
    //
    if (args.value("--bakeTex", "no").compareNoCase("yes") == 0)
    {
        logInfo("Baking texture to tga...");

        MySceneObserver sceneObserver;
        engine.scene().subscribe(&sceneObserver);

        auto scene   = engine.scene();
        auto checker = scene.createNode("checker");
        auto grid    = scene.createNode("grid");
        auto texform = scene.createNode("texform");

        // Scope for event scope helper. Using this is equivalent to calling Scene::pushEventGroup()
        // and Scene::popEventGroup(), but you cannot accidentally forget to pop the group, since the
        // EventScope will do that in its destructor.
        {
            Scene::EventScope es(scene);

            checker["color1"]  |= grid.output("outColor");
            checker["color2"]   = Rgba::magenta;
            checker["uvCoord"] |= texform.output("outUV");
            texform["repeat"]   = Vec2 {2.0, 2.0};
            texform["rotate"]   = 45.0;
        }

        // Now let's sample the texture to write a simple tga file.
        //
        auto w = 200, h = 200;
        File tga;
        tga.openWriteBinary("test.tga");
        tga.writeByte(0);
        tga.writeByte(0);
        tga.writeByte(2);
        tga.writeByte(0);
        tga.writeByte(0);
        tga.writeByte(0);
        tga.writeByte(0);
        tga.writeByte(0);
        tga.writeByte(0);
        tga.writeByte(0);
        tga.writeByte(0);
        tga.writeByte(0);
        tga.writeByte(w%256);
        tga.writeByte(w/256);
        tga.writeByte(h%256);
        tga.writeByte(h/256);
        tga.writeByte(3*8);
        tga.writeByte(0x20);

        checker.nodePrep();
        auto outColor = checker.output("outColor");

        for (auto v = 0; v < h; ++v)
        for (auto u = 0; u < w; ++u)
        {
            auto c = outColor.sample({u/(Real)w, v/(Real)h});
            tga.writeByte(c.b*255.0);
            tga.writeByte(c.g*255.0);
            tga.writeByte(c.r*255.0);
        }

        tga.close();
        scene.unsubscribe(&sceneObserver);

        logInfo("Done.");
        return 0;
    }

    // Finally, check if we got an input path on the command line. This may be given either as the
    // first argument, with no switch, or by using the -i: switch. If we don't get any path, we
    // will use the built-in preview scene.
    //
    auto path = args.iPath().canonical();
    if (path.isEmpty())
    {
        logInfo("No input file given, using standard Bella preview scene.");
        path = bella_sdk::previewPath();
    }

    // If still no path (previewPath() should not fail, but say it does), then just bail out.
    //
    if (!dl::Path::isFile(path))
    {
        logError("Input path not found: %s", path.buf());
        return 1;
    }

    // We have a file, so we'll now give it to the engine's scene to read.
    //
    if (!engine.scene().read(path))
    {
        logError("Failed to read %s from %s", path.buf(), fs::currentDir().buf());
        return 1;
    }

    // Starting rendering is as simple as calling start(). We'll subscribe an observer to the scene
    // here to receive messages from the engine as it starts up and begins to render.
    //
    MyEngineObserver engineObserver;
    engine.subscribe(&engineObserver);

    logInfo("Rendering %s", dl::Path::canonical(path).buf());

    if (!engine.start())
    {
        logError("Engine failed to start.");
        return 1;
    }

    // Now the engine is rendering (or has failed) and we will just wait in a loop til it finishes.
    // In a gui, we wouldn't usually wait in a loop like this, and the engine would just be left to
    // do its thing until it finishes, or until we click some other button that tells the engine to
    // stop.
    //
    while (engine.rendering())
        std::this_thread::sleep_for(std::chrono::milliseconds(100));

    // If the engine failed, we may not want to repeatedly try to restart it, until the user has
    // fixed whatever problem was reported. Here, we'll just log that a failure occurred.
    //
    if (engine.failed())
        logError("The solver has experienced an unknown failure; check prior messages for details.");

    // Not really necessary here, as the destructors for everything will take care of it for us, but
    // if you were implementing this in an IPR window or viewport, you might create and start the
    // engine as above, and later, when the gui is tearing down your window, you could call stop and
    // wait for it (by default it blocks until the engine has stopped).
    //
    engine.stop();
    engine.unsubscribe(&engineObserver);

    logInfo("Done.");
    return 0;
}
