import { assert } from "../../../../core/assert.js";
import { v2_distance } from "../../../../core/geom/vec2/v2_distance.js";
import { clamp } from "../../../../core/math/clamp.js";
import { clamp01 } from "../../../../core/math/clamp01.js";
import { inverseLerp } from "../../../../core/math/inverseLerp.js";
import { max2 } from "../../../../core/math/max2.js";
import { seededRandom } from "../../../../core/math/random/seededRandom.js";
import LinearModifier from "../../../../core/model/stat/LinearModifier.js";
import TransitionFunctions from "../../../../engine/animation/TransitionFunctions.js";
import { TerrainLayer } from "../../../../engine/ecs/terrain/ecs/layers/TerrainLayer.js";
import Terrain from "../../../../engine/ecs/terrain/ecs/Terrain.js";
import { CellFilterLiteralFloat } from "../../../filtering/numeric/CellFilterLiteralFloat.js";
import { MarkerNodeAction } from "../MarkerNodeAction.js";

export class MarkerNodeActionPaintTerrain extends MarkerNodeAction {
    /**
     *
     * @CellFilter
     */
    weight = CellFilterLiteralFloat.ONE;

    /**
     * What to paint
     * @type {TerrainLayerDescription}
     */
    layer = null;

    /**
     * Actual layer index, will be taken from the terrain upon initialization
     * @type {number}
     * @private
     */
    _layer_index = -1;

    /**
     *
     * @type {SplatMapping}
     * @private
     */
    _splat_map = null;

    /**
     *
     * @type {Terrain}
     * @private
     */
    _terrain = null;

    /**
     * How far does the area extend from the marker's shape, has both % and constant component
     * Can grow or shrink the area
     * @type {LinearModifier}
     */
    dilation = LinearModifier.CONSTANT_ZERO;

    /**
     * Function to apply smoothing to the area that falls into "edge"
     * @type {function(number): number}
     */
    weight_edge_easing_function = TransitionFunctions.EaseInOut;

    /**
     * How wide is the edge, computed from the effective region including padding, must produce a non-negative number
     * @type {LinearModifier}
     */
    weight_edge_easing = LinearModifier.CONSTANT_ZERO;

    /**
     *
     * @type {function():number}
     * @private
     */
    _random;

    /**
     *
     * @param {LinearModifier} dilation
     * @param {function(number):number} border_blending
     * @param {LinearModifier} border
     * @param {CellFilter} weight
     * @param {TerrainLayerDescription} layer
     * @returns {MarkerNodeActionPaintTerrain}
     */
    static from({
                    dilation = LinearModifier.CONSTANT_ZERO,
                    border_blending = TransitionFunctions.EaseInOut,
                    border = LinearModifier.CONSTANT_ZERO,
                    weight = CellFilterLiteralFloat.ONE,
                    layer
                }) {

        assert.defined(layer, 'layer');
        assert.notNull(layer, 'layer');
        assert.equal(layer.isTerrainLayerDescription, true, 'layer.isTerrainLayerDescription !== true');

        const r = new MarkerNodeActionPaintTerrain();


        r.dilation = dilation;
        r.weight_edge_easing = border;
        r.weight_edge_easing_function = border_blending;
        r.weight = weight;
        r.layer = layer;

        return r;
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @param {MarkerNode} node
     * @param {GridData} grid
     * @param {number} radius_outer
     * @param {number} radius_inner
     * @returns {number}
     * @private
     */
    _getPointWeight(x, y, node, grid, radius_outer, radius_inner) {
        // compute distance from node center
        const distance_from_center = v2_distance(node.position.x, node.position.y, x, y);

        if (distance_from_center > radius_outer) {
            // too far away
            return 0;
        }

        const weight_sampled = this.weight.execute(grid, x, y, 0);


        // compute position in the padding region, where 0 is at the outer edge of the padding, and 1 is at the
        const edge_normalized_position = clamp01(inverseLerp(radius_inner, radius_outer, distance_from_center));

        // apply edge smoothing
        const edge_weight_bias = this.weight_edge_easing_function(1 - edge_normalized_position);

        return weight_sampled * edge_weight_bias;
    }

    _getLayerIndex() {
        if (this._layer_index === -1) {
            // bind terrain layer
            const layers = this._terrain.layers;
            const layer_descriptor = this.layer;

            this._layer_index = layers.findIndexByDiffuseTextureURL(layer_descriptor.diffuse);


            if (this._layer_index !== -1) {
                // check to see if size matches
                const actual_layer = layers.get(this._layer_index);
                if (!actual_layer.size.equals(layer_descriptor.size)) {
                    console.warn(`Actual terrain layer with texture '${layer_descriptor.diffuse}' has size ${actual_layer.size}, instead of desired ${layer_descriptor.size}. Ignoring this difference, layer will still be used`);
                }
            } else {

                // layer not found, let's create it
                this._layer_index = this._terrain.addLayer(TerrainLayer.fromDescription(layer_descriptor));
            }

        }

        return this._layer_index;
    }

    execute(grid, ecd, node) {
        const _splat_layer_index = this._getLayerIndex();

        const splat = this._splat_map;

        const splat_resolution_x = splat.size.x;
        const splat_resolution_y = splat.size.y;

        // compute scale between grid and splat
        const splat_scale_x = splat_resolution_x / grid.width;
        const splat_scale_y = splat_resolution_y / grid.height;

        const splat_to_grid_x = 1 / splat_scale_x;
        const splat_to_grid_y = 1 / splat_scale_y;

        const radius_outer = node.size * (1 + this.dilation.a) + this.dilation.b;

        const edge_width = node.size * this.weight_edge_easing.a + this.weight_edge_easing.b;
        const radius_inner = radius_outer - edge_width;

        // we shift to the center of the grid cell
        const node_p_x = node.position.x + splat_to_grid_x;
        const node_p_y = node.position.y + splat_to_grid_y;

        // compute affected region in Splat space
        const x0 = Math.floor((node_p_x - radius_outer) * splat_scale_x + 0.5);
        const x1 = Math.floor((node_p_x + radius_outer) * splat_scale_x + 0.5);
        const y0 = Math.ceil((node_p_y - radius_outer) * splat_scale_y + 0.5);
        const y1 = Math.ceil((node_p_y + radius_outer) * splat_scale_y + 0.5);

        const layer_count = splat.depth;

        const splat_data = splat.weightData;

        // execute blending
        for (let y = y0; y <= y1; y++) {
            for (let x = x0; x <= x1; x++) {

                // convert position into grid coordinates
                const grid_x = (x - 0.5) * splat_to_grid_x - splat_to_grid_x;
                const grid_y = (y - 0.5) * splat_to_grid_y - splat_to_grid_y;

                const weight = this._getPointWeight(grid_x, grid_y, node, grid, radius_outer, radius_inner);

                if (weight === 0) {
                    continue;
                }

                const weight_scaled = weight * 255;

                // paint
                const weight_reciprocal_multiplier = max2(0, 1 - weight);

                for (let layer_index = 0; layer_index < layer_count; layer_index++) {

                    const layer_address = splat_resolution_x * splat_resolution_y * layer_index;
                    const texel_address = layer_address + y * splat_resolution_x + x;

                    const source_value = splat_data[texel_address];

                    let destination_value;

                    if (_splat_layer_index === layer_index) {
                        // the channel that we are stamping
                        destination_value = source_value + weight_scaled;
                    } else {
                        // other layers get erased by proportional amount
                        destination_value = source_value * weight_reciprocal_multiplier;
                    }

                    splat_data[texel_address] = clamp(destination_value, 0, 255);

                }
            }
        }

        // mark splat map for update
        splat.weightTexture.needsUpdate = true;
    }

    initialize(grid, ecd, seed) {
        // find layer index
        const terrain = ecd.getAnyComponent(Terrain);

        if (terrain.component === null) {
            throw new Error('No terrain present');
        }

        this._terrain = terrain.component;

        // bind splats
        this._splat_map = terrain.component.splat;

        this._layer_index = -1;

        this._random = seededRandom(seed);

        this.weight.ensureInitialized(grid, seed);
    }
}
