/**
 * Filename: AudioControls.ts
 * Author: Stevan Bosanac
 * Created Date: 2024-09-17
 *
 * Description:
 * The `AudioControls` class in TypeScript provides functionality for managing audio settings and
 * inspecting sounds in a web application.
 *
 * Revision History:
 * 2024-09-17: Initial creation
 *
 */
import { Pane } from 'tweakpane';
import { sound } from '@pixi/sound';
import Locator from '../../../services/Locator';
import { PaneExtended } from '../interfaces/PaneExtended';

/*
 * The `AudioControls` class in TypeScript provides functionality for managing audio settings and
 * inspecting sounds in a web application.
 */
export class AudioControls
{
    static controlName = 'AudioControls';
    pane: PaneExtended;
    inspectorFolders: any[];
    soundsInspector: any;
    panel: Pane;
    mutedCheckbox: any;
    volumeSpider: any;
    musicFolder: any;
    musicVolumeSpider: any;
    soundFolder: any;
    mutedSoundsCheckbox: any;
    soundsVolumeSpider: any;
    volumeAll: any;
    musicVolume: any;
    sfxVolume: any;
    /**
     * The constructor initializes properties for a PaneExtended object with a locator and an empty
     * array for inspector folders.
     * @param {PaneExtended} pane - The `pane` parameter is an instance of the `PaneExtended` class,
     * which is likely used to manage and display content within a pane or window in the application.
     * @param {any} locator - The `locator` parameter is a variable that can hold any type of value. It
     * is passed as an argument to the constructor function along with the `pane` parameter, which is
     * of type `PaneExtended`.
     */
    constructor(pane: PaneExtended)
    {
        this.pane = pane;
        this.inspectorFolders = [];
    }
    /** The `initialize` function adds audio-related checkboxes to a folder in a user interface. */
    initialize()
    {
        const folder = this.pane.addFolder({ title: 'Audio', expanded: false });

        this.addAudioMixerChkBx(folder);
        this.addAudioSettingsChkBx(folder);
    }
    /**
     * The function `addAudioMixerChkBx` adds a checkbox to toggle the display of a sound inspector in a
     * folder, updating the inspector's contents based on user interaction.
     * @param folder - The `addAudioMixerChkBx` function takes a `folder` parameter, which is used to add
     * a binding for the audio mixer checkbox in a UI interface. The `folder` parameter likely represents
     * the parent folder or container where the audio mixer checkbox will be added.
     */
    addAudioMixerChkBx(folder: any)
    {
        const mixer = { show: false };
        const soundInspector = folder.addBinding(mixer, 'show', { label: 'Show Sound Inspector:' });

        soundInspector.on('change', (e: any) =>
        {
            if (e.value)
            {
                this.createInspector();
                const addAllSoundsToInspector = () =>
                {
                    this.inspectorFolders.forEach((folder) => this.soundsInspector.remove(folder));
                    [
                        ...this.mapAndSortSounds((sound as any)._sounds, 'ambient'),
                        ...this.mapAndSortSounds((sound as any)._sounds, 'music'),
                        ...this.mapAndSortSounds((sound as any)._sounds, 'sfx'),
                    ]
                        .forEach((soundName) => this.addSoundToInspector(soundName));
                };

                addAllSoundsToInspector();
                Locator.events.on('audio_node_connected', addAllSoundsToInspector);
                this.setSoundsProps();
            }
            else
            {
                this.soundsInspector = null;
                this.inspectorFolders = [];
                this.removeElements('sounds-inspector-container');
            }
            this.pane.settings = { audio: { ...this.pane.settings.audio, showInspector: e.value } };
        });
        setTimeout(() => soundInspector.controller.value.rawValue = this.pane.settings.audio?.showInspector);
    }
    /**
     * The function `addAudioSettingsChkBx` adds a checkbox to toggle the visibility of audio settings in
     * a folder, updating the UI and settings accordingly.
     * @param folder - The `folder` parameter in the `addAudioSettingsChkBx` function is typically a GUI
     * folder element where the audio settings checkbox will be added. This folder is used to organize
     * and group related GUI elements together for better user interface organization.
     */
    addAudioSettingsChkBx(folder: any)
    {
        const mixer = { show: false };
        const audioSettings = folder.addBinding(mixer, 'show', { label: 'Show Audio Settings:' });

        audioSettings.on('change', (e: any) =>
        {
            if (e.value)
            {
                this.createPanel();
            }
            else
            {
                this.removeElements('audio-settings-container');
            }
            this.pane.settings = { audio: { ...this.pane.settings.audio, showAudio: e.value } };
        });
        audioSettings.controller.value.rawValue = this.pane.settings.audio?.showAudio;
    }
    /* The above code is a TypeScript function called `mapAndSortSounds` that takes in two parameters:
  `sounds` (an object containing sound data) and `soundGroup` (a string representing a sound group). */
    mapAndSortSounds = (sounds: any, soundGroup: string) => Object.entries(sounds)
        .filter(([, sound]) => (sound as any).group === soundGroup)
        .map(([soundName]) => soundName)
        .sort();
    /**
     * The function `removeElements` removes all elements with a specified class name from the DOM.
     * @param className - The `className` parameter is a string that represents the class name of the
     * elements you want to remove from the document.
     */
    removeElements(className: string)
    {
        const myElements: HTMLCollectionOf<Element> = document.getElementsByClassName(className);
        // const paneElements = [...myElements];
        // paneElements.forEach((element) => element.remove());

        for (let i = 0; i < myElements.length; i++)
        {
            const element = myElements[i];

            element.remove();
        }
    }
    /**
     * The `createPanel` function in TypeScript creates an audio settings panel with options to adjust
     * overall volume, music volume, and sound effects volume.
     */
    createPanel()
    {
        this.panel = new Pane({ title: 'Audio Settings', container: (this.pane as any).containerElem_.parentElement });
        this.panel.element.classList.add('audio-settings-container');
        this.panel.element.style.width = '300px';
        this.panel.element.style.float = 'right';
        this.panel.element.style.margin = '5px';
        this.mutedCheckbox = (this.panel as any).addBinding((Locator.audio as any), 'muteAll');
        this.volumeSpider = (this.panel as any).addBlade({
            view: 'slider',
            label: 'volume',
            min: 0,
            max: 2,
            value: (Locator.audio as any).context.volume,
        });
        this.volumeSpider.on('change', (e: any) =>
        {
            this.handleVolumeSliderChange(e);
        });
        this.musicFolder = (this.panel as any).addFolder({ title: 'Music' });
        this.musicFolder.addBinding((Locator.audio as any), 'muteMusic');
        this.musicVolumeSpider = this.musicFolder.addBlade({
            view: 'slider',
            label: 'volume',
            min: 0,
            max: 2,
            value: 1,
        });
        this.musicVolumeSpider.on('change', (e: any) =>
        {
            this.handleMusicVolumeSliderChange(e);
        });
        this.soundFolder = (this.panel as any).addFolder({ title: 'Sounds' });
        this.mutedSoundsCheckbox = this.soundFolder.addBinding((Locator.audio as any), 'muteSFX');
        this.soundsVolumeSpider = this.soundFolder.addBlade({
            view: 'slider',
            label: 'volume',
            min: 0,
            max: 2,
            value: 1,
        });
        this.soundsVolumeSpider.on('change', (e: any) =>
        {
            this.handleSoundsVolumeSliderChange(e);
        });
    }
    /**
     * The function `handleVolumeSliderChange` updates the `volumeAll` property based on the value of the
     * event `e`.
     * @param e - The parameter `e` typically represents the event object that is passed to the event
     * handler function when an event is triggered. In this case, it seems like `e` is referring to the
     * event object related to a volume slider change event. The `value` property of the event object is
     * being used
     * @param e.value
     */
    handleVolumeSliderChange(e: any)
    {
        (Locator.audio as any).volumeAll = e.value;
    }
    /**
     * The function `handleMusicVolumeSliderChange` updates the music volume based on the value of a
     * slider input.
     * @param e - The parameter `e` typically represents the event object that is passed to the event
     * handler function when an event occurs, such as a slider value change event. In this case, `e`
     * likely contains information about the event, including the new value of the music volume slider.
     * @param e.value
     */
    handleMusicVolumeSliderChange(e: any)
    {
        (Locator.audio as any).musicVolume = e.value;
    }
    /**
     * The function `handleSoundsVolumeSliderChange` updates the volume of sound effects based on the
     * value of a slider.
     * @param e - The parameter `e` typically represents an event object in JavaScript. In this context,
     * it seems to be referring to an event object related to a slider change event. The `e.value` likely
     * represents the new value of the slider after the change.
     * @param e.value
     */
    handleSoundsVolumeSliderChange(e: any)
    {
        (Locator.audio as any).sfxVolume = e.value;
    }
    /** The function `createInspector` creates a pane for inspecting sounds with specific styling properties. */
    createInspector()
    {
        this.soundsInspector = new Pane({ title: 'Sounds Inspector', container: (this.pane as any).containerElem_.parentElement });
        this.soundsInspector.element.classList.add('sounds-inspector-container');
        this.soundsInspector.element.style.width = '300px';
        this.soundsInspector.element.style.float = 'left';
        this.soundsInspector.element.style.margin = '5px';
        this.soundsInspector.element.style.overflowY = 'scroll';
        this.soundsInspector.element.style.maxHeight = '80vh';
    }
    /**
     * The function adds a sound alias to an inspector by creating a sound folder if it doesn't already
     * exist.
     * @param soundAlias - The `soundAlias` parameter is a string that represents the alias or name of a
     * sound that you want to add to the inspector.
     * @returns If `this.soundsInspector` is falsy (e.g., `null`, `undefined`, `false`, `0`, `''`,
     * `NaN`), then the function will return nothing (undefined) because of the `return` statement.
     */
    addSoundToInspector(soundAlias: any)
    {
        if (!this.soundsInspector) { return; }
        this.createSoundFolder(this.soundsInspector, soundAlias);
    }
    /**
     * The function `setSoundsProps` updates the properties of sounds in an inspector based on their
     * current state and volume levels.
     * @returns The `setSoundsProps()` function returns nothing (`undefined`) as it does not have a
     * return statement specified within the function. The function performs operations on the
     * `soundsInspector` object and updates the sound properties displayed in the UI elements, but it
     * does not explicitly return a value.
     */
    setSoundsProps()
    {
        if (!this.soundsInspector) { return; }
        const soundsProps = Object.values((sound as any)._sounds).reduce((acc, sound) =>
        {
            const { url, isPlaying, volume, fileName, group } = (sound as any);
            const soundName = url.startsWith('data') ? fileName : this.getFileNameFromURL(url);
            let totalVolume;

            switch (group)
            {
                case 'music':
                    totalVolume = volume * ((Locator.audio as any) as any).nodes.fadeMusic.gain.value;
                    break;
                case 'ambient':
                    totalVolume = volume * ((Locator.audio as any) as any).nodes.fadeAmbient.gain.value;
                    break;
                default:
                    totalVolume = volume;
                    break;
            }

            return Object.assign(acc, { [soundName]: { isPlaying, volume: totalVolume } });
        }, {});

        this.soundsInspector.children.forEach((folder: any) =>
        {
            const soundPropsElement = folder.element.getElementsByClassName('sound-props')[0];

            soundPropsElement.innerHTML = (soundsProps as any)[folder.title]?.isPlaying ? (soundsProps as any)[folder.title].volume.toFixed(2) : '';
            soundPropsElement.style.color = 'green';
        });
        requestAnimationFrame(this.setSoundsProps.bind(this));
    }
    /**
     * The function `createSoundFolder` creates a folder in a pane with controls for a sound instance and
     * allows for uploading new sound files.
     * @param pane - The `pane` parameter in the `createSoundFolder` function is likely a reference to a
     * UI element or container where the sound folder will be added. It could be a panel, window, or any
     * other UI component that can contain folders or elements. The function seems to be designed to
     * create a
     * @param soundAlias - The `soundAlias` parameter in the `createSoundFolder` function is a unique
     * identifier or key that represents a specific sound file. It is used to locate and retrieve the
     * corresponding sound instance from the audio locator.
     */
    createSoundFolder(pane: any, soundAlias: any)
    {
        const soundInstance = (Locator.audio as any).find(soundAlias);
        const folder = pane.addFolder({ title: this.getFileNameFromURL(soundInstance.url) });

        folder.expanded = false;
        const handleUpload = ((soundAlias: any) =>
        {
            folder.children.forEach((ch: any) => ch.dispose());
            this.createControls(folder, soundAlias, handleUpload);
        });

        this.createControls(folder, soundAlias, handleUpload);
        const soundPropsElement = document.createElement('div');

        soundPropsElement.style.position = 'absolute';
        soundPropsElement.style.right = '50px';
        soundPropsElement.style.top = '7px';
        soundPropsElement.classList.add('sound-props');
        folder.element.appendChild(soundPropsElement);
        this.inspectorFolders.push(folder);
    }
    /**
     * The `createControls` function creates a set of controls for adjusting volume, loop, single
     * instance, playing, stopping, and uploading sound files.
     * @param folder - Folder is an object that represents a group or container in a user interface where
     * various controls can be added. In this context, it seems to be used to organize and display
     * controls related to sound settings and playback.
     * @param soundAlias - The `soundAlias` parameter in the `createControls` function is used to
     * identify a specific sound within the audio locator. It is a unique identifier or key that helps to
     * locate and manipulate the properties of the corresponding sound object.
     * @param onUpload - The `onUpload` parameter in the `createControls` function is a callback function
     * that is used to handle the upload functionality. When the "upload" button is clicked in the
     * controls interface, the `handleUpload` function is called, which typically triggers a file upload
     * dialog or process to load a
     */
    createControls(folder: any, soundAlias: any, onUpload: any)
    {
        const slider = folder.addBlade({
            view: 'slider',
            label: 'volume',
            min: 0,
            max: 1,
            value: (Locator.audio as any).find(soundAlias).volume,
        });

        slider.on('change', (e: any) =>
        {
            (Locator.audio as any).find(soundAlias).volume = e.value;
        });
        const loop = folder.addBinding((Locator.audio as any).find(soundAlias), 'loop');

        loop.on('change', (e: any) => (Locator.audio as any).find(soundAlias).loop = e.value);
        const singleInstance = folder.addBinding((Locator.audio as any).find(soundAlias), 'singleInstance');

        singleInstance.on('change', (e: any) => (Locator.audio as any).find(soundAlias).singleInstance = e.value);
        const play = folder.addButton({ title: 'play' });

        play.on('click', () =>
        {
            (Locator.audio as any).find(soundAlias).play();
        });
        const stop = folder.addButton({ title: 'stop' });

        stop.on('click', () =>
        {
            (Locator.audio as any).find(soundAlias).stop();
        });
        const upload = folder.addButton({ title: 'upload' });
        const handleUpload = this.createLoadSoundFile(soundAlias, onUpload);

        upload.on('click', handleUpload);
    }
    /**
     * The function extracts the file name from a given URL by splitting the URL string and returning the
     * last part before the file extension.
     * @param url - The `url` parameter is the URL from which you want to extract the file name.
     * @returns the file name extracted from the given URL.
     */
    getFileNameFromURL(url: string)
    {
        // var regex = /^.*\/(.*)\.(.*)\?.*$/;
        // return url.match(regex)[1];
        const fileName = url.split('#')[0].split('?')[0].split('/').pop().split('.')[0];

        return fileName;
    }
    /**
     * The function `createLoadSoundFile` is used to upload and process audio files, creating sound
     * instances and connecting them to specific audio groups.
     * @param soundAlias - The `soundAlias` parameter in the `createLoadSoundFile` function is a unique
     * identifier or name for the sound file being loaded. It is used to reference and manage the sound
     * within the application.
     * @param onUpload - The `onUpload` parameter in the `createLoadSoundFile` function is a callback
     * function that is called after a sound file has been successfully uploaded and processed. It is
     * typically used to perform additional actions or updates related to the uploaded sound file.
     * @returns The `createLoadSoundFile` function returns a function that handles the process of
     * uploading an audio file, creating a sound instance using pixi-sound, and connecting it to the
     * appropriate audio group node based on the group specified for the sound alias. The function
     * returns a Promise that resolves with the created sound instance if the uploaded file is a valid
     * audio file, and rejects with an error if the file
     */
    createLoadSoundFile(soundAlias: any, onUpload: any)
    {
        return () =>
        {
            (Locator.audio as any).find(soundAlias).stop();
            this.soundsInspector.disabled = true;

            return new Promise((resolve, reject) =>
            {
                // создаем input и привязываем к нему слушатель
                const input = document.createElement('input');

                input.type = 'file';
                input.oncancel = () =>
                {
                    this.soundsInspector.disabled = false;
                    resolve('');
                };
                input.onchange = (e: any) =>
                {
                    const file = e.target.files[0];
                    // проверяем, является ли файл аудио

                    if (file.type.match('audio.*'))
                    {
                        const reader = new FileReader();

                        reader.onload = async (event) =>
                        {
                            // создаем sound из pixi-sound
                            const isLoop = sound.find(soundAlias).loop;
                            const isSingleInstance = sound.find(soundAlias).singleInstance;
                            const volume = sound.find(soundAlias).volume;
                            const group = (sound.find(soundAlias) as any).group;

                            delete (sound as any)._sounds[soundAlias];
                            const soundInstance = sound.add(soundAlias, event.target.result);

                            (sound as any)._sounds[soundAlias].fileName = soundAlias;
                            soundInstance.loop = isLoop;
                            soundInstance.singleInstance = isSingleInstance;
                            soundInstance.volume = volume;
                            if (group === 'music')
                            {
                                (Locator.audio as any).connectToMusicNode(soundAlias);
                            }
                            else if (group === 'ambient')
                            {
                                (Locator.audio as any).connectToAmbientNode(soundAlias);
                            }
                            else if (group === 'sfx')
                            {
                                (Locator.audio as any).connectToSFXNode(soundAlias);
                            }
                            resolve(soundInstance);
                        };
                        // генерация URL для аудиофайла
                        reader.readAsDataURL(file);
                    }
                    else
                    {
                        this.soundsInspector.disabled = false;
                        reject(new Error('Invalid audio file'));
                    }
                };
                // открываем диалог выбора файла
                input.click();
            }).then((soundInstance) =>
            {
                if (!soundInstance) { return; }
                onUpload(soundAlias);
                this.soundsInspector.disabled = false;
            });
        };
    }
}
