import '../../../assert/js/assert.js';
import '../../../globals.js';
// Copyright 2013-2024, University of Colorado Boulder
/**
 * A Path draws a Shape with a specific type of fill and stroke. Mixes in Paintable.
 *
 * @author Jonathan Olson <jonathan.olson@colorado.edu>
 */
import TinyForwardingProperty from '../../../axon/js/TinyForwardingProperty.js';
import { isTReadOnlyProperty } from '../../../axon/js/TReadOnlyProperty.js';
import Bounds2 from '../../../dot/js/Bounds2.js';
import { Shape } from '../../../kite/js/imports.js';
import optionize, { combineOptions } from '../../../phet-core/js/optionize.js';
import { Node, Paint, Paintable, PAINTABLE_DRAWABLE_MARK_FLAGS, PAINTABLE_OPTION_KEYS, PathCanvasDrawable, PathSVGDrawable, Renderer, scenery } from '../imports.js';
const PATH_OPTION_KEYS = [
    'boundsMethod',
    'shape',
    'shapeProperty'
];
const DEFAULT_OPTIONS = {
    shape: null,
    boundsMethod: 'accurate'
};
class Path extends Paintable(Node) {
    /**
     * Creates a Path with a given shape specifier (a Shape, a string in the SVG path format, or null to indicate no
     * shape).
     *
     * Path has two additional options (above what Node provides):
     * - shape: The actual Shape (or a string representing an SVG path, or null).
     * - boundsMethod: Determines how the bounds of a shape are determined.
     *
     * @param shape - The initial Shape to display. See onShapePropertyChange() for more details and documentation.
     * @param [providedOptions] - Path-specific options are documented in PATH_OPTION_KEYS above, and can be provided
     *                             along-side options for Node
     */
    constructor(shape, providedOptions) {
        assert && assert(providedOptions === undefined || Object.getPrototypeOf(providedOptions) === Object.prototype, 'Extra prototype on Node options object is a code smell');
        if (shape || providedOptions?.shape) {
            assert && assert(!shape || !providedOptions?.shape, 'Do not define shape twice. Check constructor and providedOptions.');
        }
        const initPathOptions = {
            boundsMethod: DEFAULT_OPTIONS.boundsMethod
        };
        if (isTReadOnlyProperty(shape)) {
            initPathOptions.shapeProperty = shape;
        }
        else {
            initPathOptions.shape = shape;
        }
        // Strict omit because one WILL be defined
        const options = optionize()(initPathOptions, providedOptions);
        super();
        // We'll initialize this by mutating.
        this._shapeProperty = new TinyForwardingProperty(null, false, this.onShapePropertyChange.bind(this));
        this._shape = DEFAULT_OPTIONS.shape;
        this._strokedShape = null;
        this._boundsMethod = DEFAULT_OPTIONS.boundsMethod;
        this._invalidShapeListener = this.invalidateShape.bind(this);
        this._invalidShapeListenerAttached = false;
        this.invalidateSupportedRenderers();
        this.mutate(options);
    }
    setShape(shape) {
        assert && assert(shape === null || typeof shape === 'string' || shape instanceof Shape, 'A path\'s shape should either be null, a string, or a Shape');
        this._shapeProperty.value = shape;
        return this;
    }
    set shape(value) { this.setShape(value); }
    get shape() { return this.getShape(); }
    /**
     * Returns the shape that was set for this Path (or for subtypes like Line and Rectangle, will return an immutable
     * Shape that is equivalent in appearance).
     *
     * It is best to generally assume modifications to the Shape returned is not supported. If there is no shape
     * currently, null will be returned.
     */
    getShape() {
        return this._shape;
    }
    onShapePropertyChange(shape) {
        assert && assert(shape === null || typeof shape === 'string' || shape instanceof Shape, 'A path\'s shape should either be null, a string, or a Shape');
        if (this._shape !== shape) {
            // Remove Shape invalidation listener if applicable
            if (this._invalidShapeListenerAttached) {
                this.detachShapeListener();
            }
            if (typeof shape === 'string') {
                // be content with onShapePropertyChange always invalidating the shape?
                shape = new Shape(shape);
            }
            this._shape = shape;
            this.invalidateShape();
            // Add Shape invalidation listener if applicable
            if (this._shape && !this._shape.isImmutable()) {
                this.attachShapeListener();
            }
        }
    }
    /**
     * See documentation for Node.setVisibleProperty, except this is for the shape
     */
    setShapeProperty(newTarget) {
        return this._shapeProperty.setTargetProperty(newTarget);
    }
    set shapeProperty(property) { this.setShapeProperty(property); }
    get shapeProperty() { return this.getShapeProperty(); }
    /**
     * Like Node.getVisibleProperty(), but for the shape. Note this is not the same as the Property provided in
     * setShapeProperty. Thus is the nature of TinyForwardingProperty.
     */
    getShapeProperty() {
        return this._shapeProperty;
    }
    /**
     * Returns a lazily-created Shape that has the appearance of the Path's shape but stroked using the current
     * stroke style of the Path.
     *
     * NOTE: It is invalid to call this on a Path that does not currently have a Shape (usually a Path where
     *       the shape is set to null).
     */
    getStrokedShape() {
        assert && assert(this.hasShape(), 'We cannot stroke a non-existing shape');
        // Lazily compute the stroked shape. It should be set to null when we need to recompute it
        if (!this._strokedShape) {
            this._strokedShape = this.getShape().getStrokedShape(this._lineDrawingStyles);
        }
        return this._strokedShape;
    }
    /**
     * Returns a bitmask representing the supported renderers for the current configuration of the Path or subtype.
     *
     * Should be overridden by subtypes to either extend or restrict renderers, depending on what renderers are
     * supported.
     *
     * @returns - A bitmask that includes supported renderers, see Renderer for details.
     */
    getPathRendererBitmask() {
        // By default, Canvas and SVG are accepted.
        return Renderer.bitmaskCanvas | Renderer.bitmaskSVG;
    }
    /**
     * Triggers a check and update for what renderers the current configuration of this Path or subtype supports.
     * This should be called whenever something that could potentially change supported renderers happen (which can
     * be the shape, properties of the strokes or fills, etc.)
     */
    invalidateSupportedRenderers() {
        this.setRendererBitmask(this.getFillRendererBitmask() & this.getStrokeRendererBitmask() & this.getPathRendererBitmask());
    }
    /**
     * Notifies the Path that the Shape has changed (either the Shape itself has be mutated, a new Shape has been
     * provided).
     *
     * NOTE: This should not be called on subtypes of Path after they have been constructed, like Line, Rectangle, etc.
     */
    invalidateShape() {
        this.invalidatePath();
        const stateLen = this._drawables.length;
        for (let i = 0; i < stateLen; i++) {
            this._drawables[i].markDirtyShape(); // subtypes of Path may not have this, but it's called during construction
        }
        // Disconnect our Shape listener if our Shape has become immutable.
        // see https://github.com/phetsims/sun/issues/270#issuecomment-250266174
        if (this._invalidShapeListenerAttached && this._shape && this._shape.isImmutable()) {
            this.detachShapeListener();
        }
    }
    /**
     * Invalidates the node's self-bounds and any other recorded metadata about the outline or bounds of the Shape.
     *
     * This is meant to be used for all Path subtypes (unlike invalidateShape).
     */
    invalidatePath() {
        this._strokedShape = null;
        this.invalidateSelf(); // We don't immediately compute the bounds
    }
    /**
     * Attaches a listener to our Shape that will be called whenever the Shape changes.
     */
    attachShapeListener() {
        assert && assert(!this._invalidShapeListenerAttached, 'We do not want to have two listeners attached!');
        // Do not attach shape listeners if we are disposed
        if (!this.isDisposed) {
            this._shape.invalidatedEmitter.addListener(this._invalidShapeListener);
            this._invalidShapeListenerAttached = true;
        }
    }
    /**
     * Detaches a previously-attached listener added to our Shape (see attachShapeListener).
     */
    detachShapeListener() {
        assert && assert(this._invalidShapeListenerAttached, 'We cannot detach an unattached listener');
        this._shape.invalidatedEmitter.removeListener(this._invalidShapeListener);
        this._invalidShapeListenerAttached = false;
    }
    /**
     * Computes a more efficient selfBounds for our Path.
     *
     * @returns - Whether the self bounds changed.
     */
    updateSelfBounds() {
        const selfBounds = this.hasShape() ? this.computeShapeBounds() : Bounds2.NOTHING;
        const changed = !selfBounds.equals(this.selfBoundsProperty._value);
        if (changed) {
            this.selfBoundsProperty._value.set(selfBounds);
        }
        return changed;
    }
    setBoundsMethod(boundsMethod) {
        assert && assert(boundsMethod === 'accurate' ||
            boundsMethod === 'unstroked' ||
            boundsMethod === 'tightPadding' ||
            boundsMethod === 'safePadding' ||
            boundsMethod === 'none');
        if (this._boundsMethod !== boundsMethod) {
            this._boundsMethod = boundsMethod;
            this.invalidatePath();
            this.rendererSummaryRefreshEmitter.emit(); // whether our self bounds are valid may have changed
        }
        return this;
    }
    set boundsMethod(value) { this.setBoundsMethod(value); }
    get boundsMethod() { return this.getBoundsMethod(); }
    /**
     * Returns the current bounds method. See setBoundsMethod for details.
     */
    getBoundsMethod() {
        return this._boundsMethod;
    }
    /**
     * Computes the bounds of the Path (or subtype when overridden). Meant to be overridden in subtypes for more
     * efficient bounds computations (but this will work as a fallback). Includes the stroked region if there is a
     * stroke applied to the Path.
     */
    computeShapeBounds() {
        const shape = this.getShape();
        // boundsMethod: 'none' will return no bounds
        if (this._boundsMethod === 'none' || !shape) {
            return Bounds2.NOTHING;
        }
        else {
            // boundsMethod: 'unstroked', or anything without a stroke will then just use the normal shape bounds
            if (!this.hasPaintableStroke() || this._boundsMethod === 'unstroked') {
                return shape.bounds;
            }
            else {
                // 'accurate' will always require computing the full stroked shape, and taking its bounds
                if (this._boundsMethod === 'accurate') {
                    return shape.getStrokedBounds(this.getLineStyles());
                }
                // Otherwise we compute bounds based on 'tightPadding' and 'safePadding', the one difference being that
                // 'safePadding' will include whatever bounds necessary to include miters. Square line-cap requires a
                // slightly extended bounds in either case.
                else {
                    let factor;
                    // If miterLength (inside corner to outside corner) exceeds miterLimit * strokeWidth, it will get turned to
                    // a bevel, so our factor will be based just on the miterLimit.
                    if (this._boundsMethod === 'safePadding' && this.getLineJoin() === 'miter') {
                        factor = this.getMiterLimit();
                    }
                    else if (this.getLineCap() === 'square') {
                        factor = Math.SQRT2;
                    }
                    else {
                        factor = 1;
                    }
                    return shape.bounds.dilated(factor * this.getLineWidth() / 2);
                }
            }
        }
    }
    /**
     * Whether this Node's selfBounds are considered to be valid (always containing the displayed self content
     * of this node). Meant to be overridden in subtypes when this can change (e.g. Text).
     *
     * If this value would potentially change, please trigger the event 'selfBoundsValid'.
     */
    areSelfBoundsValid() {
        if (this._boundsMethod === 'accurate' || this._boundsMethod === 'safePadding') {
            return true;
        }
        else if (this._boundsMethod === 'none') {
            return false;
        }
        else {
            return !this.hasStroke(); // 'tightPadding' and 'unstroked' options
        }
    }
    /**
     * Returns our self bounds when our rendered self is transformed by the matrix.
     */
    getTransformedSelfBounds(matrix) {
        assert && assert(this.hasShape());
        return (this._stroke ? this.getStrokedShape() : this.getShape()).getBoundsWithTransform(matrix);
    }
    /**
     * Returns our safe self bounds when our rendered self is transformed by the matrix.
     */
    getTransformedSafeSelfBounds(matrix) {
        return this.getTransformedSelfBounds(matrix);
    }
    /**
     * Called from (and overridden in) the Paintable trait, invalidates our current stroke, triggering recomputation of
     * anything that depended on the old stroke's value. (scenery-internal)
     */
    invalidateStroke() {
        this.invalidatePath();
        this.rendererSummaryRefreshEmitter.emit(); // Stroke changing could have changed our self-bounds-validitity (unstroked/etc)
        super.invalidateStroke();
    }
    /**
     * Returns whether this Path has an associated Shape (instead of no shape, represented by null)
     */
    hasShape() {
        return !!this._shape;
    }
    /**
     * Draws the current Node's self representation, assuming the wrapper's Canvas context is already in the local
     * coordinate frame of this node.
     *
     * @param wrapper
     * @param matrix - The transformation matrix already applied to the context.
     */
    canvasPaintSelf(wrapper, matrix) {
        //TODO: Have a separate method for this, instead of touching the prototype. Can make 'this' references too easily. https://github.com/phetsims/scenery/issues/1581
        PathCanvasDrawable.prototype.paintCanvas(wrapper, this, matrix);
    }
    /**
     * Creates a SVG drawable for this Path. (scenery-internal)
     *
     * @param renderer - In the bitmask format specified by Renderer, which may contain additional bit flags.
     * @param instance - Instance object that will be associated with the drawable
     */
    createSVGDrawable(renderer, instance) {
        // @ts-expect-error
        return PathSVGDrawable.createFromPool(renderer, instance);
    }
    /**
     * Creates a Canvas drawable for this Path. (scenery-internal)
     *
     * @param renderer - In the bitmask format specified by Renderer, which may contain additional bit flags.
     * @param instance - Instance object that will be associated with the drawable
     */
    createCanvasDrawable(renderer, instance) {
        // @ts-expect-error
        return PathCanvasDrawable.createFromPool(renderer, instance);
    }
    /**
     * Whether this Node itself is painted (displays something itself).
     */
    isPainted() {
        // Always true for Path nodes
        return true;
    }
    /**
     * Computes whether the provided point is "inside" (contained) in this Path's self content, or "outside".
     *
     * @param point - Considered to be in the local coordinate frame
     */
    containsPointSelf(point) {
        let result = false;
        if (!this.hasShape()) {
            return result;
        }
        // if this node is fillPickable, we will return true if the point is inside our fill area
        if (this._fillPickable) {
            result = this.getShape().containsPoint(point);
        }
        // also include the stroked region in the hit area if strokePickable
        if (!result && this._strokePickable) {
            result = this.getStrokedShape().containsPoint(point);
        }
        return result;
    }
    /**
     * Returns a Shape that represents the area covered by containsPointSelf.
     */
    getSelfShape() {
        return Shape.union([
            ...((this.hasShape() && this._fillPickable) ? [this.getShape()] : []),
            ...((this.hasShape() && this._strokePickable) ? [this.getStrokedShape()] : [])
        ]);
    }
    /**
     * Returns whether this Path's selfBounds is intersected by the specified bounds.
     *
     * @param bounds - Bounds to test, assumed to be in the local coordinate frame.
     */
    intersectsBoundsSelf(bounds) {
        // TODO: should a shape's stroke be included? https://github.com/phetsims/scenery/issues/1581
        return this._shape ? this._shape.intersectsBounds(bounds) : false;
    }
    /**
     * Returns whether we need to apply a transform workaround for https://github.com/phetsims/scenery/issues/196, which
     * only applies when we have a pattern or gradient (e.g. subtypes of Paint).
     */
    requiresSVGBoundsWorkaround() {
        if (!this._stroke || !(this._stroke instanceof Paint) || !this.hasShape()) {
            return false;
        }
        const bounds = this.computeShapeBounds();
        return (bounds.width * bounds.height) === 0; // at least one of them was zero, so the bounding box has no area
    }
    /**
     * Override for extra information in the debugging output (from Display.getDebugHTML()). (scenery-internal)
     */
    getDebugHTMLExtras() {
        return this._shape ? ` (<span style="color: #88f" onclick="window.open( 'data:text/plain;charset=utf-8,' + encodeURIComponent( '${this._shape.getSVGPath()}' ) );">path</span>)` : '';
    }
    /**
     * Disposes the path, releasing shape listeners if needed (and preventing new listeners from being added).
     */
    dispose() {
        if (this._invalidShapeListenerAttached) {
            this.detachShapeListener();
        }
        this._shapeProperty.dispose();
        super.dispose();
    }
    mutate(options) {
        return super.mutate(options);
    }
}
// Initial values for most Node mutator options
Path.DEFAULT_PATH_OPTIONS = combineOptions({}, Node.DEFAULT_NODE_OPTIONS, DEFAULT_OPTIONS);
export default Path;
/**
 * {Array.<string>} - String keys for all of the allowed options that will be set by node.mutate( options ), in the
 * order they will be evaluated in.
 *
 * NOTE: See Node's _mutatorKeys documentation for more information on how this operates, and potential special
 *       cases that may apply.
 */
Path.prototype._mutatorKeys = [...PAINTABLE_OPTION_KEYS, ...PATH_OPTION_KEYS, ...Node.prototype._mutatorKeys];
/**
 * {Array.<String>} - List of all dirty flags that should be available on drawables created from this node (or
 *                    subtype). Given a flag (e.g. radius), it indicates the existence of a function
 *                    drawable.markDirtyRadius() that will indicate to the drawable that the radius has changed.
 * (scenery-internal)
 * @override
 */
Path.prototype.drawableMarkFlags = [...Node.prototype.drawableMarkFlags, ...PAINTABLE_DRAWABLE_MARK_FLAGS, 'shape'];
scenery.register('Path', Path);
//# sourceMappingURL=Path.js.map