import { BvhClient } from "../../../../core/bvh2/bvh3/BvhClient.js";
import { combine_hash } from "../../../../core/collection/array/combine_hash.js";
import List from '../../../../core/collection/list/List.js';
import { aabb3_array_compute_from_sphere } from "../../../../core/geom/3d/aabb/aabb3_array_compute_from_sphere.js";
import Vector1 from "../../../../core/geom/Vector1.js";
import {
    interpolate_irradiance_linear
} from "../../../../core/math/physics/irradiance/interpolate_irradiance_linear.js";
import {
    interpolate_irradiance_lograrithmic
} from "../../../../core/math/physics/irradiance/interpolate_irradiance_lograrithmic.js";
import { interpolate_irradiance_smith } from "../../../../core/math/physics/irradiance/interpolate_irradiance_smith.js";
import { objectKeyByValue } from "../../../../core/model/object/objectKeyByValue.js";
import { computeHashFloat } from "../../../../core/primitives/numbers/computeHashFloat.js";
import { number_compare_ascending } from "../../../../core/primitives/numbers/number_compare_ascending.js";
import { compareStrings } from "../../../../core/primitives/strings/compareStrings.js";
import { computeStringHash } from "../../../../core/primitives/strings/computeStringHash.js";
import { SoundAttenuationFunction } from "./SoundAttenuationFunction.js";
import { SoundEmitterFlags } from "./SoundEmitterFlags.js";
import { SoundPanningModelType } from "./SoundPanningModelType.js";
import { SoundTrack } from "./SoundTrack.js";

const DEFAULT_DISTANCE_MIN = 1;
const DEFAULT_DISTANCE_MAX = 10000;

const DEFAULT_CHANNEL = null;

/**
 * @class
 */
export class SoundEmitter {


    /**
     * @readonly
     * @type {List<SoundTrack>}
     */
    tracks = new List();

    /**
     *
     * @type {String|SoundEmitterChannels|null}
     */
    channel = DEFAULT_CHANNEL;

    /**
     *
     * @type {number}
     * @private
     */
    __distanceMin = 1;

    /**
     *
     * @type {number}
     * @private
     */
    __distanceMax = 10000;

    /**
     * @deprecated
     * @type {number}
     * @private
     */
    __distanceRolloff = 1;

    /**
     * TODO add to binary serialization
     * @type {SoundPanningModelType}
     */
    panningModel = SoundPanningModelType.HRTF;

    /**
     * Type of attenuation used for sound fall-off, this is only used if Attenuation flag is set
     * @type {SoundAttenuationFunction|number}
     */
    attenuation = SoundAttenuationFunction.Smith;

    /**
     *
     * @type {number|SoundEmitterFlags}
     */
    flags = 0;

    /**
     * @readonly
     */
    nodes = {
        /**
         * @type {GainNode}
         */
        volume: null,
        /**
         * @type {PannerNode}
         */
        panner: null,
        /**
         * @type {GainNode}
         */
        attenuation: null,
        /**
         * One of the other nodes, depending on the configuration
         * @type {AudioNode}
         */
        endpoint: null
    };

    /**
     *
     * @type {Vector1}
     */
    volume = new Vector1(1);

    /**
     * @readonly
     * @type {BvhClient}
     */
    bvh = new BvhClient();

    /**
     *
     * @constructor
     */
    constructor() {

        this.volume.onChanged.add((value) => {
            const volume_node = this.nodes.volume;

            if (volume_node !== null) {
                volume_node.gain.setValueAtTime(value, 0);
            }
        });

    }

    /**
     *
     * @param {SoundEmitter} other
     * @returns number
     */
    compare(other) {
        const tracks = this.tracks.compare(other.tracks);

        if (tracks !== 0) {
            return tracks;
        }

        const channel = compareStrings(this.channel, other.channel);

        if (channel !== 0) {
            return channel;
        }

        const distanceMin = number_compare_ascending(this.__distanceMin, other.__distanceMin);

        if (distanceMin !== 0) {
            return distanceMin;
        }
        const distanceMax = number_compare_ascending(this.__distanceMax, other.__distanceMax);

        if (distanceMax !== 0) {
            return distanceMax;
        }

        const panningModel = number_compare_ascending(this.panningModel, other.panningModel);

        if (panningModel !== 0) {
            return panningModel;
        }

        const attenuation = number_compare_ascending(this.attenuation, other.attenuation);

        if (attenuation !== 0) {
            return attenuation;
        }

        const flags = number_compare_ascending(this.flags, other.flags);

        if (flags !== 0) {
            return flags;
        }

        const volume = number_compare_ascending(this.volume.getValue(), other.volume.getValue());

        if (volume !== 0) {
            return volume;
        }


        return 0;
    }


    /**
     *
     * @param {number|SoundEmitterFlags} flag
     * @returns {void}
     */
    setFlag(flag) {
        this.flags |= flag;
    }

    /**
     *
     * @param {number|SoundEmitterFlags} flag
     * @returns {void}
     */
    clearFlag(flag) {
        this.flags &= ~flag;
    }

    /**
     *
     * @param {number|SoundEmitterFlags} flag
     * @param {boolean} value
     */
    writeFlag(flag, value) {
        if (value) {
            this.setFlag(flag);
        } else {
            this.clearFlag(flag);
        }
    }

    /**
     *
     * @param {number|SoundEmitterFlags} flag
     * @returns {boolean}
     */
    getFlag(flag) {
        return (this.flags & flag) === flag;
    }


    /**
     * @deprecated
     * @param {boolean} v
     */
    set isPositioned(v) {
        this.writeFlag(SoundEmitterFlags.Spatialization, v);
    }

    /**
     * @deprecated
     * @return {boolean}
     */
    get isPositioned() {
        return this.getFlag(SoundEmitterFlags.Spatialization);
    }

    /**
     * @return {AudioNode}
     */
    getTargetNode() {
        return this.nodes.endpoint;
    }

    /**
     * @returns {boolean}
     * @param {SoundEmitter} other
     */
    equals(other) {
        return this.channel === other.channel
            && this.flags === other.flags
            && this.__distanceMin === other.__distanceMin
            && this.__distanceMax === other.__distanceMax
            && this.attenuation === other.attenuation
            && this.tracks.equals(other.tracks)
            ;
    }

    /**
     *
     * @return {number}
     */
    hash() {
        return combine_hash(
            computeStringHash(this.channel),
            this.flags,
            computeHashFloat(this.__distanceMin),
            computeHashFloat(this.__distanceMax),
            this.attenuation,
            this.tracks.hash()
        );
    }

    /**
     *
     * @param {AudioContext} ctx
     */
    buildNodes(ctx) {
        const nodes = this.nodes;

        nodes.volume = ctx.createGain();

        if (this.getFlag(SoundEmitterFlags.Attenuation)) {
            nodes.attenuation = ctx.createGain();
        }

        if (this.getFlag(SoundEmitterFlags.Spatialization)) {
            nodes.panner = ctx.createPanner();


            //
            if (this.panningModel === SoundPanningModelType.HRTF) {
                nodes.panner.panningModel = 'HRTF';
            } else if (this.panningModel === SoundPanningModelType.EqualPower) {
                nodes.panner.panningModel = 'equalpower';
            } else {
                console.error('Invalid value of panning model type:', this.panningModel);

                nodes.panner.panningModel = 'equalpower';
            }

            // we set up distance model in the most efficient way, since we are ignoring it anyway and use custom distance model via a GainNode instead
            nodes.panner.distanceModel = 'linear';
            nodes.panner.rolloffFactor = 0;
            nodes.panner.refDistance = this.distanceMin;
            nodes.panner.maxDistance = this.distanceMax;

        }

        //do wiring
        if (this.getFlag(SoundEmitterFlags.Attenuation | SoundEmitterFlags.Spatialization)) {
            nodes.attenuation.connect(nodes.panner);
            nodes.volume.connect(nodes.attenuation);

            nodes.endpoint = nodes.panner;
        } else if (this.getFlag(SoundEmitterFlags.Spatialization)) {
            nodes.volume.connect(nodes.panner);

            nodes.endpoint = nodes.panner;
        } else if (this.getFlag(SoundEmitterFlags.Attenuation)) {
            nodes.volume.connect(nodes.attenuation);

            nodes.endpoint = nodes.attenuation;
        } else {
            nodes.endpoint = nodes.volume;
        }

        nodes.volume.gain.setValueAtTime(this.volume.getValue(), 0);
    }

    /**
     * @private
     * @param {number} distance
     */
    writeAttenuationVolume(distance) {

        let volume;

        const attenuation = this.attenuation;

        const distance_min = this.__distanceMin;
        const distance_max = this.__distanceMax;

        if (attenuation === SoundAttenuationFunction.Linear) {
            volume = interpolate_irradiance_linear(distance, distance_min, distance_max);
        } else if (attenuation === SoundAttenuationFunction.Logarithmic) {
            volume = interpolate_irradiance_lograrithmic(distance, distance_min, distance_max);
        } else if (attenuation === SoundAttenuationFunction.Smith) {
            volume = interpolate_irradiance_smith(distance, distance_min, distance_max);
        } else {
            //unsupported function, don't attenuate
            volume = 1;
        }

        /**
         *
         * @type {GainNode}
         */
        const pv = this.nodes.attenuation;

        if (pv !== null) {
            pv.gain.setValueAtTime(volume, 0);
        }
    }

    /**
     *
     * @returns {number}
     */
    get distanceMin() {
        return this.__distanceMin;
    }

    /**
     * @param {number} v
     */
    set distanceMin(v) {
        this.__distanceMin = v;
    }

    /**
     *
     * @returns {number}
     */
    get distanceMax() {
        return this.__distanceMax;
    }

    /**
     * @param {number} v
     */
    set distanceMax(v) {
        this.__distanceMax = v;
    }


    /**
     * @deprecated
     * @returns {number}
     */
    get distanceRolloff() {
        return this.__distanceRolloff;
    }

    /**
     * @deprecated
     * @param {number} v
     */
    set distanceRolloff(v) {
        this.__distanceRolloff = v;

        const panner = this.nodes.panner;

        if (panner !== null) {
            panner.rolloffFactor = v;
        }
    }

    /**
     *
     * @param {number} x
     * @param {number} y
     * @param {number} z
     */
    updatePosition(x, y, z) {
        if (this.getFlag(SoundEmitterFlags.Spatialization)) {
            //update position of the panner node
            const nodes = this.nodes;

            /**
             *
             * @type {PannerNode}
             */
            const panner = nodes.panner;

            if (panner !== null) {
                panner.setPosition(x, y, z);
            }
        }

        const distanceMax = this.distanceMax;

        const bounds = this.bvh.bounds;

        aabb3_array_compute_from_sphere(bounds, 0, x, y, z, distanceMax);

        this.bvh.write_bounds();
    }

    /**
     *
     * @param {number} [duration] fade duration in seconds
     * @param startAfter
     */
    fadeOutAllTracks(duration = 1, startAfter = 0) {

        const tracks = this.tracks;
        const n = tracks.length;

        for (let i = 0; i < n; i++) {
            const soundTrack = tracks.get(i);

            soundTrack.setVolumeOverTime(0, duration, startAfter);
        }
    }

    stopAllTracks() {
        const tracks = this.tracks;
        const n = tracks.length;

        for (let i = 0; i < n; i++) {
            const soundTrack = tracks.get(i);

            soundTrack.playing = false;
        }
    }

    toJSON() {
        return {
            isPositioned: this.getFlag(SoundEmitterFlags.Spatialization),
            isAttenuated: this.getFlag(SoundEmitterFlags.Attenuation),
            channel: this.channel,
            volume: this.volume.toJSON(),
            tracks: this.tracks.toJSON(),
            distanceMin: this.distanceMin,
            distanceMax: this.distanceMax,
            attenuation: objectKeyByValue(SoundAttenuationFunction, this.attenuation),
            panningModel: objectKeyByValue(SoundPanningModelType, this.panningModel)
        };
    }

    fromJSON(json) {
        if (json.isPositioned !== undefined) {
            this.writeFlag(SoundEmitterFlags.Spatialization, json.isPositioned);
        } else {
            this.clearFlag(SoundEmitterFlags.Spatialization);
        }

        if (json.channel !== undefined) {
            this.channel = json.channel;
        } else {
            this.channel = DEFAULT_CHANNEL;
        }

        if (json.volume !== undefined) {
            this.volume.fromJSON(json.volume);
        }

        if (typeof json.distanceMin === "number") {
            this.distanceMin = json.distanceMin;
        } else {
            this.distanceMin = DEFAULT_DISTANCE_MIN;
        }

        if (typeof json.distanceMax === "number") {
            this.distanceMax = json.distanceMax;
        } else {
            this.distanceMax = DEFAULT_DISTANCE_MAX;
        }

        if (typeof json.isAttenuated === "boolean") {
            this.writeFlag(SoundEmitterFlags.Attenuation, json.isAttenuated);
        } else {
            this.setFlag(SoundEmitterFlags.Attenuation);
        }

        if (typeof json.attenuation === "string") {
            const attenuation = SoundAttenuationFunction[json.attenuation];

            if (attenuation === undefined) {
                throw new Error(`Unknown attenuation type '${json.attenuation}', valid types are: ${Object.keys(SoundAttenuationFunction)}`);
            }

            this.attenuation = attenuation;
        } else {
            this.attenuation = SoundAttenuationFunction.Linear;
        }

        //tracks
        if (json.tracks !== undefined) {
            this.tracks.fromJSON(json.tracks, SoundTrack);
        }

        if (json.panningModel === undefined) {
            this.panningModel = SoundPanningModelType.HRTF;
        } else {
            const model = SoundPanningModelType[json.panningModel];

            if (model === undefined) {
                throw new Error(`Unknown panning model '${json.panningModel}', valid types are: ${Object.keys(SoundPanningModelType)}`);
            }

            this.panningModel = model;
        }
    }


    /**
     *
     * @param json
     * @returns {SoundEmitter}
     */
    static fromJSON(json) {
        const result = new SoundEmitter();

        result.fromJSON(json);

        return result;
    }
}

SoundEmitter.typeName = "SoundEmitter";


