import { assert } from "../../../../core/assert.js";
import { float_to_uint8 } from "../../../../core/binary/float_to_uint8.js";
import LineBuilder from "../../../../core/codegen/LineBuilder.js";
import { rgb2hex } from "../../../../core/color/hex/rgb2hex.js";
import { okhsv_to_linear_srgb } from "../../../../core/color/oklab/okhsv_to_linear_srgb.js";
import { Signal } from "../../../../core/events/signal/Signal.js";
import { graphviz_escape_string } from "../../../../core/graph/format/graphviz/graphviz_escape_string.js";
import { lerp } from "../../../../core/math/lerp.js";
import { computeStringHash } from "../../../../core/primitives/strings/computeStringHash.js";
import { FramePassBuilder } from "./FramePassBuilder.js";
import { FramePassNode } from "./FramePassNode.js";
import { FramePassResources } from "./FramePassResources.js";
import { ResourceEntry } from "./ResourceEntry.js";
import { ResourceNode } from "./ResourceNode.js";

/**
 * A high-level rendering architecture system that manages render passes and their resource dependencies.
 * This class schedules frame execution by building a directed graph of resources (textures, buffers) and the passes that read or write to them.
 *
 * By deferring execution until after the graph is compiled, it automatically handles complex tasks such as:
 * - Culling unreferenced resources and passes (dead code elimination for rendering).
 * - Automatic resource lifetime management (allocating transient resources just-in-time and destroying them immediately after their last use).
 * - Visualizing the rendering pipeline via GraphViz DOT exports.
 *
 * @source Based on the Frostbite's GDC paper "FrameGraph: Extensible Rendering Architecture in Frostbite" by Yuriy O'Donnell
 * @example
 *      const graph = new FrameGraph("My Graph");
 *
 *      const pass_data = {};
 *      const pass = graph.add("GBuffer Pass", pass_data, (data, resources, context) => {
 *
 *          context.beginRenderPass({
 *              depth_attachment: resources.get(data.depth),
 *              color_attachments:[
 *                  resources.get(data.normal),
 *                  resources.get(data.albedo),
 *             ]
 *          });
 *
 *          for (const renderable in renderables){
 *              drawMesh(context, renderable.mesh, renderable.material);
 *          }
 *
 *          context.endRenderPass();
 *
 *      });
 *
 *      pass_data.depth = pass.create("depth", {...});
 *      pass_data.albedo = pass.create("albedo", {...});
 *      pass_data.normal = pass.create("normal", {...});
 *
 *      graph.compile();
 *      graph.execute(context);
 *
 */
export class FrameGraph {
    /**
     * Human-readable name, used for debugging and UI primarily.
     * There is no uniqueness guarantee
     * @type {string}
     */
    name = "";

    /**
     *
     * @type {Signal<IFrameGraphContext,this>}
     */
    onExecuted = new Signal();

    /**
     *
     * @type {FramePassNode[]}
     * @private
     */
    __pass_nodes = [];

    /**
     *
     * @type {ResourceNode[]}
     * @private
     */
    __resource_nodes = [];

    /**
     *
     * @type {ResourceEntry[]}
     * @private
     */
    __resource_registry = [];

    /**
     *
     * @param {string} name
     */
    constructor(name = "") {
        assert.isString(name, 'name');


        this.name = name;
    }

    /**
     *
     * @param {number} id Resource Node ID
     * @returns {ResourceEntry}
     */
    getResourceEntry(id) {
        const node = this.getResourceNode(id);

        assert.defined(node, 'node');

        const registry = this.__resource_registry;

        const entry = registry[node.resource_id];

        assert.defined(entry, 'entry');

        return entry;
    }

    /**
     *
     * @param {number} id
     * @returns {ResourceNode}
     */
    getResourceNode(id) {
        assert.isNonNegativeInteger(id, 'id');

        const resource = this.__resource_nodes[id];

        if (resource === undefined) {
            throw new Error(`Resource Node ${id} not found`);
        }

        assert.equal(resource.id, id, 'resource.id does not match');

        return resource;
    }

    /**
     * @template T
     * @param {number} id resource ID
     * @returns {T}
     */
    getDescriptor(id) {
        return this.getResourceEntry(id).resource_descriptor;
    }

    /**
     * @template T
     * @param {string} name
     * @param {T} descriptor
     * @returns {number}
     */
    create_resource(name, descriptor) {
        assert.isString(name, 'name');
        assert.defined(descriptor, 'descriptor');
        assert.notNull(descriptor, 'descriptor');
        assert.isObject(descriptor, 'descriptor');

        const resource = this._createResourceEntry(descriptor);

        return this._createResourceNode(name, resource.resource_id).id;
    }

    /**
     * @template T
     * @param {T} descriptor
     * @return {ResourceEntry}
     * @private
     */
    _createResourceEntry(descriptor) {
        assert.defined(descriptor, 'descriptor');
        assert.notNull(descriptor, 'descriptor');

        const entry = new ResourceEntry();

        entry.resource_id = this.__resource_registry.length;
        entry.resource_descriptor = descriptor;

        this.__resource_registry.push(entry);

        return entry;
    }

    /**
     *
     * @param {string} name
     * @param {number} resource_id
     * @return {ResourceNode}
     * @private
     */
    _createResourceNode(name, resource_id) {
        assert.isString(name, 'name');
        assert.isNonNegativeInteger(resource_id, 'resource_id');

        const n = new ResourceNode();

        const id = this.__resource_nodes.length;

        n.id = id;
        n.name = name;
        n.resource_id = resource_id;

        this.__resource_nodes[id] = n;

        return n;
    }

    /**
     *
     * @param {number} node_id
     * @returns {number} ID of the cloned resource node
     */
    clone_resource(node_id) {
        const node = this.getResourceNode(node_id);

        const entry = this.__resource_registry[node.resource_id];

        entry.resource_version++;

        const clone_node = new ResourceNode();
        clone_node.id = this.__resource_nodes.length;
        clone_node.name = node.name;
        clone_node.resource_id = node.resource_id;
        clone_node.version = entry.resource_version;

        this.__resource_nodes.push(clone_node);

        return clone_node.id;
    }

    /**
     * @template T
     * @param {string} name
     * @param {ResourceDescriptor<T>} description
     * @param {T} resource
     * @returns {number}
     */
    import_resource(name, description, resource) {
        const record = this._createResourceEntry(description);

        record.resource = resource;

        record.imported = true;

        return this._createResourceNode(name, record.resource_id).id;
    }

    /**
     * @returns {boolean}
     * @param id
     */
    is_valid_resource(id) {
        const node = this.getResourceNode(id);
        const record = this.getResourceEntry(id);

        return node.version === record.resource_version;
    }

    /**
     * Add a new pass to the graph.
     * @template T
     * @param {string} name
     * @param {T} data
     * @param {function(data:T, resources: FramePassResources, context:IFrameGraphContext):void} execute
     * @returns {FramePassBuilder}
     */
    add(name, data, execute) {
        assert.isString(name, 'name');

        assert.defined(data, 'data');
        assert.notNull(data, 'data');
        assert.isObject(data, 'data');

        assert.isFunction(execute, 'execute');

        const pass_nodes = this.__pass_nodes;

        const builder = new FramePassBuilder();
        const node = new FramePassNode();

        node.id = pass_nodes.length;
        node.name = name;
        node.execute = execute;

        pass_nodes.push(node);

        builder.init(this, node);

        node.data = data;

        return builder;
    }

    /**
     * Perform validation, useful for debugging
     * Typically done before compilation
     * @param {function(problem:string):*} problem_consumer
     * @param {*} [problem_consumer_context] thisArg for `problem_consumer`
     * @returns {boolean}
     */
    validate(problem_consumer, problem_consumer_context) {

        // TODO implement
        // TODO check for "read before write"

        return true;
    }

    compile() {
        const pass_nodes = this.__pass_nodes;

        // wire resources of passes
        const pass_node_count = pass_nodes.length;
        const resource_nodes = this.__resource_nodes;

        for (let i = 0; i < pass_node_count; i++) {
            const pass = pass_nodes[i];

            // mask pass as having number of references equal to number of resources it writes to
            pass.ref_count = pass.resource_writes.length;

            for (const id of pass.resource_reads) {
                const read_node = resource_nodes[id];

                read_node.ref_count++;
            }

            for (const id of pass.resource_writes) {
                const written_node = resource_nodes[id];

                written_node.producer = pass;
            }
        }

        // perform culling
        /**
         *
         * @type {ResourceNode[]}
         */
        const unreferenced_resources = [];

        for (const node of resource_nodes) {
            if (node.ref_count === 0) {
                unreferenced_resources.push(node);
            }
        }

        while (unreferenced_resources.length > 0) {
            const unreferenced_resource = unreferenced_resources.pop();

            const producer = unreferenced_resource.producer;

            if (producer === null || producer.has_side_effects) {
                continue;
            }

            assert.greaterThanOrEqual(producer.ref_count, 1, 'must have at least one reference'); // Why?

            // disconnect unreferenced resource from producer
            producer.ref_count--;

            if (producer.ref_count === 0) {

                // producer is no longer referenced

                for (const id of producer.resource_reads) {
                    const node = resource_nodes[id];

                    // disconnect producer from all its reads (upstream)
                    node.ref_count--;

                    if (node.ref_count === 0) {
                        // resource is no longer referenced
                        unreferenced_resources.push(node);
                    }
                }
            }
        }

        // calculate lifetimes
        for (let i = 0; i < pass_node_count; i++) {
            const pass = pass_nodes[i];
            if (pass.ref_count === 0) {
                // unused pass
                continue;
            }

            for (const id of pass.resource_creates) {
                const entry = this.getResourceEntry(id);
                entry.producer = pass;
                entry.last = pass;
            }

            for (const id of pass.resource_writes) {
                this.getResourceEntry(id).last = pass;
            }

            for (const id of pass.resource_reads) {
                this.getResourceEntry(id).last = pass;
            }
        }
    }

    /**
     *
     * @param {IFrameGraphContext} context
     */
    execute(context) {
        assert.defined(context, 'context');
        assert.notNull(context, 'context');
        assert.isObject(context, 'context');

        const pass_nodes = this.__pass_nodes;
        const pass_node_count = pass_nodes.length;

        for (let i = 0; i < pass_node_count; i++) {
            const node = pass_nodes[i];

            if (!node.can_execute()) {
                continue;
            }

            const node_created_resources = node.resource_creates;

            for (const id of node_created_resources) {

                // allocate resource
                this.getResourceEntry(id).create(context.resource_manager);
            }

            const resources = new FramePassResources();
            resources.init(this, node);

            // execute pass
            try {
                node.execute(node.data, resources, context);
            } catch (e) {
                // decorate
                const error = new Error(`RenderPass '${node.name}' failed to execute`);

                error.cause = e;

                throw error;
            }


            const resource_entries = this.__resource_registry;
            const resource_entry_count = resource_entries.length;

            for (let entry_index = 0; entry_index < resource_entry_count; entry_index++) {
                const entry = resource_entries[entry_index];

                if (entry.last === node && entry.isTransient()) {
                    // this was the last user of the resource and the resource is transient (no external usage)
                    entry.destroy(context.resource_manager);
                }

            }

        }

        this.onExecuted.send2(context, this);
    }

    /**
     * Should only call after {@link compile}
     *
     * Can be visualized with this tool: https://skaarj1989.github.io/FrameGraph/
     *
     * @see https://github.com/skaarj1989/FrameGraph/blob/viewer/JsonWriter.hpp
     * @return {{passes: [], resources: []}}
     */
    exportToJson() {

        const resources = [];

        this.__resource_registry.forEach((entry, entry_id) => {
            const node = this.getResourceNode(entry.resource_id);

            const j = {
                id: entry_id,
                name: node.name,
                transient: entry.isTransient(),
            };

            const description = entry.resource_descriptor?.toString();

            if (description) {
                j.description = description;
            }

            if (entry.producer !== null) {
                j.createdBy = entry.producer.id;
            }

            const readers = [];
            const writers = [];

            this.__pass_nodes.forEach(pass => {
                if (pass.reads(entry.resource_id)) {
                    readers.push(pass.id);
                }
                if (pass.writes(entry.resource_id)) {
                    writers.push(pass.id);
                }
            })

            if (readers.length > 0) {
                j.readers = readers;
            }
            if (writers.length > 0) {
                j.writers = writers;
            }

            resources[entry_id] = j;
        })

        return {
            passes: this.__pass_nodes.map(node => {
                return {
                    id: node.id,
                    name: node.name,
                    culled: !node.can_execute(),
                    reads: node.resource_reads,
                    writes: node.resource_writes,
                }
            }),
            resources
        }
    }

    /**
     * Export the graph diagram in GraphViz DOT format.
     * Useful for debugging.
     * @see https://en.wikipedia.org/wiki/DOT_(graph_description_language)
     * @return {string}
     */
    exportToDot() {
        const out = new LineBuilder();

        out.add("digraph FrameGraph {");
        out.indent();
        out.add("graph [style=invis, rankdir=\"TB\" ordering=out, splines=spline]");
        out.add("node [shape=record, fontname=\"helvetica\", fontsize=10, margin=\"0.2,0.03\"]");


        // -- Define pass nodes

        out.add("");
        out.add("# Pass Nodes");
        for (const node of this.__pass_nodes) {
            out.add(`P${node.id} [label=<{ {<B>${node.name}</B>} | {${(node.has_side_effects ? "&#x2605; " : "")} Refs: ${node.ref_count}<BR/> Index: ${node.id}} }> style=\"rounded,filled\", fillcolor=${(node.ref_count > 0 || node.has_side_effects) ? "orange" : "lightgray"}]`);
        }

        // -- Define resource nodes

        /**
         *
         * @param {string} type
         * @returns {number}
         */
        function type_to_color(type) {

            const hash_32 = computeStringHash(type);

            const hash_16 = (hash_32 & 0xffff) ^ (hash_32 >>> 16);

            return hash_16 / 0xffff;
        }

        /**
         *
         * @param {ResourceEntry} entry
         * @return {string}
         */
        function entry_to_background_color(entry) {

            let color_hue = type_to_color(entry.resource_descriptor?.type ?? "");

            // remap hue away from red
            color_hue = lerp(0.10, 0.90, color_hue);

            const saturation = entry.isImported() ? 0.2 : 0.07;

            const color_rgb = [];
            okhsv_to_linear_srgb(color_rgb, color_hue, saturation, 1)

            const hex = rgb2hex(...color_rgb.map(float_to_uint8));

            return `#${hex}`;
        }

        out.add("");
        out.add("# Resource Nodes");
        for (const node of this.__resource_nodes) {
            const entry = this.__resource_registry[node.resource_id];

            const type = graphviz_escape_string(entry.resource_descriptor?.type ?? "");

            const block_title = `${entry.isImported() ? "↪ " : ""}${node.name}`;
            const block_tooltip = graphviz_escape_string(entry.toString());

            out.add(`R${entry.resource_id}_${node.version} [label=<{ {<B>${block_title}</B>${node.version > 0 ? ` <FONT>v${(node.version + 1)}</FONT>` : ""}<BR/>${type}} | {Index: ${entry.resource_id}<BR/> Refs : ${node.ref_count} } }> style=filled, fillcolor="${entry_to_background_color(entry)}" tooltip="${block_tooltip}"]`);
        }

        // -- Each pass node points to the resource that it writes

        out.add("");
        out.add("# Resource Writes");
        for (const node of this.__pass_nodes) {
            out.add(`P${node.id} -> {`);
            out.indent();
            for (const id of node.resource_writes) {
                const written = this.__resource_nodes[id];
                out.add(`R${written.resource_id}_${written.version} `);
            }
            out.dedent();
            out.add("} [color=orangered]");
        }

        // -- Each resource node points to the pass where it's consumed

        out.add("");
        out.add("# Resource Reads");

        for (const node of this.__resource_nodes) {
            out.add(`R${node.resource_id}_${node.version} -> {`);
            out.indent();
            // find all readers of this resource node
            for (const pass of this.__pass_nodes) {
                for (const id of pass.resource_reads)
                    if (id === node.id) {
                        out.add(`P${pass.id} `);
                    }
            }
            out.dedent();
            out.add("} [color=olivedrab3]");
        }
        out.dedent();
        out.add("}");
        return out.build();
    }
}

/**
 * Type check shortcut.
 * @readonly
 * @type {boolean}
 */
FrameGraph.prototype.isFrameGraph = true;