text: // @flow
  import {
    type EnumeratedInstructionMetadata,
    type InstructionOrExpressionScope,
    type EnumeratedInstructionOrExpressionMetadata,
  } from './EnumeratedInstructionOrExpressionMetadata.js';
  import { fuzzyOrEmptyFilter } from '../Utils/FuzzyOrEmptyFilter';
  
  const gd: libGDevelop = global.gd;
  
  const GROUP_DELIMITER = '/';
  
  type ExtensionsExtraInstructions = {
    BuiltinObject?: {
      '': Array<string>,
    },
    Physics2?: {
      'Physics2::Physics2Behavior': Array<string>,
    },
  };
  
  const freeActionsToAddToObject: ExtensionsExtraInstructions = {
    BuiltinObject: {
      '': ['AjoutHasard', 'Create', 'AjoutObjConcern'],
    },
  };
  
  const freeConditionsToAddToObject: ExtensionsExtraInstructions = {
    BuiltinObject: {
      '': [
        'AjoutHasard',
        'AjoutObjConcern',
        'CollisionNP',
        'NbObjet',
        'PickNearest',
        'Distance',
        'SeDirige',
        'EstTourne',
        'SourisSurObjet',
      ],
    },
  };
  
  const freeActionsToAddToBehavior: ExtensionsExtraInstructions = {};
  
  const freeConditionsToAddToBehavior: ExtensionsExtraInstructions = {
    Physics2: {
      'Physics2::Physics2Behavior': ['Physics2::Collision'],
    },
  };
  
  const freeInstructionsToRemove = {
    BuiltinObject: [
      // Note: even if "Create" was added to the object actions for convenience,
      // we also keep it in the list of free actions.
  
      // $FlowFixMe
      ...freeConditionsToAddToObject.BuiltinObject[''],
    ],
    Physics2: [
      // $FlowFixMe
      ...freeConditionsToAddToBehavior.Physics2['Physics2::Physics2Behavior'],
    ],
  };
  
  /**
   * When all instructions are searched, some can be duplicated
   * (on purpose, so that it's easier to find them for users)
   * in both the object instructions and in the free instructions.
   *
   * This removes the duplication, useful for showing results in a list.
   */
  export const deduplicateInstructionsList = (
    list: Array<EnumeratedInstructionMetadata>
  ): Array<EnumeratedInstructionMetadata> => {
    let createFound = false;
    return list.filter(enumerateInstruction => {
      if (enumerateInstruction.type === 'Create') {
        if (createFound) return false;
  
        createFound = true;
      }
  
      return true;
    });
  };
  
  const filterInstructionsToRemove = (
    list: Array<EnumeratedInstructionMetadata>,
    typesToRemove: ?$ReadOnlyArray<string>
  ) => {
    const types = typesToRemove; // Make Flow happy
    if (!types) return list;
  
    return list.filter(metadata => types.indexOf(metadata.type) === -1);
  };
  
  const enumerateExtraBehaviorInstructions = (
    isCondition: boolean,
    extension: gdPlatformExtension,
    behaviorType: string,
    prefix: string,
    scope: InstructionOrExpressionScope
  ) => {
    const extensionName = extension.getName();
  
    const extensionsExtraInstructions = isCondition
      ? freeConditionsToAddToBehavior[extensionName]
      : freeActionsToAddToBehavior[extensionName];
    if (!extensionsExtraInstructions) {
      return [];
    }
  
    const objectExtraInstructions = extensionsExtraInstructions[behaviorType];
    if (!objectExtraInstructions) {
      return [];
    }
  
    const instructionMetadataMap = isCondition
      ? extension.getAllConditions()
      : extension.getAllActions();
  
    return objectExtraInstructions.map(type =>
      enumerateInstruction(prefix, type, instructionMetadataMap.get(type), scope)
    );
  };
  
  const enumerateExtraObjectInstructions = (
    isCondition: boolean,
    extension: gdPlatformExtension,
    objectType: string,
    prefix: string,
    scope: InstructionOrExpressionScope
  ) => {
    const extensionName = extension.getName();
  
    const extensionsExtraInstructions = isCondition
      ? freeConditionsToAddToObject[extensionName]
      : freeActionsToAddToObject[extensionName];
    if (!extensionsExtraInstructions) {
      return [];
    }
  
    const objectExtraInstructions = extensionsExtraInstructions[objectType];
    if (!objectExtraInstructions) {
      return [];
    }
  
    const instructionMetadataMap = isCondition
      ? extension.getAllConditions()
      : extension.getAllActions();
  
    return objectExtraInstructions.map(type =>
      enumerateInstruction(prefix, type, instructionMetadataMap.get(type), scope)
    );
  };
  
  const enumerateInstruction = (
    prefix: string,
    type: string,
    instrMetadata: gdInstructionMetadata,
    scope: InstructionOrExpressionScope
  ): EnumeratedInstructionMetadata => {
    const displayedName = instrMetadata.getFullName();
    const groupName = instrMetadata.getGroup();
    const iconFilename = instrMetadata.getIconFilename();
    const fullGroupName = prefix + groupName;
  
    return {
      type,
      metadata: instrMetadata,
      iconFilename,
      displayedName,
      fullGroupName,
      scope,
      isPrivate: instrMetadata.isPrivate(),
    };
  };
  
  const enumerateExtensionInstructions = (
    prefix: string,
    instructions: gdMapStringInstructionMetadata,
    scope: InstructionOrExpressionScope
  ): Array<EnumeratedInstructionMetadata> => {
    //Get the map containing the metadata of the instructions provided by the extension...
    const instructionsTypes = instructions.keys();
    const allInstructions = [];
  
    //... and add each instruction
    for (let j = 0; j < instructionsTypes.size(); ++j) {
      const type = instructionsTypes.at(j);
      const instrMetadata = instructions.get(type);
      if (instrMetadata.isHidden()) continue;
  
      allInstructions.push(
        enumerateInstruction(prefix, type, instrMetadata, scope)
      );
    }
  
    return allInstructions;
  };
  
  /**
   * List all the instructions available.
   */
  export const enumerateAllInstructions = (
    isCondition: boolean
  ): Array<EnumeratedInstructionMetadata> => {
    let allInstructions = [];
  
    const allExtensions = gd
      .asPlatform(gd.JsPlatform.get())
      .getAllPlatformExtensions();
    for (let i = 0; i < allExtensions.size(); ++i) {
      const extension = allExtensions.at(i);
      const extensionName = extension.getName();
      const allObjectsTypes = extension.getExtensionObjectsTypes();
      const allBehaviorsTypes = extension.getBehaviorsTypes();
  
      let prefix = '';
      if (allObjectsTypes.size() > 0 || allBehaviorsTypes.size() > 0) {
        prefix =
          extensionName === 'BuiltinObject'
            ? 'Common ' +
              (isCondition ? 'conditions' : 'actions') +
              ' for all objects'
            : extension.getFullName();
        prefix += GROUP_DELIMITER;
      }
  
      //Free instructions
      const extensionFreeInstructions = enumerateExtensionInstructions(
        prefix,
        isCondition ? extension.getAllConditions() : extension.getAllActions(),
        {
          extension,
          objectMetadata: undefined,
          behaviorMetadata: undefined,
        }
      );
      allInstructions = [
        ...allInstructions,
        ...filterInstructionsToRemove(
          extensionFreeInstructions,
          freeInstructionsToRemove[extensionName]
        ),
      ];
  
      //Objects instructions:
      for (let j = 0; j < allObjectsTypes.size(); ++j) {
        const objectType = allObjectsTypes.at(j);
        const objectMetadata = extension.getObjectMetadata(objectType);
        const scope = { extension, objectMetadata };
        allInstructions = [
          ...allInstructions,
          ...enumerateExtensionInstructions(
            prefix,
            isCondition
              ? extension.getAllConditionsForObject(objectType)
              : extension.getAllActionsForObject(objectType),
            scope
          ),
          ...enumerateExtraObjectInstructions(
            isCondition,
            extension,
            objectType,
            prefix,
            scope
          ),
        ];
      }
  
      //Behaviors instructions:
      for (let j = 0; j < allBehaviorsTypes.size(); ++j) {
        const behaviorType = allBehaviorsTypes.at(j);
        const behaviorMetadata = extension.getBehaviorMetadata(behaviorType);
        const scope = { extension, behaviorMetadata };
  
        allInstructions = [
          ...allInstructions,
          ...enumerateExtensionInstructions(
            prefix,
            isCondition
              ? extension.getAllConditionsForBehavior(behaviorType)
              : extension.getAllActionsForBehavior(behaviorType),
            scope
          ),
          ...enumerateExtraBehaviorInstructions(
            isCondition,
            extension,
            behaviorType,
            prefix,
            scope
          ),
        ];
      }
    }
  
    return allInstructions;
  };
  
  const orderFirstInstructionsWithoutGroup = (
    allInstructions: Array<EnumeratedInstructionMetadata>
  ) => {
    const noGroupInstructions = allInstructions.filter(
      instructionMetadata => instructionMetadata.fullGroupName.length === 0
    );
    const instructionsWithGroups = allInstructions.filter(
      instructionMetadata => instructionMetadata.fullGroupName.length !== 0
    );
  
    return [...noGroupInstructions, ...instructionsWithGroups];
  };
  
  /**
   * List all the instructions that can be used for the given object,
   * in the given context. This includes instructions for the behaviors
   * attached to the object.
   */
  export const enumerateObjectAndBehaviorsInstructions = (
    isCondition: boolean,
    globalObjectsContainer: gdObjectsContainer,
    objectsContainer: gdObjectsContainer,
    objectName: string
  ): Array<EnumeratedInstructionMetadata> => {
    let allInstructions = [];
  
    const objectType: string = gd.getTypeOfObject(
      globalObjectsContainer,
      objectsContainer,
      objectName,
      true
    );
    const objectBehaviorNames: Array<string> = gd
      .getBehaviorsOfObject(
        globalObjectsContainer,
        objectsContainer,
        objectName,
        true
      )
      .toJSArray();
    const objectBehaviorTypes = new Set(
      objectBehaviorNames.map(behaviorName =>
        gd.getTypeOfBehavior(
          globalObjectsContainer,
          objectsContainer,
          behaviorName,
          false
        )
      )
    );
    const baseObjectType = ''; /* An empty string means the base object */
  
    const allExtensions = gd
      .asPlatform(gd.JsPlatform.get())
      .getAllPlatformExtensions();
    for (let i = 0; i < allExtensions.size(); ++i) {
      const extension = allExtensions.at(i);
      const hasObjectType =
        extension
          .getExtensionObjectsTypes()
          .toJSArray()
          .indexOf(objectType) !== -1;
      const hasBaseObjectType =
        extension
          .getExtensionObjectsTypes()
          .toJSArray()
          .indexOf(baseObjectType) !== -1;
      const behaviorTypes = extension
        .getBehaviorsTypes()
        .toJSArray()
        .filter(behaviorType => objectBehaviorTypes.has(behaviorType));
  
      if (!hasObjectType && !hasBaseObjectType && behaviorTypes.length === 0) {
        continue;
      }
  
      const prefix = '';
  
      //Objects instructions:
      if (objectType !== baseObjectType && hasObjectType) {
        const objectMetadata = extension.getObjectMetadata(objectType);
        const scope = { extension, objectMetadata };
  
        allInstructions = [
          ...allInstructions,
          ...enumerateExtensionInstructions(
            prefix,
            isCondition
              ? extension.getAllConditionsForObject(objectType)
              : extension.getAllActionsForObject(objectType),
            scope
          ),
          ...enumerateExtraObjectInstructions(
            isCondition,
            extension,
            baseObjectType,
            prefix,
            scope
          ),
        ];
      }
  
      if (hasBaseObjectType) {
        const objectMetadata = extension.getObjectMetadata(baseObjectType);
        const scope = { extension, objectMetadata };
  
        allInstructions = [
          ...allInstructions,
          ...enumerateExtensionInstructions(
            prefix,
            isCondition
              ? extension.getAllConditionsForObject(baseObjectType)
              : extension.getAllActionsForObject(baseObjectType),
            scope
          ),
          ...enumerateExtraObjectInstructions(
            isCondition,
            extension,
            baseObjectType,
            prefix,
            scope
          ),
        ];
      }
  
      //Behaviors instructions (show them at the top of the list):
      // eslint-disable-next-line
      behaviorTypes.forEach(behaviorType => {
        const behaviorMetadata = extension.getBehaviorMetadata(behaviorType);
        const scope = { extension, behaviorMetadata };
  
        allInstructions = [
          ...enumerateExtensionInstructions(
            prefix,
            isCondition
              ? extension.getAllConditionsForBehavior(behaviorType)
              : extension.getAllActionsForBehavior(behaviorType),
            scope
          ),
          ...enumerateExtraBehaviorInstructions(
            isCondition,
            extension,
            behaviorType,
            prefix,
            scope
          ),
          ...allInstructions,
        ];
      });
    }
  
    return orderFirstInstructionsWithoutGroup(allInstructions);
  };
  
  /**
   * Enumerate all the instructions that are not directly tied
   * to an object.
   */
  export const enumerateFreeInstructions = (
    isCondition: boolean
  ): Array<EnumeratedInstructionMetadata> => {
    let allFreeInstructions = [];
  
    const allExtensions = gd
      .asPlatform(gd.JsPlatform.get())
      .getAllPlatformExtensions();
    for (let i = 0; i < allExtensions.size(); ++i) {
      const extension = allExtensions.at(i);
      const extensionName: string = extension.getName();
      const allObjectsTypes = extension.getExtensionObjectsTypes();
      const allBehaviorsTypes = extension.getBehaviorsTypes();
  
      let prefix = '';
      if (allObjectsTypes.size() > 0 || allBehaviorsTypes.size() > 0) {
        prefix = extensionName === 'BuiltinObject' ? '' : extension.getFullName();
        prefix += GROUP_DELIMITER;
      }
  
      //Free instructions
      const extensionFreeInstructions = enumerateExtensionInstructions(
        prefix,
        isCondition ? extension.getAllConditions() : extension.getAllActions(),
        {
          extension,
          objectMetadata: undefined,
          behaviorMetadata: undefined,
        }
      );
      allFreeInstructions = [
        ...allFreeInstructions,
        ...filterInstructionsToRemove(
          extensionFreeInstructions,
          freeInstructionsToRemove[extensionName]
        ),
      ];
    }
  
    return allFreeInstructions;
  };
  
  export type InstructionFilteringOptions = {|
    searchText: string,
  |};
  
  export const filterInstructionsList = <
    T: EnumeratedInstructionOrExpressionMetadata
  >(
    list: Array<T>,
    { searchText }: InstructionFilteringOptions
  ): Array<T> => {
    if (!searchText) {
      return list;
    }
  
    const directMatches = [];
    const fuzzyMatches = [];
    const lowercaseSearch = searchText.toLowerCase();
  
    list.forEach(option => {
      const lowerCaseDisplayedName = option.displayedName.toLowerCase();
      const lowerCaseFullGroupName = option.fullGroupName.toLowerCase();
      // favor direct matches first
      if (lowerCaseDisplayedName.includes(lowercaseSearch)) {
        return directMatches.push(option);
      }
      // then add fuzzy matches
      if (
        fuzzyOrEmptyFilter(lowercaseSearch, lowerCaseDisplayedName) ||
        fuzzyOrEmptyFilter(lowercaseSearch, lowerCaseFullGroupName)
      ) {
        return fuzzyMatches.push(option);
      }
    });
  
    return [...directMatches, ...fuzzyMatches];
  };
  
  export const getObjectParameterIndex = (
    instructionMetadata: gdInstructionMetadata
  ) => {
    const parametersCount = instructionMetadata.getParametersCount();
    if (parametersCount >= 1) {
      const firstParameterType = instructionMetadata.getParameter(0).getType();
      if (firstParameterType === 'object') {
        // By convention, all object conditions/actions have the object as first parameter
        return 0;
      }
      if (gd.ParameterMetadata.isObject(firstParameterType)) {
        // Some "free condition/actions" might be considered as "object" instructions, in which
        // case they are taking an object list as fist parameter - which will be identified
        // by gd.ParameterMetadata.isObject
        return 0;
      }
  
      if (
        firstParameterType === 'objectsContext' ||
        firstParameterType === 'currentScene'
      ) {
        if (parametersCount >= 2) {
          const secondParameterType = instructionMetadata
            .getParameter(1)
            .getType();
          if (gd.ParameterMetadata.isObject(secondParameterType)) {
            // Some special action/conditions like "Create", "AjoutHasard" (pick random object) or
            // "AjoutObjConcern" (pick all objects) are "free condition/actions", but are manipulating
            // objects list, so their first parameter is an "objectsContext" or "currentScene",
            // followed by an object parameter.
            return 1;
          }
        }
      }
    }
  
    return -1;
  };
  