#!/usr/bin/env node
/**
 * 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
 * @noflow
 * @oncall react_native
 */

/**
 * Generates ReactNestedScrollView.java and ReactNestedScrollViewManager.kt from
 * ReactScrollView.java and ReactScrollViewManager.kt respectively.
 *
 * This script creates variants that use NestedScrollView instead of ScrollView
 * for experimentation purposes.
 *
 * Usage:
 *     node generate-nested-scroll-view.js [--verify]
 *
 * Options:
 *     --verify    Check if generated files are up-to-date (exit 1 if not)
 */

'use strict';

const crypto = require('crypto');
const fs = require('fs');
const path = require('path');

// SignedSource token - this placeholder gets replaced with the actual hash
// Use string concatenation to avoid marking THIS script as generated
const SIGNING_TOKEN = '<<SignedSource::*O*zOeWoEQle#+L!plEphiEmie@IsG>>';
const GENERATED_ANNOTATION = '@' + 'generated';

/**
 * Sign the file content by replacing the signing token with a SignedSource hash.
 */
function signFileContent(content) {
  // Compute MD5 hash with the token in place
  const md5Hash = crypto
    .createHash('md5')
    .update(content, 'utf8')
    .digest('hex');

  // Replace the token with the actual signature
  return content.replace(SIGNING_TOKEN, `SignedSource<<${md5Hash}>>`);
}

/**
 * Get the signing token placeholder to embed in the file.
 */
function getSigningToken() {
  return SIGNING_TOKEN;
}

/**
 * Generate the header comment for a generated file.
 */
function generatedHeader(sourceFile) {
  return `/*
 * 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.
 *
 * ${GENERATED_ANNOTATION} ${getSigningToken()}
 */

/**
 * THIS FILE IS GENERATED - DO NOT EDIT DIRECTLY
 * Source: ${sourceFile}
 * Run: node generate-nested-scroll-view.js
 */

`;
}

// Regex pattern for replacing ReactScrollView with ReactNestedScrollView
// Excludes: Helper, CommandHelper, Accessible, Accessibility, ScrollState
const REACT_SCROLL_VIEW_PATTERN =
  /ReactScrollView(?!Helper|CommandHelper|Accessible|Accessibility|ScrollState)/g;

// Regex pattern for matching the original copyright header
const COPYRIGHT_HEADER_PATTERN = /\/\*\s*\n\s*\* Copyright.*?\*\/\s*\n/s;

/**
 * Replace ReactScrollView with ReactNestedScrollView in content.
 */
function replaceClassNames(content) {
  return content.replace(REACT_SCROLL_VIEW_PATTERN, 'ReactNestedScrollView');
}

/**
 * Replace the original copyright header with the generated header.
 */
function replaceCopyrightHeader(content, sourceFile) {
  return content.replace(COPYRIGHT_HEADER_PATTERN, generatedHeader(sourceFile));
}

/**
 * Transform ReactScrollView.java to ReactNestedScrollView.java
 */
function transformScrollView(content) {
  // Replace import
  content = content.replace(
    'import android.widget.ScrollView;',
    'import androidx.core.widget.NestedScrollView;',
  );

  // Replace standalone ScrollView with NestedScrollView (not when part of another word)
  // This handles: "extends ScrollView", "ScrollView.class", etc.
  // But NOT: ReactScrollView, NestedScrollView, ScrollViewHelper, etc.
  content = content.replace(
    /(?<![A-Za-z])ScrollView(?!Helper|Manager|CommandHelper|Interface|Accessible|Accessibility)/g,
    'NestedScrollView',
  );

  // Replace ReactScrollView with ReactNestedScrollView
  content = replaceClassNames(content);

  // Fix visibility differences between ScrollView and NestedScrollView:
  // NestedScrollView.onAttachedToWindow() is public, not protected
  content = content.replace(
    'protected void onAttachedToWindow()',
    'public void onAttachedToWindow()',
  );

  // Make the class package-private (remove public) to keep it internal
  content = content.replace(
    'public class ReactNestedScrollView',
    'class ReactNestedScrollView',
  );

  // Remove original copyright header and add generated header
  content = replaceCopyrightHeader(content, 'ReactScrollView.java');

  return content;
}

/**
 * Transform ReactScrollViewManager.kt to ReactNestedScrollViewManager.kt
 */
function transformViewManager(content) {
  // Replace ReactScrollView with ReactNestedScrollView
  content = replaceClassNames(content);

  // Keep the same REACT_CLASS name so this is a drop-in replacement for A/B testing
  // (no JS changes needed - both managers register as "RCTScrollView")

  // Make the class internal to keep it out of the public API
  content = content.replace(
    'public open class ReactNestedScrollViewManager',
    'internal open class ReactNestedScrollViewManager',
  );

  // Remove original copyright header and add generated header
  content = replaceCopyrightHeader(content, 'ReactScrollViewManager.kt');

  return content;
}

/**
 * Generate a file from source using the given transform function.
 * Returns true if successful (or if verify mode and files match).
 */
function generateFile(sourcePath, outputPath, transformFn, verify) {
  if (!fs.existsSync(sourcePath)) {
    console.error(`Error: Source file not found: ${sourcePath}`);
    return false;
  }

  const content = fs.readFileSync(sourcePath, 'utf8');
  const transformed = transformFn(content);

  // Sign the file content (replaces token with MD5 hash)
  const signed = signFileContent(transformed);

  if (verify) {
    if (!fs.existsSync(outputPath)) {
      console.error(`FAIL: Generated file does not exist: ${outputPath}`);
      return false;
    }

    const existing = fs.readFileSync(outputPath, 'utf8');

    if (existing !== signed) {
      const sourceDir =
        '~/fbsource/xplat/js/react-native-github/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/scroll';
      console.error(
        `FAIL: ${path.basename(outputPath)} is out of date. ` +
          `Run 'cd ${sourceDir} && node generate-nested-scroll-view.js' to update.`,
      );
      return false;
    }

    console.log(`OK: ${path.basename(outputPath)} is up-to-date`);
    return true;
  }

  fs.writeFileSync(outputPath, signed, 'utf8');
  console.log(`Generated: ${outputPath}`);
  return true;
}

/**
 * Parse command line arguments.
 */
function parseArgs(args) {
  const result = {
    verify: false,
    sourceDir: null,
  };

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    if (arg === '--verify') {
      result.verify = true;
    } else if (arg === '--source-dir') {
      if (i + 1 < args.length) {
        result.sourceDir = args[++i];
      } else {
        console.error('Error: --source-dir requires a path argument');
        process.exit(1);
      }
    } else if (arg === '--help' || arg === '-h') {
      console.log(`Usage: node generate-nested-scroll-view.js [--verify] [--source-dir DIR]

Options:
    --verify        Check if generated files are up-to-date (exit 1 if not)
    --source-dir    Directory containing source files (defaults to script directory)`);
      process.exit(0);
    }
  }

  return result;
}

function main() {
  const args = parseArgs(process.argv.slice(2));

  // Use provided source dir or fall back to script's directory
  const sourceDir = args.sourceDir || path.dirname(process.argv[1]);

  const filesToGenerate = [
    {
      source: path.join(sourceDir, 'ReactScrollView.java'),
      output: path.join(sourceDir, 'ReactNestedScrollView.java'),
      transform: transformScrollView,
    },
    {
      source: path.join(sourceDir, 'ReactScrollViewManager.kt'),
      output: path.join(sourceDir, 'ReactNestedScrollViewManager.kt'),
      transform: transformViewManager,
    },
  ];

  let success = true;
  for (const {source, output, transform} of filesToGenerate) {
    if (!generateFile(source, output, transform, args.verify)) {
      success = false;
    }
  }

  process.exit(success ? 0 : 1);
}

main();
