import groovy.json.JsonSlurper
import org.gradle.initialization.DefaultSettings
import org.apache.tools.ant.taskdefs.condition.Os

def generatedFileName = "PackageList.java"
def generatedFilePackage = "com.facebook.react"
def generatedFileContentsTemplate = """
package $generatedFilePackage;

import android.app.Application;
import android.content.Context;
import android.content.res.Resources;

import com.facebook.react.ReactPackage;
import com.facebook.react.shell.MainPackageConfig;
import com.facebook.react.shell.MainReactPackage;
import java.util.Arrays;
import java.util.ArrayList;

{{ packageImports }}

public class PackageList {
  private Application application;
  private ReactNativeHost reactNativeHost;
  private MainPackageConfig mConfig;

  public PackageList(ReactNativeHost reactNativeHost) {
    this(reactNativeHost, null);
  }

  public PackageList(Application application) {
    this(application, null);
  }

  public PackageList(ReactNativeHost reactNativeHost, MainPackageConfig config) {
    this.reactNativeHost = reactNativeHost;
    mConfig = config;
  }

  public PackageList(Application application, MainPackageConfig config) {
    this.reactNativeHost = null;
    this.application = application;
    mConfig = config;
  }

  private ReactNativeHost getReactNativeHost() {
    return this.reactNativeHost;
  }

  private Resources getResources() {
    return this.getApplication().getResources();
  }

  private Application getApplication() {
    if (this.reactNativeHost == null) return this.application;
    return this.reactNativeHost.getApplication();
  }

  private Context getApplicationContext() {
    return this.getApplication().getApplicationContext();
  }

  public ArrayList<ReactPackage> getPackages() {
    return new ArrayList<>(Arrays.<ReactPackage>asList(
      new MainReactPackage(mConfig){{ packageClassInstances }}
    ));
  }
}
"""

def cmakeTemplate = """# This code was generated by [React Native CLI](https://www.npmjs.com/package/@react-native-community/cli)

cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)

{{ libraryIncludes }}

set(AUTOLINKED_LIBRARIES
  {{ libraryModules }}
)
"""

def rncliCppTemplate = """/**
 * This code was generated by [React Native CLI](https://www.npmjs.com/package/@react-native-community/cli).
 *
 * Do not edit this file as changes may cause incorrect behavior and will be lost
 * once the code is regenerated.
 *
 */

#include "rncli.h"
{{ rncliCppIncludes }}

namespace facebook {
namespace react {

{{ rncliReactLegacyComponentNames }}

std::shared_ptr<TurboModule> rncli_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams &params) {
{{ rncliCppModuleProviders }}
  return nullptr;
}

void rncli_registerProviders(std::shared_ptr<ComponentDescriptorProviderRegistry const> providerRegistry) {
{{ rncliCppComponentDescriptors }}
{{ rncliReactLegacyComponentDescriptors }}
  return;
}

} // namespace react
} // namespace facebook
"""

def rncliHTemplate = """/**
 * This code was generated by [React Native CLI](https://www.npmjs.com/package/@react-native-community/cli).
 *
 * Do not edit this file as changes may cause incorrect behavior and will be lost
 * once the code is regenerated.
 *
 */

#pragma once

#include <ReactCommon/JavaTurboModule.h>
#include <ReactCommon/TurboModule.h>
#include <jsi/jsi.h>
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>

namespace facebook {
namespace react {

std::shared_ptr<TurboModule> rncli_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams &params);
void rncli_registerProviders(std::shared_ptr<ComponentDescriptorProviderRegistry const> providerRegistry);

} // namespace react
} // namespace facebook
"""

class ReactNativeModules {
  private Logger logger
  private String packageName
  private File root
  private ArrayList<HashMap<String, String>> reactNativeModules
  private ArrayList<String> unstable_reactLegacyComponentNames
  private HashMap<String, ArrayList> reactNativeModulesBuildVariants
  private String reactNativeVersion

  private static String LOG_PREFIX = ":ReactNative:"

  ReactNativeModules(Logger logger, File root) {
    this.logger = logger
    this.root = root

    def (nativeModules, reactNativeModulesBuildVariants, androidProject, reactNativeVersion) = this.getReactNativeConfig()
    this.reactNativeModules = nativeModules
    this.reactNativeModulesBuildVariants = reactNativeModulesBuildVariants
    this.packageName = androidProject["packageName"]
    this.unstable_reactLegacyComponentNames = androidProject["unstable_reactLegacyComponentNames"]
    this.reactNativeVersion = reactNativeVersion
  }

  /**
   * Include the react native modules android projects and specify their project directory
   */
  void addReactNativeModuleProjects(DefaultSettings defaultSettings) {
    reactNativeModules.forEach { reactNativeModule ->
      String nameCleansed = reactNativeModule["nameCleansed"]
      String androidSourceDir = reactNativeModule["androidSourceDir"]
      defaultSettings.include(":${nameCleansed}")
      defaultSettings.project(":${nameCleansed}").projectDir = new File("${androidSourceDir}")
    }
  }

  /**
   * Adds the react native modules as dependencies to the users `app` project
   */
  void addReactNativeModuleDependencies(Project appProject) {
    reactNativeModules.forEach { reactNativeModule ->
      def nameCleansed = reactNativeModule["nameCleansed"]
      def dependencyConfiguration = reactNativeModule["dependencyConfiguration"]
      appProject.dependencies {
        if (reactNativeModulesBuildVariants.containsKey(nameCleansed)) {
          reactNativeModulesBuildVariants
            .get(nameCleansed)
            .forEach { buildVariant ->
              if(dependencyConfiguration != null) {
                "${buildVariant}${dependencyConfiguration}"
              } else {
                "${buildVariant}Implementation" project(path: ":${nameCleansed}")
              }
            }
        } else {
          if(dependencyConfiguration != null) {
            "${dependencyConfiguration}"
          } else {
             implementation project(path: ":${nameCleansed}")
          }
        }
      }
    }
  }

  /**
   * Code-gen a java file with all the detected ReactNativePackage instances automatically added
   *
   * @param outputDir
   * @param generatedFileName
   * @param generatedFileContentsTemplate
   */
  void generatePackagesFile(File outputDir, String generatedFileName, String generatedFileContentsTemplate) {
    ArrayList<HashMap<String, String>> packages = this.reactNativeModules
    String packageName = this.packageName

    String packageImports = ""
    String packageClassInstances = ""

    if (packages.size() > 0) {
      def interpolateDynamicValues = {
        it
                // Before adding the package replacement mechanism,
                // BuildConfig and R classes were imported automatically
                // into the scope of the file. We want to replace all
                // non-FQDN references to those classes with the package name
                // of the MainApplication.
                //
                // We want to match "R" or "BuildConfig":
                //  - new Package(R.string…),
                //  - Module.configure(BuildConfig);
                //    ^ hence including (BuildConfig|R)
                // but we don't want to match "R":
                //  - new Package(getResources…),
                //  - new PackageR…,
                //  - new Royal…,
                //    ^ hence excluding \w before and after matches
                // and "BuildConfig" that has FQDN reference:
                //  - Module.configure(com.acme.BuildConfig);
                //    ^ hence excluding . before the match.
                .replaceAll(~/([^.\w])(BuildConfig|R)([^\w])/, {
                  wholeString, prefix, className, suffix ->
                    "${prefix}${packageName}.${className}${suffix}"
                })
      }
      packageImports = packages.collect {
        "// ${it.name}\n${interpolateDynamicValues(it.packageImportPath)}"
      }.join('\n')
      packageClassInstances = ",\n      " + packages.collect {
        interpolateDynamicValues(it.packageInstance)
      }.join(",\n      ")
    }

    String generatedFileContents = generatedFileContentsTemplate
      .replace("{{ packageImports }}", packageImports)
      .replace("{{ packageClassInstances }}", packageClassInstances)

    outputDir.mkdirs()
    final FileTreeBuilder treeBuilder = new FileTreeBuilder(outputDir)
    treeBuilder.file(generatedFileName).newWriter().withWriter { w ->
      w << generatedFileContents
    }
  }

  void generateCmakeFile(File outputDir, String generatedFileName, String generatedFileContentsTemplate) {
    ArrayList<HashMap<String, String>> packages = this.reactNativeModules
    String packageName = this.packageName
    String codegenLibPrefix = "react_codegen_"
    String libraryIncludes = ""
    String libraryModules = ""

    if (packages.size() > 0) {
      libraryIncludes = packages.collect {
        if (it.libraryName != null && it.cmakeListsPath != null) {
          // If user provided a custom cmakeListsPath, let's honor it.
          String nativeFolderPath = it.cmakeListsPath.replace("CMakeLists.txt", "")
          "add_subdirectory($nativeFolderPath ${it.libraryName}_autolinked_build)"
        } else {
          null
        }
      }.minus(null).join('\n')
      libraryModules = packages.collect {
        it.libraryName ? "${codegenLibPrefix}${it.libraryName}" : null
      }.minus(null).join('\n  ')
    }

    String generatedFileContents = generatedFileContentsTemplate
      .replace("{{ libraryIncludes }}", libraryIncludes)
      .replace("{{ libraryModules }}", libraryModules)

    outputDir.mkdirs()
    final FileTreeBuilder treeBuilder = new FileTreeBuilder(outputDir)
    treeBuilder.file(generatedFileName).newWriter().withWriter { w ->
      w << generatedFileContents
    }
  }

  void generateRncliCpp(File outputDir, String generatedFileName, String generatedFileContentsTemplate) {
    ArrayList<HashMap<String, String>> packages = this.reactNativeModules
    ArrayList<String> unstable_reactLegacyComponentNames = this.unstable_reactLegacyComponentNames
    String rncliCppIncludes = ""
    String rncliCppModuleProviders = ""
    String rncliCppComponentDescriptors = ""
    String rncliReactLegacyComponentDescriptors = ""
    String rncliReactLegacyComponentNames = ""
    String codegenComponentDescriptorsHeaderFile = "ComponentDescriptors.h"
    String codegenReactComponentsDir = "react/renderer/components"

    if (packages.size() > 0) {
      rncliCppIncludes = packages.collect {
        if (!it.libraryName) {
          return null
        }
        def result = "#include <${it.libraryName}.h>"
        if (it.componentDescriptors && it.componentDescriptors.size() > 0) {
          result += "\n#include <${codegenReactComponentsDir}/${it.libraryName}/${codegenComponentDescriptorsHeaderFile}>"
        }
        result
      }.minus(null).join('\n')
      rncliCppModuleProviders = packages.collect {
        it.libraryName ? """  auto module_${it.libraryName} = ${it.libraryName}_ModuleProvider(moduleName, params);
  if (module_${it.libraryName} != nullptr) {
    return module_${it.libraryName};
  }""" : null
      }.minus(null).join("\n")
      rncliCppComponentDescriptors = packages.collect {
        def result = ""
        if (it.componentDescriptors && it.componentDescriptors.size() > 0) {
          result += it.componentDescriptors.collect {
            "  providerRegistry->add(concreteComponentDescriptorProvider<${it}>());"
          }.join('\n')
        }
        result
      }.join("\n")
    }

    rncliReactLegacyComponentDescriptors = unstable_reactLegacyComponentNames.collect {
      "  providerRegistry->add(concreteComponentDescriptorProvider<UnstableLegacyViewManagerInteropComponentDescriptor<${it}>>());"
    }.join("\n")
    rncliReactLegacyComponentNames = unstable_reactLegacyComponentNames.collect {
      "extern const char ${it}[] = \"${it}\";"
    }.join("\n")
    if (unstable_reactLegacyComponentNames && unstable_reactLegacyComponentNames.size() > 0) {
      rncliCppIncludes += "\n#include <react/renderer/components/legacyviewmanagerinterop/UnstableLegacyViewManagerInteropComponentDescriptor.h>"
    }

    String generatedFileContents = generatedFileContentsTemplate
      .replace("{{ rncliCppIncludes }}", rncliCppIncludes)
      .replace("{{ rncliCppModuleProviders }}", rncliCppModuleProviders)
      .replace("{{ rncliCppComponentDescriptors }}", rncliCppComponentDescriptors)
      .replace("{{ rncliReactLegacyComponentDescriptors }}", rncliReactLegacyComponentDescriptors)
      .replace("{{ rncliReactLegacyComponentNames }}", rncliReactLegacyComponentNames)

    outputDir.mkdirs()
    final FileTreeBuilder treeBuilder = new FileTreeBuilder(outputDir)
    treeBuilder.file(generatedFileName).newWriter().withWriter { w ->
      w << generatedFileContents
    }
  }

  void generateRncliH(File outputDir, String generatedFileName, String generatedFileContentsTemplate) {
    String generatedFileContents = generatedFileContentsTemplate

    outputDir.mkdirs()
    final FileTreeBuilder treeBuilder = new FileTreeBuilder(outputDir)
    treeBuilder.file(generatedFileName).newWriter().withWriter { w ->
      w << generatedFileContents
    }
  }

  /**
   * Runs a specified command using Runtime exec() in a specified directory.
   * Throws when the command result is empty.
   */
  String getCommandOutput(String[] command, File directory) {
    try {
      def output = ""
      def cmdProcess = Runtime.getRuntime().exec(command, null, directory)
      def bufferedReader = new BufferedReader(new InputStreamReader(cmdProcess.getInputStream()))
      def buff = ""
      def readBuffer = new StringBuffer()
      while ((buff = bufferedReader.readLine()) != null) {
        readBuffer.append(buff)
      }
      output = readBuffer.toString()
      if (!output) {
        this.logger.error("${LOG_PREFIX}Unexpected empty result of running '${command}' command.")
        def bufferedErrorReader = new BufferedReader(new InputStreamReader(cmdProcess.getErrorStream()))
        def errBuff = ""
        def readErrorBuffer = new StringBuffer()
        while ((errBuff = bufferedErrorReader.readLine()) != null) {
          readErrorBuffer.append(errBuff)
        }
        throw new Exception(readErrorBuffer.toString())
      }
      return output
    } catch (Exception exception) {
      this.logger.error("${LOG_PREFIX}Running '${command}' command failed.")
      throw exception
    }
  }

  /**
   * Runs a process to call the React Native CLI Config command and parses the output
   */
  ArrayList<HashMap<String, String>> getReactNativeConfig() {
    if (this.reactNativeModules != null) return this.reactNativeModules

    ArrayList<HashMap<String, String>> reactNativeModules = new ArrayList<HashMap<String, String>>()
    HashMap<String, ArrayList> reactNativeModulesBuildVariants = new HashMap<String, ArrayList>()

    /**
     * Resolve the CLI location from Gradle file
     *
     * @todo: Sometimes Gradle can be called outside of the JavaScript hierarchy (-p flag) which
     * will fail to resolve the script and the dependencies. We should resolve this soon.
     *
     * @todo: `fastlane` has been reported to not work too.
     */
    def cliResolveScript = "try {console.log(require('@react-native-community/cli').bin);} catch (e) {console.log(require('react-native/cli').bin);}"
    String[] nodeCommand = ["node", "-e", cliResolveScript]
    def cliPath = this.getCommandOutput(nodeCommand, this.root)

    String[] reactNativeConfigCommand = ["node", cliPath, "config"]
    def reactNativeConfigOutput = this.getCommandOutput(reactNativeConfigCommand, this.root)

    def json
    try {
      json = new JsonSlurper().parseText(reactNativeConfigOutput)
    } catch (Exception exception) {
      throw new Exception("Calling `${reactNativeConfigCommand}` finished with an exception. Error message: ${exception.toString()}. Output: ${reactNativeConfigOutput}");
    }
    def dependencies = json["dependencies"]
    def project = json["project"]["android"]
    def reactNativeVersion = json["version"]

    if (project == null) {
      throw new Exception("React Native CLI failed to determine Android project configuration. This is likely due to misconfiguration. Config output:\n${json.toMapString()}")
    }

    def engine = new groovy.text.SimpleTemplateEngine()

    dependencies.each { name, value ->
      def platformsConfig = value["platforms"];
      def androidConfig = platformsConfig["android"]

      if (androidConfig != null && androidConfig["sourceDir"] != null) {
        this.logger.info("${LOG_PREFIX}Automatically adding native module '${name}'")

        HashMap reactNativeModuleConfig = new HashMap<String, String>()
        def nameCleansed = name.replaceAll('[~*!\'()]+', '_').replaceAll('^@([\\w-.]+)/', '$1_')
        reactNativeModuleConfig.put("name", name)
        reactNativeModuleConfig.put("nameCleansed", nameCleansed)
        reactNativeModuleConfig.put("androidSourceDir", androidConfig["sourceDir"])
        reactNativeModuleConfig.put("packageInstance", androidConfig["packageInstance"])
        reactNativeModuleConfig.put("packageImportPath", androidConfig["packageImportPath"])
        reactNativeModuleConfig.put("libraryName", androidConfig["libraryName"])
        reactNativeModuleConfig.put("componentDescriptors", androidConfig["componentDescriptors"])
        reactNativeModuleConfig.put("cmakeListsPath", androidConfig["cmakeListsPath"])

        if (androidConfig["buildTypes"] && !androidConfig["buildTypes"].isEmpty()) {
          reactNativeModulesBuildVariants.put(nameCleansed, androidConfig["buildTypes"])
        }
        if(androidConfig.containsKey("dependencyConfiguration")) {
          reactNativeModuleConfig.put("dependencyConfiguration", androidConfig["dependencyConfiguration"])
        } else if (project.containsKey("dependencyConfiguration")) {
          def bindings = ["dependencyName": nameCleansed]
          def template = engine.createTemplate(project["dependencyConfiguration"]).make(bindings)

          reactNativeModuleConfig.put("dependencyConfiguration", template.toString())
        }

        this.logger.trace("${LOG_PREFIX}'${name}': ${reactNativeModuleConfig.toMapString()}")

        reactNativeModules.add(reactNativeModuleConfig)
      } else {
        this.logger.info("${LOG_PREFIX}Skipping native module '${name}'")
      }
    }

    return [reactNativeModules, reactNativeModulesBuildVariants, json["project"]["android"], reactNativeVersion];
  }
}


/*
 * Sometimes Gradle can be called outside of JavaScript hierarchy. Detect the directory
 * where build files of an active project are located.
 */
def projectRoot = rootProject.projectDir

def autoModules = new ReactNativeModules(logger, projectRoot)

def reactNativeVersionRequireNewArchEnabled(autoModules) {
    def rnVersion = autoModules.reactNativeVersion
    def regexPattern = /^(\d+)\.(\d+)\.(\d+)(?:-(\w+(?:[-.]\d+)?))?$/

    if (rnVersion =~ regexPattern) {
        def result = (rnVersion =~ regexPattern).findAll().first()

        def major = result[1].toInteger()
        if (major > 0 && major < 1000) {
            return true
        }
    }
    return false
}

/** -----------------------
 *    Exported Extensions
 * ------------------------ */

ext.applyNativeModulesSettingsGradle = { DefaultSettings defaultSettings ->
  autoModules.addReactNativeModuleProjects(defaultSettings)
}

ext.applyNativeModulesAppBuildGradle = { Project project ->
  autoModules.addReactNativeModuleDependencies(project)

  def generatedSrcDir = new File(buildDir, "generated/rncli/src/main/java")
  def generatedCodeDir = new File(generatedSrcDir, generatedFilePackage.replace('.', '/'))
  def generatedJniDir = new File(buildDir, "generated/rncli/src/main/jni")

  task generatePackageList {
    doLast {
      autoModules.generatePackagesFile(generatedCodeDir, generatedFileName, generatedFileContentsTemplate)
    }
  }

  task generateNewArchitectureFiles {
    doLast {
      autoModules.generateCmakeFile(generatedJniDir, "Android-rncli.cmake", cmakeTemplate)
      autoModules.generateRncliCpp(generatedJniDir, "rncli.cpp", rncliCppTemplate)
      autoModules.generateRncliH(generatedJniDir, "rncli.h", rncliHTemplate)
    }
  }

  preBuild.dependsOn generatePackageList
  def isNewArchEnabled = (project.hasProperty("newArchEnabled") && project.newArchEnabled == "true") ||
    reactNativeVersionRequireNewArchEnabled(autoModules)
  if (isNewArchEnabled) {
    preBuild.dependsOn generateNewArchitectureFiles
  }

  android {
    sourceSets {
      main {
        java {
          srcDirs += generatedSrcDir
        }
      }
    }
  }
}
