import { addBreadcrumb, debug, dropUndefinedKeys, getClient, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core';
import * as React from 'react';
import { StyleSheet, View } from 'react-native';
import { createIntegration } from './integrations/factory';
import { DEFAULT_RAGE_TAP_THRESHOLD, DEFAULT_RAGE_TAP_TIME_WINDOW, RageTapDetector } from './ragetap';
import { startUserInteractionSpan } from './tracing/integrations/userInteraction';
import { UI_ACTION_TOUCH } from './tracing/ops';
import { SPAN_ORIGIN_AUTO_INTERACTION } from './tracing/origin';
const touchEventStyles = StyleSheet.create({
    wrapperView: {
        flex: 1,
    },
});
const DEFAULT_BREADCRUMB_CATEGORY = 'touch';
const DEFAULT_BREADCRUMB_TYPE = 'user';
const DEFAULT_MAX_COMPONENT_TREE_SIZE = 20;
const SENTRY_LABEL_PROP_KEY = 'sentry-label';
const SENTRY_SPAN_ATTRIBUTES_PROP_KEY = 'sentry-span-attributes';
const SENTRY_COMPONENT_PROP_KEY = 'data-sentry-component';
const SENTRY_ELEMENT_PROP_KEY = 'data-sentry-element';
const SENTRY_FILE_PROP_KEY = 'data-sentry-source-file';
const ACCESSIBILITY_LABEL_PROP_KEY = 'accessibilityLabel';
const ARIA_LABEL_PROP_KEY = 'aria-label';
const TEST_ID_PROP_KEY = 'testID';
/**
 * Boundary to log breadcrumbs for interaction events.
 */
class TouchEventBoundary extends React.Component {
    constructor(props) {
        super(props);
        this.name = 'TouchEventBoundary';
        this._rageTapDetector = new RageTapDetector({
            enabled: props.enableRageTapDetection,
            threshold: props.rageTapThreshold,
            timeWindow: props.rageTapTimeWindow,
        });
    }
    /**
     * Registers the TouchEventBoundary as a Sentry Integration.
     */
    componentDidMount() {
        var _a;
        const client = getClient();
        (_a = client === null || client === void 0 ? void 0 : client.addIntegration) === null || _a === void 0 ? void 0 : _a.call(client, createIntegration(this.name));
    }
    /**
     * Sync rage tap options when props change.
     */
    componentDidUpdate() {
        this._rageTapDetector.updateOptions({
            enabled: this.props.enableRageTapDetection,
            threshold: this.props.rageTapThreshold,
            timeWindow: this.props.rageTapTimeWindow,
        });
    }
    /**
     *
     */
    render() {
        return (React.createElement(View, { style: touchEventStyles.wrapperView, 
            // oxlint-disable-next-line typescript-eslint(no-explicit-any)
            onTouchStart: this._onTouchStart.bind(this) }, this.props.children));
    }
    /**
     * Logs the touch event given the component tree names and a label.
     */
    _logTouchEvent(touchPath, label) {
        const level = 'info';
        const root = touchPath[0];
        if (!root) {
            debug.warn('[TouchEvents] No root component found in touch path.');
            return;
        }
        const detail = label ? label : `${root.name}${root.file ? ` (${root.file})` : ''}`;
        const crumb = {
            category: this.props.breadcrumbCategory,
            data: { path: touchPath },
            level: level,
            message: `Touch event within element: ${detail}`,
            type: this.props.breadcrumbType,
        };
        addBreadcrumb(crumb);
        debug.log(`[TouchEvents] ${crumb.message}`);
    }
    /**
     * Checks if the name is supposed to be ignored.
     */
    _isNameIgnored(name) {
        let ignoreNames = this.props.ignoreNames || [];
        if (this.props.ignoredDisplayNames) {
            // This is to make it compatible with prior version.
            ignoreNames = [...ignoreNames, ...this.props.ignoredDisplayNames];
        }
        return ignoreNames.some((ignoreName) => (typeof ignoreName === 'string' && name === ignoreName) ||
            (ignoreName instanceof RegExp && name.match(ignoreName)));
    }
    // Originally was going to clean the names of any HOCs as well but decided that it might hinder debugging effectively. Will leave here in case
    // private readonly _cleanName = (name: string): string =>
    //   name.replace(/.*\(/g, "").replace(/\)/g, "");
    /**
     * Traverses through the component tree when a touch happens and logs it.
     * @param e
     */
    _onTouchStart(e) {
        var _a, _b, _c;
        if (!e._targetInst) {
            return;
        }
        let currentInst = e._targetInst;
        const touchPath = [];
        while (currentInst &&
            // maxComponentTreeSize will always be defined as we have a defaultProps. But ts needs a check so this is here.
            this.props.maxComponentTreeSize &&
            touchPath.length < this.props.maxComponentTreeSize) {
            if (
            // If the loop gets to the boundary itself, break.
            ((_a = currentInst.elementType) === null || _a === void 0 ? void 0 : _a.displayName) === TouchEventBoundary.displayName) {
                break;
            }
            const info = getTouchedComponentInfo(currentInst, this.props.labelName);
            this._pushIfNotIgnored(touchPath, info);
            currentInst = currentInst.return;
        }
        const label = (_b = touchPath.find(info => info.label)) === null || _b === void 0 ? void 0 : _b.label;
        if (touchPath.length > 0) {
            this._logTouchEvent(touchPath, label);
            this._rageTapDetector.check(touchPath, label);
        }
        const span = startUserInteractionSpan({
            elementId: label,
            op: UI_ACTION_TOUCH,
        });
        if (span) {
            span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_ORIGIN_AUTO_INTERACTION);
            // Apply custom attributes from sentry-span-attributes prop
            // Traverse the component tree to find custom attributes
            let instForAttributes = e._targetInst;
            let customAttributes;
            while (instForAttributes) {
                if (((_c = instForAttributes.elementType) === null || _c === void 0 ? void 0 : _c.displayName) === TouchEventBoundary.displayName) {
                    break;
                }
                customAttributes = getSpanAttributes(instForAttributes);
                if (customAttributes && Object.keys(customAttributes).length > 0) {
                    break;
                }
                instForAttributes = instForAttributes.return;
            }
            if (customAttributes && Object.keys(customAttributes).length > 0) {
                span.setAttributes(customAttributes);
            }
        }
    }
    /**
     * Pushes the name to the componentTreeNames array if it is not ignored.
     */
    _pushIfNotIgnored(touchPath, value) {
        if (!value) {
            return false;
        }
        if (!value.name && !value.label) {
            return false;
        }
        if (value.name && this._isNameIgnored(value.name)) {
            return false;
        }
        if (value.label && this._isNameIgnored(value.label)) {
            return false;
        }
        // Deduplicate same subsequent items.
        if (touchPath.length > 0 && JSON.stringify(touchPath[touchPath.length - 1]) === JSON.stringify(value)) {
            return false;
        }
        touchPath.push(value);
        return true;
    }
}
TouchEventBoundary.displayName = '__Sentry.TouchEventBoundary';
TouchEventBoundary.defaultProps = {
    breadcrumbCategory: DEFAULT_BREADCRUMB_CATEGORY,
    breadcrumbType: DEFAULT_BREADCRUMB_TYPE,
    ignoreNames: [],
    maxComponentTreeSize: DEFAULT_MAX_COMPONENT_TREE_SIZE,
    enableRageTapDetection: true,
    rageTapThreshold: DEFAULT_RAGE_TAP_THRESHOLD,
    rageTapTimeWindow: DEFAULT_RAGE_TAP_TIME_WINDOW,
};
function getTouchedComponentInfo(currentInst, labelKey) {
    var _a;
    const displayName = (_a = currentInst.elementType) === null || _a === void 0 ? void 0 : _a.displayName;
    const props = currentInst.memoizedProps;
    if (!props) {
        // Early return if no props are available, as we can't extract any useful information
        if (displayName) {
            return {
                name: displayName,
            };
        }
        return undefined;
    }
    return dropUndefinedKeys({
        // provided by @sentry/babel-plugin-component-annotate
        name: getComponentName(props) || displayName,
        element: getElementName(props),
        file: getFileName(props),
        // `sentry-label` or user defined label key
        label: getLabelValue(props, labelKey),
    });
}
function getComponentName(props) {
    return ((typeof props[SENTRY_COMPONENT_PROP_KEY] === 'string' &&
        props[SENTRY_COMPONENT_PROP_KEY].length > 0 &&
        props[SENTRY_COMPONENT_PROP_KEY] !== 'unknown' &&
        props[SENTRY_COMPONENT_PROP_KEY]) ||
        undefined);
}
function getElementName(props) {
    return ((typeof props[SENTRY_ELEMENT_PROP_KEY] === 'string' &&
        props[SENTRY_ELEMENT_PROP_KEY].length > 0 &&
        props[SENTRY_ELEMENT_PROP_KEY] !== 'unknown' &&
        props[SENTRY_ELEMENT_PROP_KEY]) ||
        undefined);
}
function getFileName(props) {
    return ((typeof props[SENTRY_FILE_PROP_KEY] === 'string' &&
        props[SENTRY_FILE_PROP_KEY].length > 0 &&
        props[SENTRY_FILE_PROP_KEY] !== 'unknown' &&
        props[SENTRY_FILE_PROP_KEY]) ||
        undefined);
}
function getLabelValue(props, labelKey) {
    if (typeof props[SENTRY_LABEL_PROP_KEY] === 'string' && props[SENTRY_LABEL_PROP_KEY].length > 0) {
        return props[SENTRY_LABEL_PROP_KEY];
    }
    // For some reason type narrowing doesn't work as expected with indexing when checking it all in one go in
    // the "check-label" if sentence, so we have to assign it to a variable here first
    // oxlint-disable-next-line typescript-eslint(no-unnecessary-type-assertion)
    if (typeof labelKey === 'string' && typeof props[labelKey] == 'string' && props[labelKey].length > 0) {
        // oxlint-disable-next-line typescript-eslint(no-unnecessary-type-assertion)
        return props[labelKey];
    }
    if (typeof props[ACCESSIBILITY_LABEL_PROP_KEY] === 'string' && props[ACCESSIBILITY_LABEL_PROP_KEY].length > 0) {
        return props[ACCESSIBILITY_LABEL_PROP_KEY];
    }
    if (typeof props[ARIA_LABEL_PROP_KEY] === 'string' && props[ARIA_LABEL_PROP_KEY].length > 0) {
        return props[ARIA_LABEL_PROP_KEY];
    }
    if (typeof props[TEST_ID_PROP_KEY] === 'string' && props[TEST_ID_PROP_KEY].length > 0) {
        return props[TEST_ID_PROP_KEY];
    }
    return undefined;
}
function getSpanAttributes(currentInst) {
    if (!currentInst.memoizedProps) {
        return undefined;
    }
    const props = currentInst.memoizedProps;
    const attributes = props[SENTRY_SPAN_ATTRIBUTES_PROP_KEY];
    // Validate that it's an object (not null, not array)
    if (typeof attributes === 'object' && attributes !== null && !Array.isArray(attributes)) {
        return attributes;
    }
    return undefined;
}
/**
 * Convenience Higher-Order-Component for TouchEventBoundary
 * @param WrappedComponent any React Component
 * @param boundaryProps TouchEventBoundaryProps
 */
const withTouchEventBoundary = (
// oxlint-disable-next-line typescript-eslint(no-explicit-any)
InnerComponent, boundaryProps) => {
    const WrappedComponent = props => (React.createElement(TouchEventBoundary, Object.assign({}, (boundaryProps !== null && boundaryProps !== void 0 ? boundaryProps : {})),
        React.createElement(InnerComponent, Object.assign({}, props))));
    WrappedComponent.displayName = 'WithTouchEventBoundary';
    return WrappedComponent;
};
export { TouchEventBoundary, withTouchEventBoundary };
//# sourceMappingURL=touchevents.js.map