/**
 * 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.
 *
 * @flow strict
 * @format
 */

'use strict';

import type {
  NamedShape,
  NativeModuleEnumMap,
  NativeModuleFunctionTypeAnnotation,
  NativeModuleParamTypeAnnotation,
  NativeModulePropertyShape,
  NativeModuleTypeAnnotation,
  Nullable,
  SchemaType,
} from '../../CodegenSchema';
import type {AliasResolver} from './Utils';

const {unwrapNullable} = require('../../parsers/parsers-commons');
const {createAliasResolver, getModules} = require('./Utils');

type FilesOutput = Map<string, string>;

const HostFunctionTemplate = ({
  hasteModuleName,
  methodName,
  returnTypeAnnotation,
  args,
}: $ReadOnly<{
  hasteModuleName: string,
  methodName: string,
  returnTypeAnnotation: Nullable<NativeModuleTypeAnnotation>,
  args: Array<string>,
}>) => {
  const isNullable = returnTypeAnnotation.type === 'NullableTypeAnnotation';
  const isVoid = returnTypeAnnotation.type === 'VoidTypeAnnotation';
  const methodCallArgs = ['    rt', ...args].join(',\n    ');
  const methodCall = `static_cast<${hasteModuleName}CxxSpecJSI *>(&turboModule)->${methodName}(\n${methodCallArgs}\n  )`;

  return `static jsi::Value __hostFunction_${hasteModuleName}CxxSpecJSI_${methodName}(jsi::Runtime &rt, TurboModule &turboModule, const jsi::Value* args, size_t count) {${
    isVoid
      ? `\n  ${methodCall};`
      : isNullable
      ? `\n  auto result = ${methodCall};`
      : ''
  }
  return ${
    isVoid
      ? 'jsi::Value::undefined()'
      : isNullable
      ? 'result ? jsi::Value(std::move(*result)) : jsi::Value::null()'
      : methodCall
  };
}`;
};

const ModuleTemplate = ({
  hasteModuleName,
  hostFunctions,
  moduleName,
  methods,
}: $ReadOnly<{
  hasteModuleName: string,
  hostFunctions: $ReadOnlyArray<string>,
  moduleName: string,
  methods: $ReadOnlyArray<$ReadOnly<{methodName: string, paramCount: number}>>,
}>) => {
  return `${hostFunctions.join('\n')}

${hasteModuleName}CxxSpecJSI::${hasteModuleName}CxxSpecJSI(std::shared_ptr<CallInvoker> jsInvoker)
  : TurboModule("${moduleName}", jsInvoker) {
${methods
  .map(({methodName, paramCount}) => {
    return `  methodMap_["${methodName}"] = MethodMetadata {${paramCount}, __hostFunction_${hasteModuleName}CxxSpecJSI_${methodName}};`;
  })
  .join('\n')}
}`;
};

const FileTemplate = ({
  libraryName,
  modules,
}: $ReadOnly<{
  libraryName: string,
  modules: string,
}>) => {
  return `/**
 * 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: GenerateModuleCpp.js
 */

#include "${libraryName}JSI.h"

namespace facebook::react {

${modules}


} // namespace facebook::react
`;
};

type Param = NamedShape<Nullable<NativeModuleParamTypeAnnotation>>;

function serializeArg(
  moduleName: string,
  arg: Param,
  index: number,
  resolveAlias: AliasResolver,
  enumMap: NativeModuleEnumMap,
): string {
  const {typeAnnotation: nullableTypeAnnotation, optional} = arg;
  const [typeAnnotation, nullable] =
    unwrapNullable<NativeModuleParamTypeAnnotation>(nullableTypeAnnotation);

  let realTypeAnnotation = typeAnnotation;
  if (realTypeAnnotation.type === 'TypeAliasTypeAnnotation') {
    realTypeAnnotation = resolveAlias(realTypeAnnotation.name);
  }

  function wrap(callback: (val: string) => string) {
    const val = `args[${index}]`;
    const expression = callback(val);

    // param?: T
    if (optional && !nullable) {
      // throw new Error('are we hitting this case? ' + moduleName);
      return `count <= ${index} || ${val}.isUndefined() ? std::nullopt : std::make_optional(${expression})`;
    }

    // param: ?T
    // param?: ?T
    if (nullable || optional) {
      return `count <= ${index} || ${val}.isNull() || ${val}.isUndefined() ? std::nullopt : std::make_optional(${expression})`;
    }

    // param: T
    return `count <= ${index} ? throw jsi::JSError(rt, "Expected argument in position ${index} to be passed") : ${expression}`;
  }

  switch (realTypeAnnotation.type) {
    case 'ReservedTypeAnnotation':
      switch (realTypeAnnotation.name) {
        case 'RootTag':
          return wrap(val => `${val}.asNumber()`);
        default:
          (realTypeAnnotation.name: empty);
          throw new Error(
            `Unknown prop type for "${arg.name}, found: ${realTypeAnnotation.name}"`,
          );
      }
    case 'StringTypeAnnotation':
      return wrap(val => `${val}.asString(rt)`);
    case 'StringLiteralTypeAnnotation':
      return wrap(val => `${val}.asString(rt)`);
    case 'StringLiteralUnionTypeAnnotation':
      return wrap(val => `${val}.asString(rt)`);
    case 'BooleanTypeAnnotation':
      return wrap(val => `${val}.asBool()`);
    case 'EnumDeclaration':
      switch (realTypeAnnotation.memberType) {
        case 'NumberTypeAnnotation':
          return wrap(val => `${val}.asNumber()`);
        case 'StringTypeAnnotation':
          return wrap(val => `${val}.asString(rt)`);
        default:
          throw new Error(
            `Unknown enum type for "${arg.name}, found: ${realTypeAnnotation.type}"`,
          );
      }
    case 'NumberTypeAnnotation':
      return wrap(val => `${val}.asNumber()`);
    case 'FloatTypeAnnotation':
      return wrap(val => `${val}.asNumber()`);
    case 'DoubleTypeAnnotation':
      return wrap(val => `${val}.asNumber()`);
    case 'Int32TypeAnnotation':
      return wrap(val => `${val}.asNumber()`);
    case 'NumberLiteralTypeAnnotation':
      return wrap(val => `${val}.asNumber()`);
    case 'ArrayTypeAnnotation':
      return wrap(val => `${val}.asObject(rt).asArray(rt)`);
    case 'FunctionTypeAnnotation':
      return wrap(val => `${val}.asObject(rt).asFunction(rt)`);
    case 'GenericObjectTypeAnnotation':
      return wrap(val => `${val}.asObject(rt)`);
    case 'UnionTypeAnnotation':
      switch (typeAnnotation.memberType) {
        case 'NumberTypeAnnotation':
          return wrap(val => `${val}.asNumber()`);
        case 'ObjectTypeAnnotation':
          return wrap(val => `${val}.asObject(rt)`);
        case 'StringTypeAnnotation':
          return wrap(val => `${val}.asString(rt)`);
        default:
          throw new Error(
            `Unsupported union member type for param  "${arg.name}, found: ${realTypeAnnotation.memberType}"`,
          );
      }
    case 'ObjectTypeAnnotation':
      return wrap(val => `${val}.asObject(rt)`);
    case 'MixedTypeAnnotation':
      return wrap(val => `jsi::Value(rt, ${val})`);
    default:
      (realTypeAnnotation.type: empty);
      throw new Error(
        `Unknown prop type for "${arg.name}, found: ${realTypeAnnotation.type}"`,
      );
  }
}

function serializePropertyIntoHostFunction(
  moduleName: string,
  hasteModuleName: string,
  property: NativeModulePropertyShape,
  resolveAlias: AliasResolver,
  enumMap: NativeModuleEnumMap,
): string {
  const [propertyTypeAnnotation] =
    unwrapNullable<NativeModuleFunctionTypeAnnotation>(property.typeAnnotation);

  return HostFunctionTemplate({
    hasteModuleName,
    methodName: property.name,
    returnTypeAnnotation: propertyTypeAnnotation.returnTypeAnnotation,
    args: propertyTypeAnnotation.params.map((p, i) =>
      serializeArg(moduleName, p, i, resolveAlias, enumMap),
    ),
  });
}

module.exports = {
  generate(
    libraryName: string,
    schema: SchemaType,
    packageName?: string,
    assumeNonnull: boolean = false,
    headerPrefix?: string,
  ): FilesOutput {
    const nativeModules = getModules(schema);

    const modules = Object.keys(nativeModules)
      .map((hasteModuleName: string) => {
        const nativeModule = nativeModules[hasteModuleName];
        const {
          aliasMap,
          enumMap,
          spec: {methods},
          moduleName,
        } = nativeModule;
        const resolveAlias = createAliasResolver(aliasMap);
        const hostFunctions = methods.map(property =>
          serializePropertyIntoHostFunction(
            moduleName,
            hasteModuleName,
            property,
            resolveAlias,
            enumMap,
          ),
        );

        return ModuleTemplate({
          hasteModuleName,
          hostFunctions,
          moduleName,
          methods: methods.map(
            ({name: propertyName, typeAnnotation: nullableTypeAnnotation}) => {
              const [{params}] = unwrapNullable(nullableTypeAnnotation);
              return {
                methodName: propertyName,
                paramCount: params.length,
              };
            },
          ),
        });
      })
      .join('\n');

    const fileName = `${libraryName}JSI-generated.cpp`;
    const replacedTemplate = FileTemplate({
      modules,
      libraryName,
    });
    return new Map([[fileName, replacedTemplate]]);
  },
};
