<commit_before>// Copyright 2017-2022, University of Colorado Boulder

/**
 * Base type that provides PhET-iO features. An instrumented PhetioObject is referred to on the wrapper side/design side
 * as a "PhET-iO element".  Note that sims may have hundreds or thousands of PhetioObjects, so performance and memory
 * considerations are important.  For this reason, initializePhetioObject is only called in PhET-iO brand, which means
 * many of the getters such as `phetioState` and `phetioDocumentation` will not work in other brands. We have opted
 * to have these getters throw assertion errors in other brands to help identify problems if these are called
 * unexpectedly.
 *
 * @author <NAME> (PhET Interactive Simulations)
 * @author <NAME> (PhET Interactive Simulations)
 */

import animationFrameTimer from '../../axon/js/animationFrameTimer.js';
import StrictOmit from '../../phet-core/js/types/StrictOmit.js';
import validate from '../../axon/js/validate.js';
import arrayRemove from '../../phet-core/js/arrayRemove.js';
import assertMutuallyExclusiveOptions from '../../phet-core/js/assertMutuallyExclusiveOptions.js';
import merge from '../../phet-core/js/merge.js';
import optionize, { combineOptions, EmptySelfOptions, OptionizeDefaults } from '../../phet-core/js/optionize.js';
import EventType from './EventType.js';
import LinkedElementIO from './LinkedElementIO.js';
import phetioAPIValidation from './phetioAPIValidation.js';
import Tandem from './Tandem.js';
import TandemConstants, { PhetioObjectMetadata } from './TandemConstants.js';
import tandemNamespace from './tandemNamespace.js';
import IOType from './types/IOType.js';
import IntentionalAny from '../../phet-core/js/types/IntentionalAny.js';

// constants
const PHET_IO_ENABLED = Tandem.PHET_IO_ENABLED;
const IO_TYPE_VALIDATOR = { valueType: IOType, validationMessage: 'phetioType must be an IOType' };
const BOOLEAN_VALIDATOR = { valueType: 'boolean' };

// use "<br>" instead of newlines
const PHET_IO_DOCUMENTATION_VALIDATOR = {
  valueType: 'string',
  isValidValue: ( doc: string ) => !doc.includes( '\n' ),
  validationMessage: 'phetioDocumentation must be provided in the right format'
};
const PHET_IO_EVENT_TYPE_VALIDATOR = {
  valueType: EventType,
  validationMessage: 'invalid phetioEventType'
};
const OBJECT_VALIDATOR = { valueType: [ Object, null ] };

const objectToPhetioID = ( phetioObject: PhetioObject ) => phetioObject.tandem.phetioID;

type StartEventOptions = {
  data?: Record<string, IntentionalAny> | null;
  getData?: ( () => Record<string, IntentionalAny> ) | null;
};

// When an event is suppressed from the data stream, we keep track of it with this token.
const SKIPPING_MESSAGE = -1;

const DEFAULTS: OptionizeDefaults<PhetioObjectOptions> = {

  // Subtypes can use `Tandem.tandemRequired` to require a named tandem passed in
  tandem: Tandem.OPTIONAL,

  // Defines API methods, events and serialization
  phetioType: IOType.ObjectIO,

  // {string} Useful notes about an instrumented PhetioObject, shown in the PhET-iO Studio Wrapper. It's an html
  // string, so "<br>" tags are required instead of "\n" characters for proper rendering in Studio
  phetioDocumentation: TandemConstants.PHET_IO_OBJECT_METADATA_DEFAULTS.phetioDocumentation,

  // When true, includes the PhetioObject in the PhET-iO state (not automatically recursive, must be specified for
  // children explicitly)
  phetioState: TandemConstants.PHET_IO_OBJECT_METADATA_DEFAULTS.phetioState,

  // This option controls how PhET-iO wrappers can interface with this PhetioObject. Predominately this occurs via
  // public methods defined on this PhetioObject's phetioType, in which some method are not executable when this flag
  // is true. See `ObjectIO.methods` for further documentation, especially regarding `invocableForReadOnlyElements`.
  phetioReadOnly: TandemConstants.PHET_IO_OBJECT_METADATA_DEFAULTS.phetioReadOnly,

  // Category of event type, can be overridden in phetioStartEvent options.  Cannot be supplied through TandemConstants because
  // that would create an import loop
  // @ts-ignore
  phetioEventType: EventType.MODEL,

  // High frequency events such as mouse moves can be omitted from data stream, see ?phetioEmitHighFrequencyEvents
  // and Client.launchSim option
  // @deprecated - see https://github.com/phetsims/phet-io/issues/1629#issuecomment-608002410
  phetioHighFrequency: TandemConstants.PHET_IO_OBJECT_METADATA_DEFAULTS.phetioHighFrequency,

  // When true, emits events for data streams for playback, see handlePlaybackEvent.js
  phetioPlayback: TandemConstants.PHET_IO_OBJECT_METADATA_DEFAULTS.phetioPlayback,

  // When true, this is categorized as an important "featured" element in Studio.
  phetioFeatured: TandemConstants.PHET_IO_OBJECT_METADATA_DEFAULTS.phetioFeatured,

  // {boolean} optional - indicates that an object may or may not have been created. Applies recursively automatically
  // and should only be set manually on the root dynamic element. Dynamic archetypes will have this overwritten to
  // false even if explicitly provided as true, as archetypes cannot be dynamic.
  phetioDynamicElement: TandemConstants.PHET_IO_OBJECT_METADATA_DEFAULTS.phetioDynamicElement,

  // {boolean} Marking phetioDesigned: true opts-in to API change detection tooling that can be used to catch inadvertent
  // changes to a designed API.  A phetioDesigned:true PhetioObject (or any of its tandem descendants) will throw
  // assertion errors on CT (or when running with ?phetioCompareAPI) when the following are true:
  // (a) its package.json lists compareDesignedAPIChanges:true in the "phet-io" section
  // (b) the simulation is listed in perennial/data/phet-io-api-stable
  // (c) any of its metadata values deviate from the reference API
  phetioDesigned: TandemConstants.PHET_IO_OBJECT_METADATA_DEFAULTS.phetioDesigned,

  // {Object|null} optional - delivered with each event, if specified. phetioPlayback is appended here, if true.
  // Note: unlike other options, this option can be mutated downstream, and hence should be created newly for each instance.
  phetioEventMetadata: null
};

// If you run into a type error here, feel free to add any type that is supported by the browsers "structured cloning algorithm" https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
type EventMetadata = Record<string, string | boolean | number | Array<string | boolean | number>>;

// @ts-ignore
assert && assert( EventType.phetioType.toStateObject( DEFAULTS.phetioEventType ) === TandemConstants.PHET_IO_OBJECT_METADATA_DEFAULTS.phetioEventType,
  'phetioEventType must have the same default as the default metadata values.' );

// Options for creating a PhetioObject
export type PhetioObjectOptions = StrictOmit<Partial<PhetioObjectMetadata>, 'phetioTypeName' | 'phetioArchetypePhetioID' |
  'phetioIsArchetype' | 'phetioEventType'> & {
  tandem?: Tandem;
  phetioType?: IOType;
  phetioEventType?: EventType;
  phetioEventMetadata?: EventMetadata | null;
};

type PhetioObjectMetadataKeys = keyof ( StrictOmit<PhetioObjectMetadata, 'phetioTypeName'> ) | 'phetioType';
export type PhetioObjectMetadataInput = Pick<PhetioObject, PhetioObjectMetadataKeys>;

export type LinkableElement = Pick<PhetioObject, 'phetioFeatured' | 'isPhetioInstrumented'>;

class PhetioObject {

  // assigned in initializePhetioObject - see docs at DEFAULTS declaration
  public tandem: Tandem;

  // track whether the object has been initialized.  This is necessary because initialization can happen in the
  // constructor or in a subsequent call to initializePhetioObject (to support scenery Node)
  private phetioObjectInitialized: boolean;

  // Has it been disposed?
  public isDisposed: boolean;

  // See documentation in DEFAULTS
  public phetioIsArchetype!: boolean;
  public phetioBaselineMetadata!: PhetioObjectMetadata | null;
  private _phetioType!: IOType;
  private _phetioState!: boolean;
  private _phetioReadOnly!: boolean;
  private _phetioDocumentation!: string;
  private _phetioEventType!: EventType;
  private _phetioHighFrequency!: boolean;
  private _phetioPlayback!: boolean;
  private _phetioDynamicElement!: boolean;
  private _phetioFeatured!: boolean;
  private _phetioEventMetadata!: EventMetadata | null;
  private _phetioDesigned!: boolean;

  // Public only for PhetioObjectMetadataInput
  public phetioArchetypePhetioID!: string | null;

  // Public only for tests
  public linkedElements!: LinkedElement[] | null;
  public phetioNotifiedObjectCreated!: boolean;
  private phetioMessageStack!: number[];
  public static DEFAULT_OPTIONS = DEFAULTS;

  public constructor( options?: PhetioObjectOptions ) {

    this.tandem = DEFAULTS.tandem;
    this.phetioObjectInitialized = false;
    this.isDisposed = false;

    if ( options ) {
      this.initializePhetioObject( {}, options );
    }

    if ( assert ) {

      // Wrap the prototype dispose method with a check. NOTE: We will not catch devious cases where the dispose() is
      // overridden after the Node constructor (which may happen).
      const protoDispose = this.dispose;
      this.dispose = () => {
        assert && assert( !this.isDisposed, 'This PhetioObject has already been disposed, and cannot be disposed again' );
        protoDispose.call( this );
        assert && assert( this.isDisposed, 'PhetioObject.dispose() call is missing from an overridden dispose method' );
      };
    }
  }

  /**
   * Like SCENERY/Node, PhetioObject can be configured during construction or later with a mutate call.
   * Noop if provided options keys don't intersect with any key in DEFAULTS; baseOptions are ignored for this calculation.
   */<commit_msg>convert public entities to private if only used in tests (use bracket notation), https://github.com/phetsims/chipper/issues/1295<commit_after>// Copyright 2017-2022, University of Colorado Boulder

/**
 * Base type that provides PhET-iO features. An instrumented PhetioObject is referred to on the wrapper side/design side
 * as a "PhET-iO element".  Note that sims may have hundreds or thousands of PhetioObjects, so performance and memory
 * considerations are important.  For this reason, initializePhetioObject is only called in PhET-iO brand, which means
 * many of the getters such as `phetioState` and `phetioDocumentation` will not work in other brands. We have opted
 * to have these getters throw assertion errors in other brands to help identify problems if these are called
 * unexpectedly.
 *
 * @author <NAME> (PhET Interactive Simulations)
 * @author <NAME> (PhET Interactive Simulations)
 */

import animationFrameTimer from '../../axon/js/animationFrameTimer.js';
import StrictOmit from '../../phet-core/js/types/StrictOmit.js';
import validate from '../../axon/js/validate.js';
import arrayRemove from '../../phet-core/js/arrayRemove.js';
import assertMutuallyExclusiveOptions from '../../phet-core/js/assertMutuallyExclusiveOptions.js';
import merge from '../../phet-core/js/merge.js';
import optionize, { combineOptions, EmptySelfOptions, OptionizeDefaults } from '../../phet-core/js/optionize.js';
import EventType from './EventType.js';
import LinkedElementIO from './LinkedElementIO.js';
import phetioAPIValidation from './phetioAPIValidation.js';
import Tandem from './Tandem.js';
import TandemConstants, { PhetioObjectMetadata } from './TandemConstants.js';
import tandemNamespace from './tandemNamespace.js';
import IOType from './types/IOType.js';
import IntentionalAny from '../../phet-core/js/types/IntentionalAny.js';

// constants
const PHET_IO_ENABLED = Tandem.PHET_IO_ENABLED;
const IO_TYPE_VALIDATOR = { valueType: IOType, validationMessage: 'phetioType must be an IOType' };
const BOOLEAN_VALIDATOR = { valueType: 'boolean' };

// use "<br>" instead of newlines
const PHET_IO_DOCUMENTATION_VALIDATOR = {
  valueType: 'string',
  isValidValue: ( doc: string ) => !doc.includes( '\n' ),
  validationMessage: 'phetioDocumentation must be provided in the right format'
};
const PHET_IO_EVENT_TYPE_VALIDATOR = {
  valueType: EventType,
  validationMessage: 'invalid phetioEventType'
};
const OBJECT_VALIDATOR = { valueType: [ Object, null ] };

const objectToPhetioID = ( phetioObject: PhetioObject ) => phetioObject.tandem.phetioID;

type StartEventOptions = {
  data?: Record<string, IntentionalAny> | null;
  getData?: ( () => Record<string, IntentionalAny> ) | null;
};

// When an event is suppressed from the data stream, we keep track of it with this token.
const SKIPPING_MESSAGE = -1;

const DEFAULTS: OptionizeDefaults<PhetioObjectOptions> = {

  // Subtypes can use `Tandem.tandemRequired` to require a named tandem passed in
  tandem: Tandem.OPTIONAL,

  // Defines API methods, events and serialization
  phetioType: IOType.ObjectIO,

  // {string} Useful notes about an instrumented PhetioObject, shown in the PhET-iO Studio Wrapper. It's an html
  // string, so "<br>" tags are required instead of "\n" characters for proper rendering in Studio
  phetioDocumentation: TandemConstants.PHET_IO_OBJECT_METADATA_DEFAULTS.phetioDocumentation,

  // When true, includes the PhetioObject in the PhET-iO state (not automatically recursive, must be specified for
  // children explicitly)
  phetioState: TandemConstants.PHET_IO_OBJECT_METADATA_DEFAULTS.phetioState,

  // This option controls how PhET-iO wrappers can interface with this PhetioObject. Predominately this occurs via
  // public methods defined on this PhetioObject's phetioType, in which some method are not executable when this flag
  // is true. See `ObjectIO.methods` for further documentation, especially regarding `invocableForReadOnlyElements`.
  phetioReadOnly: TandemConstants.PHET_IO_OBJECT_METADATA_DEFAULTS.phetioReadOnly,

  // Category of event type, can be overridden in phetioStartEvent options.  Cannot be supplied through TandemConstants because
  // that would create an import loop
  // @ts-ignore
  phetioEventType: EventType.MODEL,

  // High frequency events such as mouse moves can be omitted from data stream, see ?phetioEmitHighFrequencyEvents
  // and Client.launchSim option
  // @deprecated - see https://github.com/phetsims/phet-io/issues/1629#issuecomment-608002410
  phetioHighFrequency: TandemConstants.PHET_IO_OBJECT_METADATA_DEFAULTS.phetioHighFrequency,

  // When true, emits events for data streams for playback, see handlePlaybackEvent.js
  phetioPlayback: TandemConstants.PHET_IO_OBJECT_METADATA_DEFAULTS.phetioPlayback,

  // When true, this is categorized as an important "featured" element in Studio.
  phetioFeatured: TandemConstants.PHET_IO_OBJECT_METADATA_DEFAULTS.phetioFeatured,

  // {boolean} optional - indicates that an object may or may not have been created. Applies recursively automatically
  // and should only be set manually on the root dynamic element. Dynamic archetypes will have this overwritten to
  // false even if explicitly provided as true, as archetypes cannot be dynamic.
  phetioDynamicElement: TandemConstants.PHET_IO_OBJECT_METADATA_DEFAULTS.phetioDynamicElement,

  // {boolean} Marking phetioDesigned: true opts-in to API change detection tooling that can be used to catch inadvertent
  // changes to a designed API.  A phetioDesigned:true PhetioObject (or any of its tandem descendants) will throw
  // assertion errors on CT (or when running with ?phetioCompareAPI) when the following are true:
  // (a) its package.json lists compareDesignedAPIChanges:true in the "phet-io" section
  // (b) the simulation is listed in perennial/data/phet-io-api-stable
  // (c) any of its metadata values deviate from the reference API
  phetioDesigned: TandemConstants.PHET_IO_OBJECT_METADATA_DEFAULTS.phetioDesigned,

  // {Object|null} optional - delivered with each event, if specified. phetioPlayback is appended here, if true.
  // Note: unlike other options, this option can be mutated downstream, and hence should be created newly for each instance.
  phetioEventMetadata: null
};

// If you run into a type error here, feel free to add any type that is supported by the browsers "structured cloning algorithm" https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
type EventMetadata = Record<string, string | boolean | number | Array<string | boolean | number>>;

// @ts-ignore
assert && assert( EventType.phetioType.toStateObject( DEFAULTS.phetioEventType ) === TandemConstants.PHET_IO_OBJECT_METADATA_DEFAULTS.phetioEventType,
  'phetioEventType must have the same default as the default metadata values.' );

// Options for creating a PhetioObject
export type PhetioObjectOptions = StrictOmit<Partial<PhetioObjectMetadata>, 'phetioTypeName' | 'phetioArchetypePhetioID' |
  'phetioIsArchetype' | 'phetioEventType'> & {
  tandem?: Tandem;
  phetioType?: IOType;
  phetioEventType?: EventType;
  phetioEventMetadata?: EventMetadata | null;
};

type PhetioObjectMetadataKeys = keyof ( StrictOmit<PhetioObjectMetadata, 'phetioTypeName'> ) | 'phetioType';
export type PhetioObjectMetadataInput = Pick<PhetioObject, PhetioObjectMetadataKeys>;

export type LinkableElement = Pick<PhetioObject, 'phetioFeatured' | 'isPhetioInstrumented'>;

class PhetioObject {

  // assigned in initializePhetioObject - see docs at DEFAULTS declaration
  public tandem: Tandem;

  // track whether the object has been initialized.  This is necessary because initialization can happen in the
  // constructor or in a subsequent call to initializePhetioObject (to support scenery Node)
  private phetioObjectInitialized: boolean;

  // Has it been disposed?
  public isDisposed: boolean;

  // See documentation in DEFAULTS
  public phetioIsArchetype!: boolean;
  public phetioBaselineMetadata!: PhetioObjectMetadata | null;
  private _phetioType!: IOType;
  private _phetioState!: boolean;
  private _phetioReadOnly!: boolean;
  private _phetioDocumentation!: string;
  private _phetioEventType!: EventType;
  private _phetioHighFrequency!: boolean;
  private _phetioPlayback!: boolean;
  private _phetioDynamicElement!: boolean;
  private _phetioFeatured!: boolean;
  private _phetioEventMetadata!: EventMetadata | null;
  private _phetioDesigned!: boolean;

  // Public only for PhetioObjectMetadataInput
  public phetioArchetypePhetioID!: string | null;
  private linkedElements!: LinkedElement[] | null;
  public phetioNotifiedObjectCreated!: boolean;
  private phetioMessageStack!: number[];
  public static DEFAULT_OPTIONS = DEFAULTS;

  public constructor( options?: PhetioObjectOptions ) {

    this.tandem = DEFAULTS.tandem;
    this.phetioObjectInitialized = false;
    this.isDisposed = false;

    if ( options ) {
      this.initializePhetioObject( {}, options );
    }

    if ( assert ) {

      // Wrap the prototype dispose method with a check. NOTE: We will not catch devious cases where the dispose() is
      // overridden after the Node constructor (which may happen).
      const protoDispose = this.dispose;
      this.dispose = () => {
        assert && assert( !this.isDisposed, 'This PhetioObject has already been disposed, and cannot be disposed again' );
        protoDispose.call( this );
        assert && assert( this.isDisposed, 'PhetioObject.dispose() call is missing from an overridden dispose method' );
      };
    }
  }

  /**
   * Like SCENERY/Node, PhetioObject can be configured during construction or later with a mutate call.
   * Noop if provided options keys don't intersect with any key in DEFAULTS; baseOptions are ignored for this calculation.
   */
{'fraction_non_alphanumeric': 0.08495021045067241, 'fraction_numerical': 0.0024638127502309825, 'mean_word_length': 4.65544267053701, 'pattern_counts': {'":': 0, '<': 31, '<?xml version=': 0, '>': 39, 'https://': 5, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1156736', 'n_tokens_mistral': 5898, 'n_tokens_neox': 5718, 'n_words': 1873}