import { Action } from "../../../src/core/process/undo/Action.js";
import { assert } from "../../../src/core/assert.js";
import { texture_array_2d_copy } from "../../../src/engine/graphics/texture/texture_array_2d_copy.js";
import { clamp } from "../../../src/core/math/clamp.js";

export class ModifyPatchTextureArray2DAction extends Action {
    /**
     *
     * @param {number[]|Uint8Array|Uint32Array|Float32Array} data
     * @param {number[]} data_resolution
     * @param {number[]} bounds
     */
    constructor(data, data_resolution, bounds) {
        super();

        assert.isArrayLike(data, 'data');

        this.__data = data;
        this.__data_resolution = data_resolution;
        this.__bounds = bounds;


        const width = this.__bounds[2] - this.__bounds[0];
        const height = this.__bounds[3] - this.__bounds[1];

        const depth = data_resolution[2];

        const CTOR = Object.getPrototypeOf(data).constructor;

        /**
         * @type {ArrayLike<number>}
         * @private
         */
        this.__patch_new = new CTOR(height * width * depth);
        /**
         * @type {ArrayLike<number>}
         * @private
         */
        this.__patch_old = new CTOR(height * width * depth);
    }

    /**
     *
     * @param {number} center_x
     * @param {number} center_y
     * @param {Sampler2D} stamp
     * @param {number} layer_index
     * @param {number} weight
     * @param {number} clamp_min
     * @param {number} clamp_max
     */
    applyWeightStampToLayer(
        center_x, center_y,
        stamp, layer_index, weight,
        clamp_min = Number.NEGATIVE_INFINITY, clamp_max = Number.POSITIVE_INFINITY
    ) {

        const bounds_x0 = this.__bounds[0];
        const bounds_y0 = this.__bounds[1];

        const bounds_x1 = this.__bounds[2];
        const bounds_y1 = this.__bounds[3];

        const width = bounds_x1 - bounds_x0;
        const height = bounds_y1 - bounds_y0;

        const center_offset_x = (bounds_x0 + bounds_x1) * 0.5 - center_x;
        const center_offset_y = (bounds_y0 + bounds_y1) * 0.5 - center_y;

        const source_data = this.__data;

        const source_width = this.__data_resolution[0];
        const source_height = this.__data_resolution[1];
        const depth = this.__data_resolution[2];

        for (let y = 0; y < height; y++) {
            const marker_v = (y + center_offset_y) / (height - 1);

            for (let x = 0; x < width; x++) {
                const marker_u = (x + center_offset_x) / (width - 1);

                const stamp_value = stamp.sampleChannelBilinearUV(marker_u, marker_v, 3);

                if (Number.isNaN(stamp_value)) {
                    continue;
                }

                const weighed_stamp_value = stamp_value * weight;

                const source_x = (x + bounds_x0);
                const source_y = (y + bounds_y0);

                const weight_reciprocal = weighed_stamp_value / (depth - 1);

                for (let i = 0; i < depth; i++) {
                    const source_layer_address = source_width * source_height * i;
                    const source_texel_address = source_layer_address + source_y * source_width + source_x;

                    const destination_texel_address = width * height * i + y * width + x;

                    const source_value = source_data[source_texel_address];

                    let destination_value;
                    if (i === layer_index) {
                        // the channel that we are stamping
                        destination_value = source_value + weighed_stamp_value;
                    } else {
                        destination_value = source_value - weight_reciprocal;
                    }

                    this.__patch_new[destination_texel_address] = clamp(destination_value, clamp_min, clamp_max);
                }
            }
        }
    }

    get patch_data() {
        return this.__patch_new;
    }

    computeByteSize() {
        // TODO take size of array elements into account
        return this.__patch_new.length + this.__patch_old.length + 280;
    }

    async apply(context) {
        const data = this.__data;

        const depth = this.__data_resolution[2];

        const bounds_x0 = this.__bounds[0];
        const bounds_y0 = this.__bounds[1];

        const bounds_x1 = this.__bounds[2];
        const bounds_y1 = this.__bounds[3];

        const width = bounds_x1 - bounds_x0;
        const height = bounds_y1 - bounds_y0;

        const source_resolution_x = this.__data_resolution[0];
        const source_resolution_y = this.__data_resolution[1];

        // store old data
        texture_array_2d_copy(
            data, bounds_x0, bounds_y0, source_resolution_x, source_resolution_y,
            this.__patch_old, 0, 0, width, height,
            width, height, depth
        );

        texture_array_2d_copy(
            this.__patch_new, 0, 0, width, height,
            data, bounds_x0, bounds_y0, source_resolution_x, source_resolution_y,
            width, height, depth
        );

    }

    async revert(context) {

        const data = this.__data;

        const depth = this.__data_resolution[2];

        const bounds_x0 = this.__bounds[0];
        const bounds_y0 = this.__bounds[1];

        const bounds_x1 = this.__bounds[2];
        const bounds_y1 = this.__bounds[3];

        const width = bounds_x1 - bounds_x0;
        const height = bounds_y1 - bounds_y0;

        const source_resolution_x = this.__data_resolution[0];
        const source_resolution_y = this.__data_resolution[1];

        texture_array_2d_copy(
            this.__patch_old, 0, 0, width, height,
            data, bounds_x0, bounds_y0, source_resolution_x, source_resolution_y,
            width, height, depth
        );
    }
}
