import _ from 'lodash';
import '../../../assert/js/assert.js';
import '../../../globals.js';
// Copyright 2020-2024, University of Colorado Boulder
/**
 * IOTypes form a synthetic type system used to describe PhET-iO Elements. A PhET-iO Element is an instrumented PhetioObject
 * that is interoperable from the "wrapper" frame (outside the sim frame). An IOType includes documentation, methods,
 * names, serialization, etc.
 *
 * @author Sam Reid (PhET Interactive Simulations)
 */
import validate from '../../../axon/js/validate.js';
import Validation from '../../../axon/js/Validation.js';
import optionize from '../../../phet-core/js/optionize.js';
import PhetioConstants from '../PhetioConstants.js';
import TandemConstants from '../TandemConstants.js';
import tandemNamespace from '../tandemNamespace.js';
import StateSchema from './StateSchema.js';
// constants
const VALIDATE_OPTIONS_FALSE = { validateValidator: false };
const truthy = (x) => !!x;
// Global flag that triggers pruning the state object down to only that which gets tracked by the PhET-iO API, see
// apiStateKeys to opt into api state tracking
let GETTING_STATE_FOR_API = false;
let API_STATE_NESTED_COUNT = 0;
/**
 * Estimate the core type name from a given IOType name.
 */
const getCoreTypeName = (ioTypeName) => {
    const index = ioTypeName.indexOf(PhetioConstants.IO_TYPE_SUFFIX);
    assert && assert(index >= 0, 'IO should be in the type name');
    return ioTypeName.substring(0, index);
};
// StateType is the whole thing, SelfStateType is just at this level
// export default class IOType<T = any, SelfStateType = any, ParentStateType = EmptyParent, StateType extends SelfStateType & ParentStateType = SelfStateType & ParentStateType> { // eslint-disable-line @typescript-eslint/no-explicit-any
export default class IOType {
    /**
     * @param typeName - The name that this IOType will have in the public PhET-iO API. In general, this should
     *    only be word characters, ending in "IO". Parametric types are a special subset of IOTypes that include their
     *    parameters in their typeName. If an IOType's parameters are other IOType(s), then they should be included within
     *    angle brackets, like "PropertyIO<BooleanIO>". Some other types use a more custom format for displaying their
     *    parameter types, in this case the parameter section of the type name (immediately following "IO") should begin
     *    with an open paren, "(". Thus the schema for a typeName could be defined (using regex) as `[A-Z]\w*IO([(<].*){0,1}`.
     *    Parameterized types should also include a `parameterTypes` field on the IOType.
     * @param providedOptions
     */
    constructor(typeName, providedOptions) {
        this.typeName = typeName;
        // For reference in the options
        const supertype = providedOptions.supertype || IOType.ObjectIO;
        const toStateObjectSupplied = !!(providedOptions.toStateObject);
        const applyStateSupplied = !!(providedOptions.applyState);
        const stateSchemaSupplied = !!(providedOptions.stateSchema);
        const options = optionize()({
            supertype: IOType.ObjectIO,
            methods: {},
            events: [],
            metadataDefaults: {},
            //  Most likely this will remain PhET-iO internal, and shouldn't need to be used when creating IOTypes outside of tandem/.
            dataDefaults: {},
            methodOrder: [],
            parameterTypes: [],
            documentation: `PhET-iO Type for ${getCoreTypeName(typeName)}`,
            isFunctionType: false,
            /**** STATE ****/
            toStateObject: null,
            fromStateObject: null,
            stateObjectToCreateElementArguments: null,
            applyState: null,
            stateSchema: null,
            apiStateKeys: null,
            defaultDeserializationMethod: 'fromStateObject',
            addChildElement: supertype && supertype.addChildElement
        }, providedOptions);
        if (assert && supertype) {
            Object.keys(options.metadataDefaults).forEach(metadataDefaultKey => {
                assert && supertype.getAllMetadataDefaults().hasOwnProperty(metadataDefaultKey) &&
                    assert(supertype.getAllMetadataDefaults()[metadataDefaultKey] !== options.metadataDefaults[metadataDefaultKey], `${metadataDefaultKey} should not have the same default value as the ancestor metadata default.`);
            });
        }
        this.supertype = supertype;
        this.documentation = options.documentation;
        this.methods = options.methods;
        this.events = options.events;
        this.metadataDefaults = options.metadataDefaults;
        this.dataDefaults = options.dataDefaults;
        this.methodOrder = options.methodOrder;
        this.parameterTypes = options.parameterTypes;
        // Validation
        this.validator = _.pick(options, Validation.VALIDATOR_KEYS);
        this.validator.validationMessage = this.validator.validationMessage || `Validation failed IOType Validator: ${this.typeName}`;
        this.defaultDeserializationMethod = options.defaultDeserializationMethod;
        if (options.stateSchema === null || options.stateSchema instanceof StateSchema) {
            this.stateSchema = options.stateSchema;
        }
        else {
            const compositeSchema = typeof options.stateSchema === 'function' ? options.stateSchema(this) : options.stateSchema;
            this.stateSchema = new StateSchema({
                compositeSchema: compositeSchema,
                apiStateKeys: options.apiStateKeys
            });
        }
        // Assert that toStateObject method is provided for value StateSchemas. Do this with the following logic:
        // 1. It is acceptable to not provide a stateSchema (for IOTypes that aren't stateful)
        // 2. You must either provide a toStateObject, or have a composite StateSchema. Composite state schemas support default serialization methods.
        assert && assert(!this.stateSchema || (toStateObjectSupplied || this.stateSchema.isComposite()), 'toStateObject method must be provided for value StateSchemas');
        this.toStateObjectOption = options.toStateObject;
        this.fromStateObjectOption = options.fromStateObject;
        this.applyStateOption = options.applyState;
        this.stateObjectToCreateElementArgumentsOption = options.stateObjectToCreateElementArguments;
        this.toStateObjectSupplied = toStateObjectSupplied;
        this.applyStateSupplied = applyStateSupplied;
        this.stateSchemaSupplied = stateSchemaSupplied;
        this.isFunctionType = options.isFunctionType;
        this.addChildElement = options.addChildElement;
        if (assert) {
            assert && assert(supertype || this.typeName === 'ObjectIO', 'supertype is required');
            assert && assert(!this.typeName.includes('.'), 'Dots should not appear in type names');
            assert && assert(this.typeName.split(/[<(]/)[0].endsWith(PhetioConstants.IO_TYPE_SUFFIX), `IOType name must end with ${PhetioConstants.IO_TYPE_SUFFIX}`);
            assert && assert(this.hasOwnProperty('typeName'), 'this.typeName is required');
            // assert that each public method adheres to the expected schema
            this.methods && Object.values(this.methods).forEach((methodObject) => {
                if (typeof methodObject === 'object') {
                    assert && methodObject.invocableForReadOnlyElements && assert(typeof methodObject.invocableForReadOnlyElements === 'boolean', `invocableForReadOnlyElements must be of type boolean: ${methodObject.invocableForReadOnlyElements}`);
                }
            });
            assert && assert(this.documentation.length > 0, 'documentation must be provided');
            this.methods && this.hasOwnProperty('methodOrder') && this.methodOrder.forEach(methodName => {
                assert && assert(this.methods[methodName], `methodName not in public methods: ${methodName}`);
            });
            if (supertype) {
                const typeHierarchy = supertype.getTypeHierarchy();
                assert && this.events && this.events.forEach(event => {
                    // Make sure events are not listed again
                    assert && assert(!_.some(typeHierarchy, t => t.events.includes(event)), `IOType should not declare event that parent also has: ${event}`);
                });
                if (this.stateSchema?.apiStateKeys) {
                    const supertypeAPIKeys = supertype.getAllAPIStateKeys();
                    this.stateSchema?.apiStateKeys.forEach(apiStateKey => {
                        assert && assert(!supertypeAPIKeys.includes(apiStateKey), `apiStateKey is already in the super: ${apiStateKey}`);
                    });
                }
            }
            else {
                // The root IOType must supply all 4 state methods.
                assert && assert(typeof options.toStateObject === 'function', 'toStateObject must be defined');
                assert && assert(typeof options.fromStateObject === 'function', 'fromStateObject must be defined');
                assert && assert(typeof options.stateObjectToCreateElementArguments === 'function', 'stateObjectToCreateElementArguments must be defined');
                assert && assert(typeof options.applyState === 'function', 'applyState must be defined');
            }
        }
    }
    toStateObject(coreObject) {
        API_STATE_NESTED_COUNT++;
        // validate( coreObject, this.validator, VALIDATE_OPTIONS_FALSE );
        let stateObject;
        // Only do this non-standard toStateObject function if there is a stateSchema but no toStateObject provided
        if (!this.toStateObjectSupplied && this.stateSchemaSupplied && this.stateSchema && this.stateSchema.isComposite()) {
            stateObject = this.defaultToStateObject(coreObject);
        }
        else {
            assert && !this.toStateObjectOption && assert(this.supertype, 'supertype expected if no toStateObject option is provided');
            stateObject = this.toStateObjectOption ? this.toStateObjectOption(coreObject) : this.supertype.toStateObject(coreObject);
        }
        // Do not validate the api state, which get's pruned based on provided apiStateKeys, only validate the complete state
        if (assert && !GETTING_STATE_FOR_API &&
            // only if this IOType instance has more to validate than the supertype
            (this.toStateObjectSupplied || this.stateSchemaSupplied)) {
            // Only validate the stateObject if it is phetioState:true.
            // This is an n*m algorithm because for each time toStateObject is called and needs validation, this.validateStateObject
            // looks all the way up the IOType hierarchy. This is not efficient, but gains us the ability to make sure that
            // the stateObject doesn't have any superfluous, unexpected keys. The "m" portion is based on how many sub-properties
            // in a state call `toStateObject`, and the "n" portion is based on how many IOTypes in the hierarchy define a
            // toStateObject or stateSchema. In the future we could potentially improve performance by having validateStateObject
            // only check against the schema at this level, but then extra keys in the stateObject would not be caught. From work done in https://github.com/phetsims/phet-io/issues/1774
            this.validateStateObject(stateObject);
        }
        let resolvedStateObject;
        // When getting API state, prune out any state that don't opt in as desired for API tracking, see apiStateKeys
        if (GETTING_STATE_FOR_API && this.isCompositeStateSchema() &&
            // When running a nested toStateObject call while generating api state, values should be opt in, because the
            // element state has asked for these values. For example PropertyIO<RangeIO> wants to see min/max state in
            // its validValues.
            !(API_STATE_NESTED_COUNT > 1 && this.apiStateKeysProvided())) {
            resolvedStateObject = _.pick(stateObject, this.getAllAPIStateKeys());
        }
        else {
            resolvedStateObject = stateObject;
        }
        API_STATE_NESTED_COUNT--;
        return resolvedStateObject;
    }
    fromStateObject(stateObject) {
        if (this.fromStateObjectOption) {
            return this.fromStateObjectOption(stateObject);
        }
        assert && assert(this.supertype);
        return this.supertype.fromStateObject(stateObject);
    }
    applyState(coreObject, stateObject) {
        validate(coreObject, this.validator, VALIDATE_OPTIONS_FALSE);
        // Validate, but only if this IOType instance has more to validate than the supertype
        if (this.applyStateSupplied || this.stateSchemaSupplied) {
            // Validate that the provided stateObject is of the expected schema
            // NOTE: Cannot use this.validateStateObject because options adopts supertype.applyState, which is bounds to the
            // parent IOType. This prevents correct validation because the supertype doesn't know about the subtype schemas.
            // @ts-expect-error we cannot type check against PhetioObject from this file
            assert && coreObject.phetioType && coreObject.phetioType.validateStateObject(stateObject);
        }
        // Only do this non-standard applyState function from stateSchema if there is a stateSchema but no applyState provided
        if (!this.applyStateSupplied && this.stateSchemaSupplied && this.stateSchema && this.stateSchema.isComposite()) {
            this.defaultApplyState(coreObject, stateObject);
        }
        else {
            assert && !this.applyStateOption && assert(this.supertype, 'supertype expected if no applyState option is provided');
            this.applyStateOption ? this.applyStateOption(coreObject, stateObject) : this.supertype.applyState(coreObject, stateObject);
        }
    }
    stateObjectToCreateElementArguments(stateObject) {
        if (this.stateObjectToCreateElementArgumentsOption) {
            return this.stateObjectToCreateElementArgumentsOption(stateObject);
        }
        assert && assert(this.supertype);
        return this.supertype.stateObjectToCreateElementArguments(stateObject);
    }
    // Include state from all composite state schemas up and down the type hierarchy (children overriding parents).
    defaultToStateObject(coreObject) {
        let superStateObject = {};
        if (this.supertype) {
            superStateObject = this.supertype.defaultToStateObject(coreObject);
        }
        if (this.stateSchema && this.stateSchema.isComposite()) {
            return _.merge(superStateObject, this.stateSchema.defaultToStateObject(coreObject));
        }
        else {
            return superStateObject;
        }
    }
    // Include state from all composite state schemas up and down the type hierarchy (children overriding parents).
    defaultApplyState(coreObject, stateObject) {
        if (this.supertype) {
            this.supertype.defaultApplyState(coreObject, stateObject);
        }
        if (this.stateSchema && this.stateSchema.isComposite()) {
            this.stateSchema.defaultApplyState(coreObject, stateObject);
        }
    }
    /**
     * Gets an array of IOTypes of the self type and all the supertype ancestors.
     */
    getTypeHierarchy() {
        const array = [];
        let ioType = this; // eslint-disable-line consistent-this, @typescript-eslint/no-this-alias
        while (ioType) {
            array.push(ioType);
            ioType = ioType.supertype;
        }
        return array;
    }
    /**
     * Returns true if this IOType is a subtype of the passed-in type (or if they are the same).
     */
    extends(type) {
        // memory-based implementation OK since this method is only used in assertions
        return this.getTypeHierarchy().includes(type);
    }
    /**
     * Return all the metadata defaults (for the entire IOType hierarchy)
     */
    getAllMetadataDefaults() {
        return _.merge({}, this.supertype ? this.supertype.getAllMetadataDefaults() : {}, this.metadataDefaults);
    }
    /**
     * Return all the data defaults (for the entire IOType hierarchy)
     */
    getAllDataDefaults() {
        return _.merge({}, this.supertype ? this.supertype.getAllDataDefaults() : {}, this.dataDefaults);
    }
    /**
     * This cannot be in stateSchema, because some IOTypes do not have stateSchema instances, but their supertype does.
     */
    isCompositeStateSchema() {
        return this.supertype?.isCompositeStateSchema() || !!this.stateSchema?.compositeSchema;
    }
    /**
     * Return all the apiStateKey option values (for the entire IOType hierarchy)
     * For example:
     *  [ null, null, ['validValues'], null ] if there were three supertypes, and your parent was the only IOType with apiStateKeys
     */
    getAllAPIStateKeyValues(apiStateKeysPerLevel = []) {
        this.supertype && this.supertype.getAllAPIStateKeyValues(apiStateKeysPerLevel);
        apiStateKeysPerLevel.push(this.stateSchema?.apiStateKeys || null);
        return apiStateKeysPerLevel;
    }
    /**
     * See if any IOType up the hierarchy actually supplied apiStateKeys, even in `[]`, meaning "don't opt-in to nested
     * API state.
     */
    apiStateKeysProvided() {
        return this.getAllAPIStateKeyValues().filter(truthy).length === 0;
    }
    /**
     * Return all the apiStateKeys (for the entire IOType hierarchy) in one array.
     */
    getAllAPIStateKeys() {
        return _.concat(...this.getAllAPIStateKeyValues().map(x => x || []));
    }
    /**
     * Get the state object for a PhET-iO Element, but only the entries that should be tracked by the PhET-iO API. See
     * StateSchema.apiStateKeys for details. This implementation sets a global to make sure that nested state also only
     * selects the apiStateKeys for api tracking (PropertyIO<RangeIO> could have validValues of PointIO that shouldn't
     * include non-tracked values of PointIO, if there are any).
     */
    toStateObjectForAPI(coreObject) {
        assert && assert(!GETTING_STATE_FOR_API, 'API state cannot nest due to limitation of the global');
        GETTING_STATE_FOR_API = true;
        assert && assert(API_STATE_NESTED_COUNT === 0, 'not nested before getting API state');
        const stateObjectForAPIOnly = this.toStateObject(coreObject);
        assert && assert(API_STATE_NESTED_COUNT === 0, 'not nested after getting API state');
        GETTING_STATE_FOR_API = false;
        return stateObjectForAPIOnly;
    }
    /**
     * @param stateObject - the stateObject to validate against
     * @param toAssert=false - whether to assert when invalid
     * @param schemaKeysPresentInStateObject=[]
     * @returns if the stateObject is valid or not.
     */
    isStateObjectValid(stateObject, toAssert = false, schemaKeysPresentInStateObject = []) {
        // Set to false when invalid
        let valid = true;
        // make sure the stateObject has everything the schema requires and nothing more
        if (this.stateSchema) {
            const validSoFar = this.stateSchema.checkStateObjectValid(stateObject, toAssert, schemaKeysPresentInStateObject);
            // null as a marker to keep checking up the hierarchy, otherwise we reached our based case because the stateSchema was a value, not a composite
            if (validSoFar !== null) {
                return validSoFar;
            }
        }
        if (this.supertype) {
            return valid && this.supertype.isStateObjectValid(stateObject, toAssert, schemaKeysPresentInStateObject);
        }
        // When we reach the root, make sure there isn't anything in the stateObject that isn't described by a schema
        if (!this.supertype && stateObject && typeof stateObject !== 'string' && !Array.isArray(stateObject)) {
            // Visit the state
            Object.keys(stateObject).forEach(key => {
                const keyValid = schemaKeysPresentInStateObject.includes(key);
                if (!keyValid) {
                    valid = false;
                }
                assert && toAssert && assert(keyValid, `stateObject provided a key that is not in the schema: ${key}`);
            });
            return valid;
        }
        return true;
    }
    /**
     * Assert if the provided stateObject is not valid to this IOType's stateSchema
     */
    validateStateObject(stateObject) {
        this.isStateObjectValid(stateObject, true);
    }
    toString() {
        return this.typeName;
    }
    /**
     * Return an object that indicates the API type, including documentation, methods & signatures, supertypes, etc.
     * The object is intended for serialization via JSON.stringify().
     *
     * This function could be static, but that doesn't work well with the singleton pattern, so keep in on the prototype.
     */
    getAPI() {
        // Enumerate the methods specific to the type (not for supertype).
        const methods = {};
        const methodNames = _.keys(this.methods);
        // iterate over each method
        for (let i = 0; i < methodNames.length; i++) {
            const methodName = methodNames[i];
            const method = this.methods[methodName];
            const m = {
                // Return names for parameter types and return types to prevent loops in type graph
                returnType: method.returnType.typeName,
                parameterTypes: method.parameterTypes.map(mapAPIForType),
                documentation: method.documentation
            };
            // invocableForReadOnlyElements===false is opt-in
            if (method.invocableForReadOnlyElements === false) {
                m.invocableForReadOnlyElements = false;
            }
            methods[methodName] = m;
        }
        const supertype = this.supertype;
        // Return all parts of the API as an object
        const phetioType = {
            methods: methods,
            supertype: supertype ? supertype.typeName : supertype,
            typeName: this.typeName,
            documentation: this.documentation,
            events: this.events,
            metadataDefaults: this.metadataDefaults,
            dataDefaults: this.dataDefaults,
            methodOrder: this.methodOrder
        };
        if (this.stateSchema) {
            phetioType.stateSchema = this.stateSchema.getStateSchemaAPI();
            if (this.stateSchema.apiStateKeys && this.stateSchema.apiStateKeys.length > 0) {
                phetioType.apiStateKeys = this.stateSchema.apiStateKeys;
            }
        }
        // This way we don't have this key unless there are parameterTypes possible (empty array allowed)
        if (this.parameterTypes) {
            phetioType.parameterTypes = this.parameterTypes.map(mapAPIForType);
        }
        return phetioType;
    }
}
const mapAPIForType = (parameterType) => parameterType.typeName;
// default state value
const DEFAULT_STATE = null;
// This must be declared after the class declaration to avoid a circular dependency with PhetioObject.
// @readonly
IOType.ObjectIO = new IOType(TandemConstants.OBJECT_IO_TYPE_NAME, {
    isValidValue: () => true,
    supertype: null,
    documentation: 'The root of the PhET-iO Type hierarchy',
    toStateObject: (coreObject) => {
        if (phet && phet.tandem && phet.tandem.Tandem.VALIDATION) {
            assert && assert(coreObject.tandem, 'coreObject must be PhET-iO object');
            assert && assert(!coreObject.phetioState, `fell back to root serialization state for ${coreObject.tandem.phetioID}. Potential solutions:
         * mark the type as phetioState: false
         * create a custom toStateObject method in your IOType
         * perhaps you have everything right, but forgot to pass in the IOType via phetioType in the constructor`);
        }
        return DEFAULT_STATE;
    },
    fromStateObject: () => {
        throw new Error('ObjectIO.fromStateObject should not be called');
    },
    stateObjectToCreateElementArguments: () => [],
    applyState: _.noop,
    metadataDefaults: TandemConstants.PHET_IO_OBJECT_METADATA_DEFAULTS,
    dataDefaults: {
        initialState: DEFAULT_STATE
    },
    stateSchema: null
});
tandemNamespace.register('IOType', IOType);
//# sourceMappingURL=IOType.js.map