import { Cache } from "../../../../../core/cache/Cache.js";
import { SignalBinding } from "../../../../../core/events/signal/SignalBinding.js";
import { passThrough } from "../../../../../core/function/passThrough.js";
import { strictEquals } from "../../../../../core/function/strictEquals.js";
import Vector2 from "../../../../../core/geom/Vector2.js";
import { clamp } from "../../../../../core/math/clamp.js";
import { ResourceAccessKind } from "../../../../../core/model/ResourceAccessKind.js";
import { ResourceAccessSpecification } from "../../../../../core/model/ResourceAccessSpecification.js";
import { System } from "../../../../ecs/System.js";
import { obtainTerrain } from "../../../../ecs/terrain/util/obtainTerrain.js";
import { buildCameraTargetSampler } from "../../../../input/ecs/util/TerrainCameraTargetSampler.js";
import TopDownCameraController from "./TopDownCameraController.js";
import { TopDownCameraLander } from "./TopDownCameraLander.js";


/**
 *
 * @param {Terrain} terrain
 * @param {Cache<number,Sampler2D>} cache
 */
function getSampler(terrain, cache) {

    let sampler = cache.get(terrain.id);

    if (sampler === null) {
        //build new sampler
        const heightSampler = terrain.samplerHeight;

        if (heightSampler === null) {
            return null;
        }

        sampler = buildCameraTargetSampler({ heightSampler });

        cache.put(terrain.id, sampler);
    }

    return sampler;
}

const v2_scratch = new Vector2();

/**
 *
 * @param {TopDownCameraLander} lander
 * @param {TopDownCameraController} controller
 * @param {Terrain} terrain
 * @param {Cache<number,Sampler2D>} samplerCache
 */
function land(lander, controller, terrain, samplerCache) {

    const sampler = getSampler(terrain, samplerCache);

    if (sampler === null) {
        //no sampler
        return;
    }

    /**
     *
     * @type {Vector3}
     */
    const target = controller.target;


    terrain.mapPointWorld2Grid(target, v2_scratch);

    v2_scratch.divide(terrain.size);

    const x = clamp(v2_scratch.x * sampler.width, 0, sampler.width - 1);
    const y = clamp(v2_scratch.y * sampler.height, 0, sampler.height - 1);

    const value = sampler.sampleChannelBilinear(x, y, 0);

    target.setY(value);
}

export class TopDownCameraLanderSystem extends System {
    constructor() {
        super();

        /**
         *
         * @type {Cache<number,Sampler2D>}
         */
        this.samplerCache = new Cache({
            maxWeight: 2,
            keyHashFunction: passThrough,
            keyEqualityFunction: strictEquals
        });

        this.dependencies = [TopDownCameraLander, TopDownCameraController];

        this.components_used = [
            ResourceAccessSpecification.from(TopDownCameraController,ResourceAccessKind.Read|ResourceAccessKind.Write)
        ];

        /**
         * @private
         * @type {Array}
         */
        this.data = [];
    }

    /**
     *
     * @param {TopDownCameraLander} lander
     * @param {TopDownCameraController} controller
     * @param {number} entity
     */
    link(lander, controller, entity) {
        const doLand = () => {
            const em = this.entityManager;

            const ecd = em.dataset;

            if (ecd === null) {
                return;
            }


            /**
             *
             * @type {Terrain}
             */
            const terrain = obtainTerrain(ecd);

            if (terrain === null) {
                return;
            }


            //TODO add scheduling of landing if any of the prerequisites are missing
            land(lander, controller, terrain, this.samplerCache);
        };

        const binding = new SignalBinding(controller.target.onChanged, doLand);

        binding.link();

        doLand();

        this.data[entity] = binding;
    }

    /**
     *
     * @param {TopDownCameraLander} lander
     * @param {TopDownCameraController} controller
     * @param {number} entity
     */
    unlink(lander, controller, entity) {
        const bindings = this.data[entity];

        if (bindings !== undefined) {
            bindings.unlink();

            delete this.data[entity];
        }
    }

    update(timeDelta) {

    }
}
