// Copyright 2024-2025, University of Colorado Boulder
/**
 * This doc assumes you have read the doc in GroupSelectModel. Read that first as it explains the "group select
 * interaction" more generally.
 *
 * The view of the "Group Sort Interaction." This type handles adding the controller for selecting and grabbing
 * in the interaction for (keyboard). It also handles the individual and group focus highlights.
 *
 * This class can be used per scene, but the model is best used per screen.
 *
 * @author Michael Kauzmann (PhET Interactive Simulations)
 * @author Marla Schulz (PhET Interactive Simulations)
 */
import { string_scenery_phet_a11y_groupSort_navigable_StringProperty } from '../../../strings/a11y/groupSort/navigable.js';
import { string_scenery_phet_a11y_groupSort_sortable_StringProperty } from '../../../strings/a11y/groupSort/sortable.js';
import '../../../../../globals.js';
import _ from 'lodash';
import { assert } from '../../../../../scenerystack/assert.js';
import Disposable from '../../../../../axon/js/Disposable.js';
import Emitter from '../../../../../axon/js/Emitter.js';
import Multilink from '../../../../../axon/js/Multilink.js';
import Shape from '../../../../../kite/js/Shape.js';
import optionize, { combineOptions } from '../../../../../phet-core/js/optionize.js';
import HighlightFromNode from '../../../../../scenery/js/accessibility/HighlightFromNode.js';
import HighlightPath from '../../../../../scenery/js/accessibility/HighlightPath.js';
import { isInteractiveHighlighting } from '../../../../../scenery/js/accessibility/voicing/isInteractiveHighlighting.js';
import animatedPanZoomSingleton from '../../../../../scenery/js/listeners/animatedPanZoomSingleton.js';
import KeyboardListener from '../../../../../scenery/js/listeners/KeyboardListener.js';
import sceneryPhet from '../../../sceneryPhet.js';
import GrabReleaseCueNode from '../../nodes/GrabReleaseCueNode.js';
function GROUP_SELECT_ACCESSIBLE_NAME_BEHAVIOR(node, options, accessibleName) {
    options.ariaLabel = accessibleName; // IMPORTANT! Divs with innerContent aren't recognized with accessibleNames
    options.innerContent = accessibleName;
    return options;
}
const navigableStringProperty = string_scenery_phet_a11y_groupSort_navigable_StringProperty;
const sortableStringProperty = string_scenery_phet_a11y_groupSort_sortable_StringProperty;
class GroupSelectView extends Disposable {
    constructor(model, primaryFocusedNode, // Client is responsible for setting accessibleName and nothing else!
    providedOptions) {
        const options = optionize()({
            onGrab: _.noop,
            onRelease: _.noop,
            getHighlightNodeFromModelItem: providedOptions.getNodeFromModelItem,
            // By default, a group item is enabled it if corresponding Node is enabled.
            isGroupItemEnabled: groupItem => {
                const itemNode = providedOptions.getNodeFromModelItem(groupItem);
                assert && assert(itemNode, 'should have a node for the group item');
                return itemNode.enabled;
            },
            primaryFocusedNodeOptions: {
                tagName: 'div',
                focusable: true,
                ariaRole: 'application',
                accessibleNameBehavior: GROUP_SELECT_ACCESSIBLE_NAME_BEHAVIOR
            },
            grabReleaseCueOptions: {}
        }, providedOptions);
        super(options);
        this.model = model;
        this.primaryFocusedNode = primaryFocusedNode;
        // Emitted when the sorting cue should be repositioned. Most likely because the selection has changed.
        this.positionSortCueNodeEmitter = new Emitter();
        this.getNodeFromModelItem = options.getNodeFromModelItem;
        this.isGroupItemEnabled = options.isGroupItemEnabled;
        const selectedGroupItemProperty = this.model.selectedGroupItemProperty;
        const isKeyboardFocusedProperty = this.model.isKeyboardFocusedProperty;
        const isGroupItemKeyboardGrabbedProperty = this.model.isGroupItemKeyboardGrabbedProperty;
        const hasKeyboardGrabbedGroupItemProperty = this.model.hasKeyboardGrabbedGroupItemProperty;
        // Provide the general accessible content for the provided Node
        primaryFocusedNode.mutate(options.primaryFocusedNodeOptions);
        const grabbedPropertyListener = (grabbed) => {
            const selectedGroupItem = selectedGroupItemProperty.value;
            if (selectedGroupItem !== null) {
                if (grabbed) {
                    options.onGrab(selectedGroupItem);
                }
                else {
                    options.onRelease(selectedGroupItem);
                }
            }
        };
        isGroupItemKeyboardGrabbedProperty.lazyLink(grabbedPropertyListener);
        const focusListener = {
            focus: () => {
                // It's possible that getGroupItemToSelect's heuristic said that there is nothing to focus here
                if (selectedGroupItemProperty.value === null) {
                    selectedGroupItemProperty.value = options.getGroupItemToSelect();
                }
                isKeyboardFocusedProperty.value = true;
                // When the group receives keyboard focus, make sure that the selected group item is displayed
                if (selectedGroupItemProperty.value !== null) {
                    const node = options.getNodeFromModelItem(selectedGroupItemProperty.value);
                    node && animatedPanZoomSingleton.listener.panToNode(node, true);
                }
            },
            blur: () => {
                isKeyboardFocusedProperty.value = false;
                isGroupItemKeyboardGrabbedProperty.value = false;
            },
            over: () => {
                // When you mouse over while focused, the highlights are hidden, and so update the state (even though we are
                // still technically keyboard focused). This will assist in showing the mouse cue, https://github.com/phetsims/center-and-variability/issues/406
                isKeyboardFocusedProperty.value = false;
            },
            down: () => {
                // We want to remove focus from this node entirely to prevent the focus highlight from showing up when
                // there is no selected group item.
                primaryFocusedNode.blur();
            }
        };
        // When interactive highlights become active on the group, interaction with a mouse has begun while using
        // Interactive Highlighting. When that happens, clear the selection to prevent focus highlight flickering/thrashing.
        // See https://github.com/phetsims/center-and-variability/issues/557 and https://github.com/phetsims/scenery-phet/issues/815
        if (isInteractiveHighlighting(primaryFocusedNode)) {
            const interactiveHighlightingActiveListener = (active) => {
                if (active) {
                    if (model.selectedGroupItemProperty.value !== null) {
                        // Release the selection if grabbed
                        model.isGroupItemKeyboardGrabbedProperty.value = false;
                        // Clear the selection so that there isn't potential for flickering in between input modalities
                        model.selectedGroupItemProperty.value = null;
                    }
                    // This controls the visibility of interaction cues (keyboard vs mouse), so we need to clear it when
                    // switching interaction modes.
                    isKeyboardFocusedProperty.value = false;
                }
            };
            primaryFocusedNode.isInteractiveHighlightActiveProperty.lazyLink(interactiveHighlightingActiveListener);
            this.disposeEmitter.addListener(() => {
                primaryFocusedNode.isInteractiveHighlightActiveProperty.unlink(interactiveHighlightingActiveListener);
            });
        }
        const updateFocusHighlight = new Multilink([
            selectedGroupItemProperty,
            isGroupItemKeyboardGrabbedProperty
        ], (selectedGroupItem, isGroupItemGrabbed) => {
            let focusHighlightSet = false;
            if (selectedGroupItem !== null) {
                const node = options.getHighlightNodeFromModelItem(selectedGroupItem);
                if (node) {
                    const focusForSelectedGroupItem = new HighlightFromNode(node, { dashed: isGroupItemGrabbed });
                    // If available, set to the focused selection for this scene.
                    primaryFocusedNode.setFocusHighlight(focusForSelectedGroupItem);
                    focusHighlightSet = true;
                }
            }
            // If not set above, then actively hide it.
            !focusHighlightSet && primaryFocusedNode.setFocusHighlight('invisible');
            if (selectedGroupItem !== null) {
                this.positionSortCueNodeEmitter.emit();
            }
        });
        // "release" into selecting state when disabled
        const enabledListener = (enabled) => {
            if (!enabled) {
                hasKeyboardGrabbedGroupItemProperty.value = false;
            }
        };
        this.model.enabledProperty.link(enabledListener);
        this.disposeEmitter.addListener(() => {
            this.model.enabledProperty.unlink(enabledListener);
        });
        // A KeyboardListener that changes the "sorting" vs "selecting" state of the interaction.
        const grabReleaseKeyboardListener = new KeyboardListener({
            fireOnDown: false,
            keys: ['enter', 'space', 'escape'],
            // This option will ensure that this listener doesn't disrupt other keys
            overlapBehavior: 'allow',
            fire: (event, keysPressed) => {
                // Do no grab when the interaction is disabled, if there is no selection, or when the individual group item is disabled
                if (this.model.enabled && selectedGroupItemProperty.value !== null && this.isGroupItemEnabled(selectedGroupItemProperty.value)) {
                    // Do the "Grab/release" action to switch to sorting or selecting
                    if (keysPressed === 'enter' || keysPressed === 'space') {
                        isGroupItemKeyboardGrabbedProperty.toggle();
                        hasKeyboardGrabbedGroupItemProperty.value = true;
                    }
                    else if (isGroupItemKeyboardGrabbedProperty.value && keysPressed === 'escape') {
                        isGroupItemKeyboardGrabbedProperty.value = false;
                    }
                    // Reset to true from keyboard input, in case mouse/touch input set to false during the keyboard interaction.
                    isKeyboardFocusedProperty.value = true;
                }
            }
        });
        const defaultGroupShape = primaryFocusedNode.visibleBounds.isFinite() ? Shape.bounds(primaryFocusedNode.visibleBounds) : null;
        // Set the outer group focus highlight to surround the entire area where group items are located.
        this.groupSortGroupFocusHighlightPath = new HighlightPath(defaultGroupShape, {
            outerStroke: HighlightPath.OUTER_LIGHT_GROUP_FOCUS_COLOR,
            innerStroke: HighlightPath.INNER_LIGHT_GROUP_FOCUS_COLOR,
            outerLineWidth: HighlightPath.GROUP_OUTER_LINE_WIDTH,
            innerLineWidth: HighlightPath.GROUP_INNER_LINE_WIDTH
        });
        Multilink.multilink([
            model.isGroupItemKeyboardGrabbedProperty
        ], isGrabbed => {
            primaryFocusedNode.accessibleRoleDescription = isGrabbed ? sortableStringProperty : navigableStringProperty;
        });
        this.grabReleaseCueNode = new GrabReleaseCueNode(combineOptions({
            visibleProperty: this.model.grabReleaseCueVisibleProperty
        }, options.grabReleaseCueOptions));
        this.groupSortGroupFocusHighlightPath.addChild(this.grabReleaseCueNode);
        primaryFocusedNode.setGroupFocusHighlight(this.groupSortGroupFocusHighlightPath);
        primaryFocusedNode.addInputListener(focusListener);
        primaryFocusedNode.addInputListener(grabReleaseKeyboardListener);
        this.disposeEmitter.addListener(() => {
            isGroupItemKeyboardGrabbedProperty.unlink(grabbedPropertyListener);
            primaryFocusedNode.setGroupFocusHighlight(false);
            primaryFocusedNode.setFocusHighlight(null);
            primaryFocusedNode.removeInputListener(grabReleaseKeyboardListener);
            primaryFocusedNode.removeInputListener(focusListener);
            updateFocusHighlight.dispose();
            grabReleaseKeyboardListener.dispose();
        });
    }
    // By "change" we mean sort or selection.
    onGroupItemChange(newGroupItem) {
        // When using keyboard input, make sure that the selected group item is still displayed by panning to keep it
        // in view. `panToCenter` is false because centering the group item in the screen is too much movement.
        const node = this.getNodeFromModelItem(newGroupItem);
        node && animatedPanZoomSingleton.listener.panToNode(node, false);
        // Reset to true from keyboard input, in case mouse/touch input set to false during the keyboard interaction.
        this.model.isKeyboardFocusedProperty.value = true;
    }
    /**
     * Programmatic way to activate the group select interaction, set its selection, and grab that selection.
     */
    keyboardGrab(groupItem) {
        if (this.model.enabled && this.isGroupItemEnabled(groupItem)) {
            // focus before anything else
            this.primaryFocusedNode.focus();
            // Selecting before grabbing ensures that the item is correct as it pertains to Property listeners.
            this.model.selectedGroupItemProperty.value = groupItem;
            this.model.hasKeyboardGrabbedGroupItemProperty.value = true;
            this.model.isGroupItemKeyboardGrabbedProperty.value = true;
            this.model.isKeyboardFocusedProperty.value = true;
        }
    }
    dispose() {
        this.groupSortGroupFocusHighlightPath.dispose();
        this.grabReleaseCueNode.dispose();
        this.positionSortCueNodeEmitter.dispose();
        super.dispose();
    }
}
sceneryPhet.register('GroupSelectView', GroupSelectView);
export default GroupSelectView;
