import _ from 'lodash';
import '../../../../assert/js/assert.js';
import '../../../../globals.js';
import TinyEmitter from '../../../../axon/js/TinyEmitter.js';
import memoize from '../../../../phet-core/js/memoize.js';
import ResponsePatternCollection from '../../../../utterance-queue/js/ResponsePatternCollection.js';
import { DelayedMutate, Focus, Node, PDOMInstance, ReadingBlockHighlight, ReadingBlockUtterance, scenery, Voicing, voicingManager } from '../../imports.js';
const READING_BLOCK_OPTION_KEYS = [
    'readingBlockTagName',
    'readingBlockDisabledTagName',
    'readingBlockNameResponse',
    'readingBlockHintResponse',
    'readingBlockResponsePatternCollection',
    'readingBlockActiveHighlight'
];
// Use an assertion signature to narrow the type to ReadingBlockUtterance
function assertReadingBlockUtterance(utterance) {
    if (!(utterance instanceof ReadingBlockUtterance)) {
        assert && assert(false, 'utterance is not a ReadinBlockUtterance');
    }
}
// An implementation class for ReadingBlock.ts, only used in this class so that we know if we own the Utterance and can
// therefore dispose it.
class OwnedReadingBlockUtterance extends ReadingBlockUtterance {
    constructor(focus, providedOptions) {
        super(focus, providedOptions);
    }
}
const DEFAULT_CONTENT_HINT_PATTERN = new ResponsePatternCollection({
    nameHint: '{{NAME}}. {{HINT}}'
});
const ReadingBlock = memoize((Type) => {
    const ReadingBlockClass = DelayedMutate('ReadingBlock', READING_BLOCK_OPTION_KEYS, class ReadingBlockClass extends Voicing(Type) {
        constructor(...args) {
            super(...args);
            this._readingBlockTagName = 'button';
            this._readingBlockDisabledTagName = 'p';
            this._readingBlockActiveHighlight = null;
            this.readingBlockActiveHighlightChangedEmitter = new TinyEmitter();
            this.readingBlockResponsePatternCollection = DEFAULT_CONTENT_HINT_PATTERN;
            this._localBoundsChangedListener = this._onLocalBoundsChanged.bind(this);
            this.localBoundsProperty.link(this._localBoundsChangedListener);
            this._readingBlockInputListener = {
                focus: event => this._speakReadingBlockContentListener(event),
                up: event => this._speakReadingBlockContentListener(event),
                click: event => this._speakReadingBlockContentListener(event)
            };
            this._readingBlockFocusableChangeListener = this._onReadingBlockFocusableChanged.bind(this);
            voicingManager.speechAllowedAndFullyEnabledProperty.link(this._readingBlockFocusableChangeListener);
            // All ReadingBlocks have a ReadingBlockHighlight, a focus highlight that is black to indicate it has
            // a different behavior.
            this.focusHighlight = new ReadingBlockHighlight(this);
            // All ReadingBlocks use a ReadingBlockUtterance with Focus and Trail data to this Node so that it can be
            // highlighted in the FocusOverlay when this Utterance is being announced.
            this.voicingUtterance = new OwnedReadingBlockUtterance(null);
        }
        /**
         * Whether a Node composes ReadingBlock.
         */
        get _isReadingBlock() {
            return true;
        }
        /**
         * Set the tagName for the node composing ReadingBlock. This is the tagName (of ParallelDOM) that will be applied
         * to this Node when Reading Blocks are enabled.
         */
        setReadingBlockTagName(tagName) {
            this._readingBlockTagName = tagName;
            this._onReadingBlockFocusableChanged(voicingManager.speechAllowedAndFullyEnabledProperty.value);
        }
        set readingBlockTagName(tagName) { this.setReadingBlockTagName(tagName); }
        get readingBlockTagName() { return this.getReadingBlockTagName(); }
        /**
         * Get the tagName for this Node (of ParallelDOM) when Reading Blocks are enabled.
         */
        getReadingBlockTagName() {
            return this._readingBlockTagName;
        }
        /**
         * Sets the tagName for the node composing ReadingBlock. This is the tagName (of ParallelDOM) that will be applied
         * to this Node when Reading Blocks are disabled. If you do not want the ReadingBlock to appear at all, set to null.
         */
        setReadingBlockDisabledTagName(tagName) {
            this._readingBlockDisabledTagName = tagName;
            this._onReadingBlockFocusableChanged(voicingManager.speechAllowedAndFullyEnabledProperty.value);
        }
        set readingBlockDisabledTagName(tagName) { this.setReadingBlockDisabledTagName(tagName); }
        get readingBlockDisabledTagName() { return this.getReadingBlockDisabledTagName(); }
        getReadingBlockDisabledTagName() {
            return this._readingBlockDisabledTagName;
        }
        /**
         * Sets the content that should be read whenever the ReadingBlock receives input that initiates speech.
         */
        setReadingBlockNameResponse(content) {
            this._voicingResponsePacket.nameResponse = content;
        }
        set readingBlockNameResponse(content) { this.setReadingBlockNameResponse(content); }
        get readingBlockNameResponse() { return this.getReadingBlockNameResponse(); }
        /**
         * Gets the content that is spoken whenever the ReadingBLock receives input that would initiate speech.
         */
        getReadingBlockNameResponse() {
            return this._voicingResponsePacket.nameResponse;
        }
        /**
         * Sets the hint response for this ReadingBlock. This is only spoken if "Helpful Hints" are enabled by the user.
         */
        setReadingBlockHintResponse(content) {
            this._voicingResponsePacket.hintResponse = content;
        }
        set readingBlockHintResponse(content) { this.setReadingBlockHintResponse(content); }
        get readingBlockHintResponse() { return this.getReadingBlockHintResponse(); }
        /**
         * Get the hint response for this ReadingBlock. This is additional content that is only read if "Helpful Hints"
         * are enabled.
         */
        getReadingBlockHintResponse() {
            return this._voicingResponsePacket.hintResponse;
        }
        /**
         * Sets the collection of patterns to use for voicing responses, controlling the order, punctuation, and
         * additional content for each combination of response. See ResponsePatternCollection.js if you wish to use
         * a collection of string patterns that are not the default.
         */
        setReadingBlockResponsePatternCollection(patterns) {
            this._voicingResponsePacket.responsePatternCollection = patterns;
        }
        set readingBlockResponsePatternCollection(patterns) { this.setReadingBlockResponsePatternCollection(patterns); }
        get readingBlockResponsePatternCollection() { return this.getReadingBlockResponsePatternCollection(); }
        /**
         * Get the ResponsePatternCollection object that this ReadingBlock Node is using to collect responses.
         */
        getReadingBlockResponsePatternCollection() {
            return this._voicingResponsePacket.responsePatternCollection;
        }
        /**
         * ReadingBlock must take a ReadingBlockUtterance for its voicingUtterance. You generally shouldn't be using this.
         * But if you must, you are responsible for setting the ReadingBlockUtterance.readingBlockFocus when this
         * ReadingBlock is activated so that it gets highlighted correctly. See how the default readingBlockFocus is set.
         */
        setVoicingUtterance(utterance) {
            super.setVoicingUtterance(utterance);
        }
        set voicingUtterance(utterance) { super.voicingUtterance = utterance; }
        get voicingUtterance() { return this.getVoicingUtterance(); }
        getVoicingUtterance() {
            const utterance = super.getVoicingUtterance();
            assertReadingBlockUtterance(utterance);
            return utterance;
        }
        setVoicingNameResponse() { assert && assert(false, 'ReadingBlocks only support setting the name response via readingBlockNameResponse'); }
        getVoicingNameResponse() { assert && assert(false, 'ReadingBlocks only support getting the name response via readingBlockNameResponse'); }
        setVoicingObjectResponse() { assert && assert(false, 'ReadingBlocks do not support setting object response'); }
        getVoicingObjectResponse() { assert && assert(false, 'ReadingBlocks do not support setting object response'); }
        setVoicingContextResponse() { assert && assert(false, 'ReadingBlocks do not support setting context response'); }
        getVoicingContextResponse() { assert && assert(false, 'ReadingBlocks do not support setting context response'); }
        setVoicingHintResponse() { assert && assert(false, 'ReadingBlocks only support setting the hint response via readingBlockHintResponse.'); }
        getVoicingHintResponse() { assert && assert(false, 'ReadingBlocks only support getting the hint response via readingBlockHintResponse.'); }
        setVoicingResponsePatternCollection() { assert && assert(false, 'ReadingBlocks only support setting the response patterns via readingBlockResponsePatternCollection.'); }
        getVoicingResponsePatternCollection() { assert && assert(false, 'ReadingBlocks only support getting the response patterns via readingBlockResponsePatternCollection.'); }
        /**
         * Sets the highlight used to surround this Node while the Voicing framework is speaking this content.
         * If a Node is provided, do not add this Node to the scene graph, it is added and made visible by the HighlightOverlay.
         */
        setReadingBlockActiveHighlight(readingBlockActiveHighlight) {
            if (this._readingBlockActiveHighlight !== readingBlockActiveHighlight) {
                this._readingBlockActiveHighlight = readingBlockActiveHighlight;
                this.readingBlockActiveHighlightChangedEmitter.emit();
            }
        }
        set readingBlockActiveHighlight(readingBlockActiveHighlight) { this.setReadingBlockActiveHighlight(readingBlockActiveHighlight); }
        get readingBlockActiveHighlight() { return this._readingBlockActiveHighlight; }
        /**
         * Returns the highlight used to surround this Node when the Voicing framework is reading its
         * content.
         */
        getReadingBlockActiveHighlight() {
            return this._readingBlockActiveHighlight;
        }
        /**
         * Returns true if this ReadingBlock is "activated", indicating that it has received interaction
         * and the Voicing framework is speaking its content.
         */
        isReadingBlockActivated() {
            let activated = false;
            const trailIds = Object.keys(this.displays);
            for (let i = 0; i < trailIds.length; i++) {
                const pointerFocus = this.displays[trailIds[i]].focusManager.readingBlockFocusProperty.value;
                if (pointerFocus && pointerFocus.trail.lastNode() === this) {
                    activated = true;
                    break;
                }
            }
            return activated;
        }
        get readingBlockActivated() { return this.isReadingBlockActivated(); }
        /**
         * When this Node becomes focusable (because Reading Blocks have just been enabled or disabled), either
         * apply or remove the readingBlockTagName.
         *
         * @param focusable - whether ReadingBlocks should be focusable
         */
        _onReadingBlockFocusableChanged(focusable) {
            this.focusable = focusable;
            if (focusable) {
                this.tagName = this._readingBlockTagName;
                // don't add the input listener if we are already active, we may just be updating the tagName in this case
                if (!this.hasInputListener(this._readingBlockInputListener)) {
                    this.addInputListener(this._readingBlockInputListener);
                }
            }
            else {
                this.tagName = this._readingBlockDisabledTagName;
                if (this.hasInputListener(this._readingBlockInputListener)) {
                    this.removeInputListener(this._readingBlockInputListener);
                }
            }
        }
        /**
         * Update the hit areas for this Node whenever the bounds change.
         */
        _onLocalBoundsChanged(localBounds) {
            this.mouseArea = localBounds;
            this.touchArea = localBounds;
        }
        /**
         * Speak the content associated with the ReadingBlock. Sets the readingBlockFocusProperties on
         * the displays so that HighlightOverlays know to activate a highlight while the voicingManager
         * is reading about this Node.
         */
        _speakReadingBlockContentListener(event) {
            const displays = this.getConnectedDisplays();
            const readingBlockUtterance = this.voicingUtterance;
            const content = this.collectResponse({
                nameResponse: this.getReadingBlockNameResponse(),
                hintResponse: this.getReadingBlockHintResponse(),
                ignoreProperties: this.voicingIgnoreVoicingManagerProperties,
                responsePatternCollection: this._voicingResponsePacket.responsePatternCollection,
                utterance: readingBlockUtterance
            });
            if (content) {
                for (let i = 0; i < displays.length; i++) {
                    if (!this.getDescendantsUseHighlighting(event.trail)) {
                        // the SceneryEvent might have gone through a descendant of this Node
                        const rootToSelf = event.trail.subtrailTo(this);
                        // the trail to a Node may be discontinuous for PDOM events due to pdomOrder,
                        // this finds the actual visual trail to use
                        const visualTrail = PDOMInstance.guessVisualTrail(rootToSelf, displays[i].rootNode);
                        const focus = new Focus(displays[i], visualTrail);
                        readingBlockUtterance.readingBlockFocus = focus;
                        this.speakContent(content);
                    }
                }
            }
        }
        /**
         * If we created and own the voicingUtterance we can fully dispose of it.
         * @mixin-protected - made public for use in the mixin only
         */
        cleanVoicingUtterance() {
            if (this._voicingUtterance instanceof OwnedReadingBlockUtterance) {
                this._voicingUtterance.dispose();
            }
            super.cleanVoicingUtterance();
        }
        dispose() {
            voicingManager.speechAllowedAndFullyEnabledProperty.unlink(this._readingBlockFocusableChangeListener);
            this.localBoundsProperty.unlink(this._localBoundsChangedListener);
            // remove the input listener that activates the ReadingBlock, only do this if the listener is attached while
            // the ReadingBlock is enabled
            if (this.hasInputListener(this._readingBlockInputListener)) {
                this.removeInputListener(this._readingBlockInputListener);
            }
            super.dispose();
        }
        mutate(options) {
            return super.mutate(options);
        }
    });
    /**
     * {Array.<string>} - String keys for all the allowed options that will be set by Node.mutate( options ), in
     * the order they will be evaluated.
     *
     * NOTE: See Node's _mutatorKeys documentation for more information on how this operates, and potential special
     *       cases that may apply.
     */
    ReadingBlockClass.prototype._mutatorKeys = READING_BLOCK_OPTION_KEYS.concat(ReadingBlockClass.prototype._mutatorKeys);
    assert && assert(ReadingBlockClass.prototype._mutatorKeys.length === _.uniq(ReadingBlockClass.prototype._mutatorKeys).length, 'x mutator keys in ReadingBlock');
    return ReadingBlockClass;
});
export function isReadingBlock(something) {
    return something instanceof Node && something._isReadingBlock;
}
scenery.register('ReadingBlock', ReadingBlock);
export default ReadingBlock;
//# sourceMappingURL=ReadingBlock.js.map