import { assert } from "../../core/assert.js";
import { ArrayIteratorRandom } from "../../core/collection/array/iterator/ArrayIteratorRandom.js";
import { ArrayIteratorSequential } from "../../core/collection/array/iterator/ArrayIteratorSequential.js";
import { PI_HALF } from "../../core/math/PI_HALF.js";
import { seededRandom } from "../../core/math/random/seededRandom.js";
import TaskGroup from "../../core/process/task/TaskGroup.js";
import { countTask } from "../../core/process/task/util/countTask.js";
import { randomCountTask } from "../../core/process/task/util/randomCountTask.js";
import { RuleSelectionPolicyType } from "./RuleSelectionPolicyType.js";


/**
 *
 * @enum {Class<AbstractArrayIterator>}
 */
const POLICY_ITERATORS = {
    [RuleSelectionPolicyType.Sequential]: ArrayIteratorSequential,
    [RuleSelectionPolicyType.Random]: ArrayIteratorRandom
};

export class GridActionRuleSet {

    /**
     *
     * @type {GridCellPlacementRule[]}
     */
    elements = [];

    /**
     *
     * @type {RuleSelectionPolicyType|number}
     */
    policy = RuleSelectionPolicyType.Sequential;

    /**
     * Rules will be evaluated for each cell, this pattern will be applied within each cell
     * @type {number[]}
     */
    pattern = [0, 0];

    /**
     *
     * @type {boolean}
     */
    matchOrigin = true;

    /**
     *
     * @param {GridCellPlacementRule[]} rules
     * @param {RuleSelectionPolicyType} [policy]
     * @param {number[]} [pattern]
     * @param {boolean} [allowEveryRotation]
     * @returns {GridActionRuleSet}
     */
    static from({ rules, policy = RuleSelectionPolicyType.Sequential, pattern = [0, 0], allowEveryRotation }) {
        assert.enum(policy, RuleSelectionPolicyType, 'policy');
        assert.isArray(rules, 'rules');
        assert.isArray(pattern, 'pattern');

        const r = new GridActionRuleSet();

        rules.forEach(r.add, r);
        r.policy = policy;
        r.pattern = pattern;

        if (allowEveryRotation !== undefined) {
            console.warn('.allowEveryRotation is deprecated');
        }

        return r;
    }

    /**
     *
     * @param {GridCellPlacementRule} rule
     */
    add(rule) {
        this.elements.push(rule);
    }

    /**
     *
     * @param {GridData} grid
     * @param {number} seed
     * @param {number} [resolution=1] Number of sub-samples per single grid cell in each dimension
     * @returns {Task}
     */
    process(grid, seed, resolution = 1) {
        assert.isNumber(seed, 'seed');

        const width = grid.width;
        const height = grid.height;

        const random = seededRandom(seed);

        /**
         * @type {Class<AbstractArrayIterator>}
         */
        const RuleIteratorClass = POLICY_ITERATORS[this.policy];

        /**
         *
         * @type {AbstractArrayIterator<GridCellPlacementRule>}
         */
        const ruleIterator = new RuleIteratorClass();

        //initialize rules
        const tInitializeRules = countTask(0, this.elements.length, i => {
            const rule = this.elements[i];

            rule.initialize(grid, seed);
        });


        /**
         *
         * @type {number[]}
         */
        const pattern = this.pattern;
        const patternSize = pattern.length;

        const matchOrigin = this.matchOrigin;

        const sampleCountX = width * resolution;
        const sampleCountY = height * resolution;

        const sampleCount = sampleCountX * sampleCountY;

        const iteratorValue = {
            done: false,
            value: undefined
        };

        const tMain = randomCountTask(seed, 0, sampleCount, index => {


            const sample_y = (index / sampleCountX) | 0;
            const sample_x = index % sampleCountX;

            const x = sample_x / resolution;
            const y = sample_y / resolution;

            ruleIterator.initialize(this.elements);

            ruleIterator.next(iteratorValue);

            while (!iteratorValue.done) {
                /**
                 *
                 * @type {GridCellPlacementRule}
                 */
                const element = iteratorValue.value;

                const matcher = element.pattern;

                sampling_loop: for (let sampleIndex = 0; sampleIndex < patternSize;) {
                    const sampleOffsetX = pattern[sampleIndex++];
                    const sampleOffsetY = pattern[sampleIndex++];

                    const ROTATION_COUNT = element.allowRotation ? 4 : 1;

                    for (let j = 0; j < ROTATION_COUNT; j++) {

                        const rotation = j * PI_HALF;

                        const sin = Math.sin(rotation);
                        const cos = Math.cos(rotation);

                        const final_sample_offset_x = sampleOffsetX * cos - sampleOffsetY * sin;
                        const final_sample_offset_y = sampleOffsetX * sin + sampleOffsetY * cos;

                        const p_x = final_sample_offset_x + x;
                        const p_y = final_sample_offset_y + y;


                        let match;

                        if (matchOrigin) {
                            match = matcher.match(grid, x, y, rotation);
                        } else {
                            match = matcher.match(grid, p_x, p_y, rotation);
                        }

                        if (!match) {
                            continue;
                        }

                        const roll = random();

                        const probabilityValue = element.probability.execute(grid, p_x, p_y, rotation);

                        if (probabilityValue > roll) {

                            element.execute(grid, p_x, p_y, rotation);

                            break sampling_loop;
                        }

                    }
                }

                ruleIterator.next(iteratorValue);
            }

        });

        tMain.addDependency(tInitializeRules);

        return new TaskGroup([tInitializeRules, tMain]);
    }
}
