/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 *
 * @format
 */

'use strict';

const _require = require('../Utils'),
  indent = _require.indent;
const _require2 = require('./CppHelpers'),
  IncludeTemplate = _require2.IncludeTemplate,
  generateEventStructName = _require2.generateEventStructName;

// File path -> contents

const FileTemplate = ({events, extraIncludes, headerPrefix}) => `
/**
 * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
 *
 * Do not edit this file as changes may cause incorrect behavior and will be lost
 * once the code is regenerated.
 *
 * ${'@'}generated by codegen project: GenerateEventEmitterCpp.js
 */

${IncludeTemplate({
  headerPrefix,
  file: 'EventEmitters.h',
})}
${[...extraIncludes].join('\n')}

namespace facebook::react {
${events}
} // namespace facebook::react
`;
const ComponentTemplate = ({
  className,
  eventName,
  structName,
  dispatchEventName,
  implementation,
}) => {
  const capture = implementation.includes('$event')
    ? '$event=std::move($event)'
    : '';
  return `
void ${className}EventEmitter::${eventName}(${structName} $event) const {
  dispatchEvent("${dispatchEventName}", [${capture}](jsi::Runtime &runtime) {
    ${implementation}
  });
}
`;
};
const BasicComponentTemplate = ({className, eventName, dispatchEventName}) =>
  `
void ${className}EventEmitter::${eventName}() const {
  dispatchEvent("${dispatchEventName}");
}
`.trim();
function generateSetter(
  variableName,
  propertyName,
  propertyParts,
  usingEvent,
  valueMapper = value => value,
) {
  const eventChain = usingEvent
    ? `$event.${[...propertyParts, propertyName].join('.')}`
    : [propertyParts, propertyName].join('.');
  return `${variableName}.setProperty(runtime, "${propertyName}", ${valueMapper(
    eventChain,
  )});`;
}
function generateObjectSetter(
  variableName,
  propertyName,
  propertyParts,
  typeAnnotation,
  extraIncludes,
  usingEvent,
) {
  return `
{
  auto ${propertyName} = jsi::Object(runtime);
  ${indent(
    generateSetters(
      propertyName,
      typeAnnotation.properties,
      propertyParts.concat([propertyName]),
      extraIncludes,
      usingEvent,
    ),
    2,
  )}
  ${variableName}.setProperty(runtime, "${propertyName}", ${propertyName});
}
`.trim();
}
function setValueAtIndex(
  propertyName,
  indexVariable,
  loopLocalVariable,
  mappingFunction = value => value,
) {
  return `${propertyName}.setValueAtIndex(runtime, ${indexVariable}++, ${mappingFunction(
    loopLocalVariable,
  )});`;
}
function generateArraySetter(
  variableName,
  propertyName,
  propertyParts,
  elementType,
  extraIncludes,
  usingEvent,
) {
  const eventChain = usingEvent
    ? `$event.${[...propertyParts, propertyName].join('.')}`
    : [propertyParts, propertyName].join('.');
  const indexVar = `${propertyName}Index`;
  const innerLoopVar = `${propertyName}Value`;
  return `
    auto ${propertyName} = jsi::Array(runtime, ${eventChain}.size());
    size_t ${indexVar} = 0;
    for (auto ${innerLoopVar} : ${eventChain}) {
      ${handleArrayElementType(
        elementType,
        propertyName,
        indexVar,
        innerLoopVar,
        propertyParts,
        extraIncludes,
        usingEvent,
      )}
    }
    ${variableName}.setProperty(runtime, "${propertyName}", ${propertyName});
  `;
}
function handleArrayElementType(
  elementType,
  propertyName,
  indexVariable,
  loopLocalVariable,
  propertyParts,
  extraIncludes,
  usingEvent,
) {
  switch (elementType.type) {
    case 'BooleanTypeAnnotation':
      return setValueAtIndex(
        propertyName,
        indexVariable,
        loopLocalVariable,
        val => `(bool)${val}`,
      );
    case 'StringTypeAnnotation':
    case 'Int32TypeAnnotation':
    case 'DoubleTypeAnnotation':
    case 'FloatTypeAnnotation':
      return setValueAtIndex(propertyName, indexVariable, loopLocalVariable);
    case 'MixedTypeAnnotation':
      return setValueAtIndex(
        propertyName,
        indexVariable,
        loopLocalVariable,
        val => `jsi::valueFromDynamic(runtime, ${val})`,
      );
    case 'StringEnumTypeAnnotation':
      return setValueAtIndex(
        propertyName,
        indexVariable,
        loopLocalVariable,
        val => `toString(${val})`,
      );
    case 'ObjectTypeAnnotation':
      return convertObjectTypeArray(
        propertyName,
        indexVariable,
        loopLocalVariable,
        propertyParts,
        elementType,
        extraIncludes,
      );
    case 'ArrayTypeAnnotation':
      return convertArrayTypeArray(
        propertyName,
        indexVariable,
        loopLocalVariable,
        propertyParts,
        elementType,
        extraIncludes,
        usingEvent,
      );
    default:
      throw new Error(
        `Received invalid elementType for array ${elementType.type}`,
      );
  }
}
function convertObjectTypeArray(
  propertyName,
  indexVariable,
  loopLocalVariable,
  propertyParts,
  objectTypeAnnotation,
  extraIncludes,
) {
  return `auto ${propertyName}Object = jsi::Object(runtime);
      ${generateSetters(
        `${propertyName}Object`,
        objectTypeAnnotation.properties,
        [].concat([loopLocalVariable]),
        extraIncludes,
        false,
      )}
      ${setValueAtIndex(propertyName, indexVariable, `${propertyName}Object`)}`;
}
function convertArrayTypeArray(
  propertyName,
  indexVariable,
  loopLocalVariable,
  propertyParts,
  eventTypeAnnotation,
  extraIncludes,
  usingEvent,
) {
  if (eventTypeAnnotation.type !== 'ArrayTypeAnnotation') {
    throw new Error(
      `Inconsistent eventTypeAnnotation received. Expected type = 'ArrayTypeAnnotation'; received = ${eventTypeAnnotation.type}`,
    );
  }
  return `auto ${propertyName}Array = jsi::Array(runtime, ${loopLocalVariable}.size());
      size_t ${indexVariable}Internal = 0;
      for (auto ${loopLocalVariable}Internal : ${loopLocalVariable}) {
        ${handleArrayElementType(
          eventTypeAnnotation.elementType,
          `${propertyName}Array`,
          `${indexVariable}Internal`,
          `${loopLocalVariable}Internal`,
          propertyParts,
          extraIncludes,
          usingEvent,
        )}
      }
      ${setValueAtIndex(propertyName, indexVariable, `${propertyName}Array`)}`;
}
function generateSetters(
  parentPropertyName,
  properties,
  propertyParts,
  extraIncludes,
  usingEvent = true,
) {
  const propSetters = properties
    .map(eventProperty => {
      const typeAnnotation = eventProperty.typeAnnotation;
      switch (typeAnnotation.type) {
        case 'BooleanTypeAnnotation':
        case 'StringTypeAnnotation':
        case 'Int32TypeAnnotation':
        case 'DoubleTypeAnnotation':
        case 'FloatTypeAnnotation':
          return generateSetter(
            parentPropertyName,
            eventProperty.name,
            propertyParts,
            usingEvent,
          );
        case 'MixedTypeAnnotation':
          extraIncludes.add('#include <jsi/JSIDynamic.h>');
          return generateSetter(
            parentPropertyName,
            eventProperty.name,
            propertyParts,
            usingEvent,
            prop => `jsi::valueFromDynamic(runtime, ${prop})`,
          );
        case 'StringEnumTypeAnnotation':
          return generateSetter(
            parentPropertyName,
            eventProperty.name,
            propertyParts,
            usingEvent,
            prop => `toString(${prop})`,
          );
        case 'ObjectTypeAnnotation':
          return generateObjectSetter(
            parentPropertyName,
            eventProperty.name,
            propertyParts,
            typeAnnotation,
            extraIncludes,
            usingEvent,
          );
        case 'ArrayTypeAnnotation':
          return generateArraySetter(
            parentPropertyName,
            eventProperty.name,
            propertyParts,
            typeAnnotation.elementType,
            extraIncludes,
            usingEvent,
          );
        default:
          typeAnnotation.type;
          throw new Error(
            `Received invalid event property type ${typeAnnotation.type}`,
          );
      }
    })
    .join('\n');
  return propSetters;
}
function generateEvent(componentName, event, extraIncludes) {
  // This is a gross hack necessary because native code is sending
  // events named things like topChange to JS which is then converted back to
  // call the onChange prop. We should be consistent throughout the system.
  // In order to migrate to this new system we have to support the current
  // naming scheme. We should delete this once we are able to control this name
  // throughout the system.
  const dispatchEventName =
    event.paperTopLevelNameDeprecated != null
      ? event.paperTopLevelNameDeprecated
      : `${event.name[2].toLowerCase()}${event.name.slice(3)}`;
  if (event.typeAnnotation.argument) {
    const implementation = `
    auto $payload = jsi::Object(runtime);
    ${generateSetters(
      '$payload',
      event.typeAnnotation.argument.properties,
      [],
      extraIncludes,
    )}
    return $payload;
  `.trim();
    if (!event.name.startsWith('on')) {
      throw new Error('Expected the event name to start with `on`');
    }
    return ComponentTemplate({
      className: componentName,
      eventName: event.name,
      dispatchEventName,
      structName: generateEventStructName([event.name]),
      implementation,
    });
  }
  return BasicComponentTemplate({
    className: componentName,
    eventName: event.name,
    dispatchEventName,
  });
}
module.exports = {
  generate(
    libraryName,
    schema,
    packageName,
    assumeNonnull = false,
    headerPrefix,
  ) {
    const moduleComponents = Object.keys(schema.modules)
      .map(moduleName => {
        const module = schema.modules[moduleName];
        if (module.type !== 'Component') {
          return;
        }
        const components = module.components;
        // No components in this module
        if (components == null) {
          return null;
        }
        return components;
      })
      .filter(Boolean)
      .reduce((acc, components) => Object.assign(acc, components), {});
    const extraIncludes = new Set();
    const componentEmitters = Object.keys(moduleComponents)
      .map(componentName => {
        const component = moduleComponents[componentName];
        return component.events
          .map(event => generateEvent(componentName, event, extraIncludes))
          .join('\n');
      })
      .join('\n');
    const fileName = 'EventEmitters.cpp';
    const replacedTemplate = FileTemplate({
      events: componentEmitters,
      extraIncludes,
      headerPrefix:
        headerPrefix !== null && headerPrefix !== void 0 ? headerPrefix : '',
    });
    return new Map([[fileName, replacedTemplate]]);
  },
};
