/*******************************************************************************
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or (at your option) any later
 * version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see <http://www.gnu.org/licenses/>.
 * *****************************************************************************
 * Original Author: Gopi Sankar Karmegam
 ******************************************************************************/
/* jshint moz:true */

const Main = imports.ui.main;
const PopupMenu = imports.ui.popupMenu;
const VolumeMenu = imports.ui.status.volume;
const { Atk, St, GObject, GLib } = imports.gi;

const Gvc = imports.gi.Gvc;
const ExtensionUtils = imports.misc.extensionUtils;
const Config = imports.misc.config;
const Me = ExtensionUtils.getCurrentExtension();
const Lib = Me.imports.convenience;
const _d = Lib._log;
const Prefs = Me.imports.prefs;
const SignalManager = Lib.SignalManager;

function _isDeviceInValid(uidevice) {
    return (!uidevice || (uidevice.description != null && uidevice.description.match(/Dummy\s+(Output|Input)/gi)));
}

function getMixerControl() {
    return VolumeMenu.getMixerControl();
}

var ProfileMenuItem = class ProfileMenuItem
    extends PopupMenu.PopupMenuItem {
    constructor(id, title, profileName, callback) {
        super(title);
        this._init(id, title, profileName, callback);
    }

    _init(id, title, profileName, callback) {
        if (super._init) {
            super._init(title);
        }
        _d("ProfileMenuItem: _init:" + title);
        this.id = id;
        this.profileName = profileName;
        this._ornamentLabel.set_style("min-width: 3em;margin-left: 3em;");
        this.setProfileActive(false);
        this.connect('activate', () => {
            _d("Activating Profile:" + id + profileName);
            callback(this.id, this.profileName);
        });
    }

    setProfileActive(active) {
        if (active) {
            this.setOrnament(PopupMenu.Ornament.DOT);
            // this._ornamentLabel.text = "\u2727";
            this._ornamentLabel.text = "\u266A";
            if (this.add_style_pseudo_class) {
                this.remove_style_pseudo_class('insensitive');
            }
            else {
                this.actor.remove_style_pseudo_class('insensitive');
            }
        }
        else {
            this.setOrnament(PopupMenu.Ornament.NONE);
            if (this.add_style_pseudo_class) {
                this.add_style_pseudo_class('insensitive');
            }
            else {
                this.actor.add_style_pseudo_class('insensitive');
            }
        }
    }

    setVisibility(visibility) {
        this.actor.visible = visibility;
    }
}

var SoundDeviceMenuItem = class SoundDeviceMenuItem extends PopupMenu.PopupImageMenuItem {
    constructor(id, title, icon_name, profiles, callback, profileCallback) {
        super(title, icon_name);
        this._init(id, title, icon_name, profiles, callback, profileCallback);
    }

    _init(id, title, icon_name, profiles, callback, profileCallback) {
        if (super._init) {
            super._init(title, icon_name);
        }
        _d("SoundDeviceMenuItem: _init:" + title);
        this.id = id;
        this.title = title;
        this.icon_name = icon_name;
        this.profiles = (profiles) ? profiles : [];

        this.profilesitems = new Map();
        for (let profile of this.profiles) {
            if (!this.profilesitems.has(profile.name)) {
                this.profilesitems.set(profile.name, new ProfileMenuItem(id, "Profile: " + profile.human_name, profile.name, profileCallback));
            }
        }

        this.connect('activate', () => {
            _d("Device Change request for " + id);
            callback(this.id);
        });
        this.available = true;
        this.activeProfile = "";
        this.activeDevice = false;
        this.visible = false;
    }

    isAvailable() {
        return this.available;
    }

    setAvailable(_ac) {
        this.available = _ac;
    }

    setActiveProfile(_p) {
        if (_p && this.activeProfile != _p) {
            if (this.profilesitems.has(this.activeProfile)) {
                this.profilesitems.get(this.activeProfile).setProfileActive(false);
            }
            this.activeProfile = _p;
            if (this.profilesitems.has(_p)) {
                this.profilesitems.get(_p).setProfileActive(true);
            }
        }
    }

    setVisibility(_v) {
        this.actor.visible = _v;
        if (!_v) {
            this.profilesitems.forEach((p) => p.setVisibility(false));
        }
        this.visible = _v;
    };

    isVisible() {
        return this.visible;
    }

    setActiveDevice(_a) {
        this.activeDevice = _a;
        if (!_a) {
            this.setOrnament(PopupMenu.Ornament.NONE);
        }
        else {
            this.setOrnament(PopupMenu.Ornament.CHECK);
            this._ornamentLabel.text = '\u266B';
        }
    }

    setProfileVisibility(_v) {
        this.profilesitems.forEach(p =>
            p.setVisibility(_v && this.canShowProfile()));
    }

    canShowProfile() {
        return (this.isVisible() && this.profilesitems.size > 1);
    }
}

if (parseFloat(Config.PACKAGE_VERSION) >= 3.34) {
    ProfileMenuItem = GObject.registerClass({ GTypeName: 'ProfileMenuItem' }, ProfileMenuItem);

    SoundDeviceMenuItem = GObject.registerClass({ GTypeName: 'SoundDeviceMenuItem' }, SoundDeviceMenuItem);
}

var SoundDeviceChooserBase = class SoundDeviceChooserBase {

    constructor(deviceType) {
        _d("SDC: init");
        this.menuItem = new PopupMenu.PopupSubMenuMenuItem('Extension initialising...', true);
        this.deviceType = deviceType;
        this._devices = new Map();
        let _control = this._getMixerControl();
        this._settings = Lib.getSettings(Prefs.SETTINGS_SCHEMA);
        _d("Constructor:" + deviceType);

        this._setLog();
        this._signalManager = new SignalManager();
        this._signalManager.addSignal(this._settings, "changed::" + Prefs.ENABLE_LOG, this._setLog.bind(this));

        if (_control.get_state() == Gvc.MixerControlState.READY) {
            this._onControlStateChanged(_control);
        }
        else {
            this._controlStateChangeSignal = this._signalManager.addSignal(_control, "state-changed", this._onControlStateChanged.bind(this));
        }
    }

    _getMixerControl() { return getMixerControl(); }

    _setLog() { Lib.setLog(this._settings.get_boolean(Prefs.ENABLE_LOG)); }

    _onControlStateChanged(control) {
        if (control.get_state() == Gvc.MixerControlState.READY) {
            if (!this._initialised) {
                this._initialised = true;

                this._signalManager.addSignal(control, this.deviceType + "-added", this._deviceAdded.bind(this));
                this._signalManager.addSignal(control, this.deviceType + "-removed", this._deviceRemoved.bind(this));
                this._signalManager.addSignal(control, "active-" + this.deviceType + "-update", this._deviceActivated.bind(this));

                this._signalManager.addSignal(this._settings, "changed::" + Prefs.HIDE_ON_SINGLE_DEVICE, this._setChooserVisibility.bind(this));
                this._signalManager.addSignal(this._settings, "changed::" + Prefs.SHOW_PROFILES, this._setProfileVisibility.bind(this));
                this._signalManager.addSignal(this._settings, "changed::" + Prefs.ICON_THEME, this._setIcons.bind(this));
                this._signalManager.addSignal(this._settings, "changed::" + Prefs.HIDE_MENU_ICONS, this._setIcons.bind(this));
                this._signalManager.addSignal(this._settings, "changed::" + Prefs.PORT_SETTINGS, this._resetDevices.bind(this));

                this._show_device_signal = Prefs["SHOW_" + this.deviceType.toUpperCase() + "_DEVICES"];

                this._signalManager.addSignal(this._settings, "changed::" + this._show_device_signal, this._setVisibility.bind(this));

                this._portsSettings = Prefs.getPortsFromSettings(this._settings);

                /**
                 * There is no direct way to get all the UI devices from
                 * mixercontrol. When enabled after shell loads, the signals
                 * will not be emitted, a simple hack to look for ids, until any
                 * uidevice is not found. The UI devices are always serialed
                 * from from 1 to n
                 */

                let id = 0;

                let dummyDevice = new Gvc.MixerUIDevice();
                let maxId = dummyDevice.get_id();

                _d("Max Id:" + maxId);

                while (++id < maxId) {
                    this._deviceAdded(control, id);
                }
                let defaultStream = this.getDefaultStream(control);
                if (defaultStream) {
                    let defaultDevice = control.lookup_device_from_stream(defaultStream);
                    if (defaultDevice) {
                        this._deviceActivated(control, defaultDevice.get_id());
                    }
                }

                let profileVisibility = this._settings.get_boolean(Prefs.SHOW_PROFILES);
                this._setProfileTimer(profileVisibility);

                if (this._controlStateChangeSignal) {
                    this._controlStateChangeSignal.disconnect();
                    delete this._controlStateChangeSignal;
                }
                this._setVisibility();
            }
        }
    }

    _setProfileTimer(v) {
        //We dont have any way to understand that the profile has changed in the settings
        //Just an useless workaround 
        if (v) {
            if (!this.activeProfileTimeout) {
                this.activeProfileTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 2000,
                    this._setActiveProfile.bind(this));
            }
        }
        else {
            if (this.activeProfileTimeout) {
                GLib.source_remove(this.activeProfileTimeout);
                this.activeProfileTimeout = null;
            }
        }
    }


    _deviceAdded(control, id, dontcheck) {
        let obj = this._devices.get(id);
        let uidevice = this.lookupDeviceById(control, id);

        _d("Added - " + id);

        if (!obj) {
            if (_isDeviceInValid(uidevice)) {
                return null;
            }

            let title = uidevice.description;
            if (uidevice.origin != "")
                title += " - " + uidevice.origin;

            let icon = uidevice.get_icon_name();
            if (icon == null || icon.trim() == "")
                icon = this.getDefaultIcon();
            icon = this._getIcon(icon);

            obj = new SoundDeviceMenuItem(id, title, icon, Lib.getProfiles(control, uidevice), this._changeDeviceBase.bind(this), this._profileChangeCallback.bind(this));

            this.menuItem.menu.addMenuItem(obj);
            obj.profilesitems.forEach(i => this.menuItem.menu.addMenuItem(i));

            this._devices.set(id, obj);
        }
        else if (obj.isAvailable()) {
            return //uidevice;
        }
        else {
            obj.setAvailable(true);
        }

        _d("Device Name:" + obj.title);

        _d("Added: " + id + ":" + uidevice.description + ":" + uidevice.port_name + ":" + uidevice.origin);

        let stream = control.get_stream_from_device(uidevice);
        if (stream) {
            obj.setActiveProfile(uidevice.get_active_profile());
        }

        if (!dontcheck && !this._canShowDevice(control, uidevice, uidevice.port_available)) {
            this._deviceRemoved(control, id, true);
        }

        this._setChooserVisibility();
        this._setVisibility();
        return //uidevice;
    }

    _profileChangeCallback(id, profileName) {
        let control = this._getMixerControl();
        let uidevice = this.lookupDeviceById(control, id);
        if (!uidevice) {
            this._deviceRemoved(control, id);
        }
        else {
            _d("i am setting profile, " + profileName + ":" + uidevice.description + ":" + uidevice.port_name);
            if (id != this._activeDeviceId) {
                _d("Changing active device to " + uidevice.description + ":" + uidevice.port_name);
                this._changeDeviceBase(id, control);
            }
            control.change_profile_on_selected_device(uidevice, profileName);
            this._setDeviceActiveProfile(control, this._devices.get(id));
        }
    }

    _deviceRemoved(control, id, dontcheck) {
        let obj = this._devices.get(id);
        //let uidevice = this.lookupDeviceById(control,id);
        if (obj && obj.isAvailable()) {
            _d("Removed: " + id + ":" + obj.title);
            /*
            if (!dontcheck && this._canShowDevice(control, uidevice, false)) {
                _d('Device removed, but not hiding as its set to be shown always');
                return;
            }*/
            obj.setVisibility(false);
            obj.setAvailable(false);

            /*
            if (this.deviceRemovedTimout) {
                GLib.source_remove(this.deviceRemovedTimout);
                this.deviceRemovedTimout = null;
            }
            */
            /**
             * If the active uidevice is removed, then need to activate the
             * first available uidevice. However for some cases like Headphones,
             * when the uidevice is removed, Speakers are automatically
             * activated. So, lets wait for sometime before activating.
             */
            /* THIS MAY NOT BE NEEDED AS SHELL SEEMS TO ACTIVATE NEXT DEVICE
           this.deviceRemovedTimout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 1500, function() {
               _d("Device Removed timeout");
               if (obj === this._activeDevice) {
                   let device = Object.keys(this._devices).map((id) => this._devices[id]).find(({active}) => active === true);
                   if(device){
                       this._changeDeviceBase(this._getMixerControl(), device.id);
                   }                    
               }
               this.deviceRemovedTimout = null;
               return false;
           }.bind(this));
           */
            this._setChooserVisibility();
            this._setVisibility();
        }
    }

    _deviceActivated(control, id) {
        _d("Activated:- " + id);
        let obj = this._devices.get(id);
        if (!obj) {
            _d("Activated device not found in the list of devices, try to add");
            this._deviceAdded(control, id);
            obj = this._devices.get(id);
        }
        if (obj && id != this._activeDeviceId) {
            _d("Activated: " + id + ":" + obj.title);
            if (this._activeDeviceId) {
                this._devices.get(this._activeDeviceId).setActiveDevice(false);
            }
            this._activeDeviceId = id;
            obj.setActiveDevice(true);

            this.menuItem.label.text = obj.title;

            if (!this._settings.get_boolean(Prefs.HIDE_MENU_ICONS)) {
                this.menuItem.icon.icon_name = obj.icon_name;
            } else {
                this.menuItem.icon.gicon = null;
            }
        }
    }

    _changeDeviceBase(id, control) {
        if (!control) {
            control = this._getMixerControl();
        }
        let uidevice = this.lookupDeviceById(control, id);
        if (uidevice) {
            this.changeDevice(control, uidevice);
        }
        else {
            this._deviceRemoved(control, id);
        }
    }

    _setActiveProfile() {
        if (!this.menuItem._getOpenState()) {
            return;
        }
        let control = this._getMixerControl();
        //_d("Setting Active Profile");
        this._devices.forEach(device => {
            if (device.isAvailable()) {
                this._setDeviceActiveProfile(control, device);
            }
        });
        return true;
    }

    _setDeviceActiveProfile(control, device) {
        if (!device || !device.isAvailable()) {
            return;
        }

        let uidevice = this.lookupDeviceById(control, device.id);
        if (!uidevice) {
            this._deviceRemoved(control, device.id);
        }
        else {
            let activeProfile = uidevice.get_active_profile();
            _d("Active Profile:" + activeProfile);
            device.setActiveProfile(activeProfile);
        }
    }

    _getDeviceVisibility() {
        let hideChooser = this._settings.get_boolean(Prefs.HIDE_ON_SINGLE_DEVICE);
        if (hideChooser) {
            return (Array.from(this._devices.values()).filter(x => x.isAvailable()).length > 1);
        }
        else {
            return true;
        }
    }

    _setChooserVisibility() {
        let visibility = this._getDeviceVisibility();
        Array.from(this._devices.values()).filter(x => x.isAvailable()).forEach(x => x.setVisibility(visibility))

        this.menuItem._triangleBin.visible = visibility;
        this._setProfileVisibility();
    }

    _setProfileVisibility() {
        let visibility = this._settings.get_boolean(Prefs.SHOW_PROFILES);
        Array.from(this._devices.values()).filter(x => x.isAvailable()).forEach(device => device.setProfileVisibility(visibility));
        this._setProfileTimer(visibility);
    }

    _getIcon(name) {
        let iconsType = this._settings.get_string(Prefs.ICON_THEME);
        switch (iconsType) {
            case Prefs.ICON_THEME_COLORED:
                return name;
            case Prefs.ICON_THEME_MONOCHROME:
                return name + "-symbolic";
            default:
                //return "none";
                return null;
        }
    }

    _setIcons() {
        // Set the icons in the selection list
        let control = this._getMixerControl();
        this._devices.forEach((device, id) => {
            let uidevice = this.lookupDeviceById(control, id);
            if (uidevice) {
                let icon = uidevice.get_icon_name();
                if (icon == null || icon.trim() == "")
                    icon = this.getDefaultIcon();
                _d(icon + " _setIcons")
                device.setIcon(this._getIcon(icon));
            }
        });

        // These indicate the active device, which is displayed directly in the
        // Gnome menu, not in the list.
        if (!this._settings.get_boolean(Prefs.HIDE_MENU_ICONS)) {
            this.menuItem.icon.icon_name = this._getIcon(this._devices.get(this._activeDeviceId).icon_name);
        } else {
            this.menuItem.icon.icon_name = null;
        }
    }

    _canShowDevice(control, uidevice, defaultValue) {
        if (!uidevice || !this._portsSettings || uidevice.port_name == null || uidevice.description == null) {
            return defaultValue;
        }
        let stream = control.get_stream_from_device(uidevice);
        let cardName = null;
        if (stream) {
            let cardId = stream.get_card_index();
            if (cardId != null) {
                _d("Card Index" + cardId);
                let _card = Lib.getCard(cardId);
                if (_card) {
                    cardName = _card.name;
                }
                else {
                    //card id found, but not available in list
                    return false;
                }
                _d("Card Name" + cardName);
            }
        }

        for (let port of this._portsSettings) {
            //_d("P" + port.name + "==" + uidevice.port_name + "==" + port.human_name + "==" + uidevice.description + "==" + cardName + "==" + port.card_name)
            if (port && port.name == uidevice.port_name && port.human_name == uidevice.description && (!cardName || cardName == port.card_name)) {
                switch (port.display_option) {
                    case 1:
                        return true;
                    case 2:
                        return false;
                    default:
                        return defaultValue;
                }
            }
        }
        return defaultValue;
    }

    _resetDevices() {
        //this._portsSettings = JSON.parse(this._settings.get_string(Prefs.PORT_SETTINGS));
        this._portsSettings = Prefs.getPortsFromSettings(this._settings);
        let control = this._getMixerControl();
        for (let id of this._devices.keys()) {
            let uidevice = this.lookupDeviceById(control, id);
            if (_isDeviceInValid(uidevice)) {
                _d("Device is invalid");
            }
            else {
                switch (this._canShowDevice(control, uidevice, uidevice.port_available)) {
                    case true:
                        this._deviceAdded(control, id, true);
                        break;
                    case false:
                        this._deviceRemoved(control, id, true);
                        break;
                }
            }
        }
    }

    _setVisibility() {
        if (!this._settings.get_boolean(this._show_device_signal))
            this.menuItem.actor.visible = false;
        else
            // if setting says to show device, check for any device, otherwise
            // hide the "actor"
            this.menuItem.actor.visible = (Array.from(this._devices.values()).some(x => x.isAvailable()));
    }

    destroy() {
        this._signalManager.disconnectAll();
        if (this.deviceRemovedTimout) {
            GLib.source_remove(this.deviceRemovedTimout);
            this.deviceRemovedTimout = null;
        }
        if (this.activeProfileTimeout) {
            GLib.source_remove(this.activeProfileTimeout);
            this.activeProfileTimeout = null;
        }
        this.menuItem.destroy();
    }
};