import {
  ConfigPlugin,
  createRunOncePlugin,
  ExportedConfigWithProps,
  WarningAggregator,
  withDangerousMod,
  withMainActivity,
} from '@expo/config-plugins';
import { ExpoConfig } from '@expo/config-types';
import fs from 'fs';
import path from 'path';
import semver from 'semver';

import { resolveExpoUpdatesVersion } from './resolveExpoUpdatesVersion';
import { withDevLauncherAppDelegate } from './withDevLauncherAppDelegate';

const pkg = require('expo-dev-launcher/package.json');

const DEV_LAUNCHER_ANDROID_IMPORT = 'expo.modules.devlauncher.DevLauncherController';
const DEV_LAUNCHER_UPDATES_ANDROID_IMPORT = 'expo.modules.updates.UpdatesDevLauncherController';
const DEV_LAUNCHER_ON_NEW_INTENT = `
  @Override
  public void onNewIntent(Intent intent) {
      if (DevLauncherController.tryToHandleIntent(this, intent)) {
         return;
      }
      super.onNewIntent(intent);
  }
`;
const DEV_LAUNCHER_WRAPPED_ACTIVITY_DELEGATE = `DevLauncherController.wrapReactActivityDelegate(this, () -> $1);`;
const DEV_LAUNCHER_ANDROID_INIT = 'DevLauncherController.initialize(this, getReactNativeHost());';
const DEV_LAUNCHER_UPDATES_ANDROID_INIT = `if (BuildConfig.DEBUG) {
      DevLauncherController.getInstance().setUpdatesInterface(UpdatesDevLauncherController.initialize(this));
    }`;
const DEV_LAUNCHER_UPDATES_DEVELOPER_SUPPORT =
  'return DevLauncherController.getInstance().getUseDeveloperSupport();';

const DEV_LAUNCHER_JS_REGISTER_ERROR_HANDLERS = `import 'expo-dev-client'`;
const DEV_LAUNCHER_JS_REGISTER_ERROR_HANDLERS_VIA_LAUNCHER = `import 'expo-dev-launcher'`;

async function readFileAsync(path: string): Promise<string> {
  return fs.promises.readFile(path, 'utf8');
}

async function saveFileAsync(path: string, content: string): Promise<void> {
  return fs.promises.writeFile(path, content, 'utf8');
}

function addLines(content: string, find: string | RegExp, offset: number, toAdd: string[]) {
  const lines = content.split('\n');

  let lineIndex = lines.findIndex(line => line.match(find));

  for (const newLine of toAdd) {
    if (!content.includes(newLine)) {
      lines.splice(lineIndex + offset, 0, newLine);
      lineIndex++;
    }
  }

  return lines.join('\n');
}

function replaceLine(content: string, find: string | RegExp, replace: string) {
  const lines = content.split('\n');

  if (!content.includes(replace)) {
    const lineIndex = lines.findIndex(line => line.match(find));
    lines.splice(lineIndex, 1, replace);
  }

  return lines.join('\n');
}

function addJavaImports(javaSource: string, javaImports: string[]): string {
  const lines = javaSource.split('\n');
  const lineIndexWithPackageDeclaration = lines.findIndex(line => line.match(/^package .*;$/));
  for (const javaImport of javaImports) {
    if (!javaSource.includes(javaImport)) {
      const importStatement = `import ${javaImport};`;
      lines.splice(lineIndexWithPackageDeclaration + 1, 0, importStatement);
    }
  }
  return lines.join('\n');
}

async function editMainApplication(
  config: ExportedConfigWithProps,
  action: (mainApplication: string) => string
): Promise<void> {
  const mainApplicationPath = path.join(
    config.modRequest.platformProjectRoot,
    'app',
    'src',
    'main',
    'java',
    ...config.android!.package!.split('.'),
    'MainApplication.java'
  );

  try {
    const mainApplication = action(await readFileAsync(mainApplicationPath));
    return await saveFileAsync(mainApplicationPath, mainApplication);
  } catch (e) {
    WarningAggregator.addWarningIOS(
      'expo-dev-launcher',
      `Couldn't modify MainApplication.java - ${e}.`
    );
  }
}

async function editPodfile(config: ExportedConfigWithProps, action: (podfile: string) => string) {
  const podfilePath = path.join(config.modRequest.platformProjectRoot, 'Podfile');
  try {
    const podfile = action(await readFileAsync(podfilePath));
    return await saveFileAsync(podfilePath, podfile);
  } catch (e) {
    WarningAggregator.addWarningIOS('expo-dev-launcher', `Couldn't modify AppDelegate.m - ${e}.`);
  }
}

async function editIndex(config: ExportedConfigWithProps, action: (index: string) => string) {
  const indexPath = path.join(config.modRequest.projectRoot, 'index.js');
  try {
    const index = action(await readFileAsync(indexPath));
    return await saveFileAsync(indexPath, index);
  } catch (e) {
    WarningAggregator.addWarningIOS('expo-dev-launcher', `Couldn't modify index.js - ${e}.`);
  }
}

const withDevLauncherApplication: ConfigPlugin = config => {
  return withDangerousMod(config, [
    'android',
    async config => {
      await editMainApplication(config, mainApplication => {
        mainApplication = addJavaImports(mainApplication, [DEV_LAUNCHER_ANDROID_IMPORT]);

        mainApplication = addLines(mainApplication, 'initializeFlipper\\(this', 0, [
          `    ${DEV_LAUNCHER_ANDROID_INIT}`,
        ]);

        let expoUpdatesVersion;
        try {
          expoUpdatesVersion = resolveExpoUpdatesVersion(config.modRequest.projectRoot);
        } catch (e) {
          WarningAggregator.addWarningAndroid(
            'expo-dev-launcher',
            `Failed to check compatibility with expo-updates - ${e}`
          );
        }
        if (expoUpdatesVersion && semver.gt(expoUpdatesVersion, '0.6.0')) {
          mainApplication = addJavaImports(mainApplication, [DEV_LAUNCHER_UPDATES_ANDROID_IMPORT]);
          mainApplication = addLines(mainApplication, 'initializeFlipper\\(this', 0, [
            `    ${DEV_LAUNCHER_UPDATES_ANDROID_INIT}`,
          ]);
          mainApplication = replaceLine(
            mainApplication,
            'return BuildConfig.DEBUG;',
            `      ${DEV_LAUNCHER_UPDATES_DEVELOPER_SUPPORT}`
          );
        }

        return mainApplication;
      });
      return config;
    },
  ]);
};

const withDevLauncherActivity: ConfigPlugin = config => {
  return withMainActivity(config, config => {
    if (config.modResults.language === 'java') {
      let content = addJavaImports(config.modResults.contents, [
        DEV_LAUNCHER_ANDROID_IMPORT,
        'android.content.Intent',
      ]);

      if (!content.includes(DEV_LAUNCHER_ON_NEW_INTENT)) {
        const lines = content.split('\n');
        const onCreateIndex = lines.findIndex(line => line.includes('public class MainActivity'));

        lines.splice(onCreateIndex + 1, 0, DEV_LAUNCHER_ON_NEW_INTENT);

        content = lines.join('\n');
      }

      if (!content.includes('DevLauncherController.wrapReactActivityDelegate')) {
        content = content.replace(
          /(new ReactActivityDelegate(.*|\s)*});$/m,
          DEV_LAUNCHER_WRAPPED_ACTIVITY_DELEGATE
        );
      }

      config.modResults.contents = content;
    } else {
      WarningAggregator.addWarningAndroid(
        'expo-dev-launcher',
        `Cannot automatically configure MainActivity if it's not java`
      );
    }

    return config;
  });
};

const withDevLauncherPodfile: ConfigPlugin = config => {
  return withDangerousMod(config, [
    'ios',
    async config => {
      await editPodfile(config, podfile => {
        podfile = podfile.replace("platform :ios, '10.0'", "platform :ios, '11.0'");
        // Match both variations of Ruby config:
        // unknown: pod 'expo-dev-launcher', path: '../node_modules/expo-dev-launcher', :configurations => :debug
        // Rubocop: pod 'expo-dev-launcher', path: '../node_modules/expo-dev-launcher', configurations: :debug
        if (
          !podfile.match(
            /pod ['"]expo-dev-launcher['"],\s?path: ['"][^'"]*node_modules\/expo-dev-launcher['"],\s?:?configurations:?\s(?:=>\s)?:debug/
          )
        ) {
          const packagePath = path.dirname(require.resolve('expo-dev-launcher/package.json'));
          const relativePath = path.relative(config.modRequest.platformProjectRoot, packagePath);
          podfile = addLines(podfile, 'use_react_native', 0, [
            `  pod 'expo-dev-launcher', path: '${relativePath}', :configurations => :debug`,
          ]);
        }
        return podfile;
      });
      return config;
    },
  ]);
};

const withErrorHandling: ConfigPlugin = config => {
  const injectErrorHandlers = async (config: ExportedConfigWithProps) => {
    await editIndex(config, index => {
      if (
        !index.includes(DEV_LAUNCHER_JS_REGISTER_ERROR_HANDLERS) &&
        !index.includes(DEV_LAUNCHER_JS_REGISTER_ERROR_HANDLERS_VIA_LAUNCHER)
      ) {
        index = DEV_LAUNCHER_JS_REGISTER_ERROR_HANDLERS + ';\n\n' + index;
      }
      return index;
    });
    return config;
  };

  // We need to run the same task twice to ensure it will work on both platforms,
  // because if someone runs `expo run:ios`, it will trigger only dangerous mode for that specific platform.
  // Note: after the first execution, the second one won't change anything.
  config = withDangerousMod(config, ['android', injectErrorHandlers]);
  config = withDangerousMod(config, ['ios', injectErrorHandlers]);

  return config;
};

const withDevLauncher = (config: ExpoConfig) => {
  config = withDevLauncherActivity(config);
  config = withDevLauncherApplication(config);
  config = withDevLauncherPodfile(config);
  config = withDevLauncherAppDelegate(config);
  config = withErrorHandling(config);
  return config;
};

export default createRunOncePlugin(withDevLauncher, pkg.name, pkg.version);
