const esbuild = require('esbuild');
const resolve = require('esbuild-plugin-resolve');
const fs = require('fs-extra');
const { exec } = require('child_process');
const babel = require('./esbuild-plugin-babel');
const path = require('path')

const BUILD_DIR = './web-build';

async function readAndReplaceFileContent(path, writeFn) {
  const content = fs.readFileSync(path, 'utf-8');
  return Promise.resolve().then(() => {    
      return writeFn && writeFn(content);
  }).then((modifiedContent) => {
      if (modifiedContent !== undefined && modifiedContent !== null) {
          fs.writeFileSync(path, modifiedContent);
          return modifiedContent;
      }
      return content;
  });
}

class EsBuilder {
  cleanBuildDir() {
    if (fs.existsSync(BUILD_DIR)) {
      fs.removeSync(BUILD_DIR);
    }
  }

  prepareIndexHtml(result) {
    console.log('build success');
    try {
    const bundleName = fs.readdirSync('./web-build').find(n => /^bundle.*js$/.test(n));
    let content = fs.readFileSync(__dirname + '/index.html', 'utf-8');
    content = content.replace(/".\/bundle.js"/, `"./${bundleName}"`);
    fs.writeFileSync('./web-build/index.html', content);

    const metafilePath = path.join(__dirname, './', 'metafile.json');
    fs.writeFileSync(metafilePath, JSON.stringify(result.metafile));
    } catch (err) {
      console.error(err);
    }
  }

  patchPackageLib(paths) {
    paths.forEach((path) => {
      const packageFile = `${__dirname}/../node_modules/${path}/package.json`;
      if (fs.existsSync(packageFile)) {
        const _package = fs.readJSONSync(packageFile);
        console.log('patch applied on package at.. ', packageFile);
        delete _package['module'];
        delete _package['type'];
        fs.writeFileSync(packageFile, JSON.stringify(_package, null, 4));
      }
    });
  }

  async patchCamera() {
    return readAndReplaceFileContent(`${__dirname}/../node_modules/expo-camera/build/useWebQRScanner.js`, (c) => {
      if (c.indexOf('@koale/useworker') > 0) {
          return fs.readFileSync(`${__dirname}/expo-camera-patch/useWebQRScanner.js`, {
              encoding: 'utf-8'
          })
      }
      return c;
    });
  }

  async patchReactNativeAssets() {
    const path = `${__dirname}/../node_modules/@react-native/assets-registry/`
    fs.removeSync(path+'registry.js');
    const registry_content = `
    'use strict';

    export type PackagerAsset = {
      __packager_asset: boolean,
      fileSystemLocation: string,
      httpServerLocation: string,
      width?: number,
      height?: number,
      scales: Array<number>,
      hash: string,
      name: string,
      type: string,
    };
    
    const assets: Array<PackagerAsset> = [];
    
    export function registerAsset(asset: PackagerAsset): number {
      // push returns new array length, so the first asset will
      // get id 1 (not 0) to make the value truthy
      return assets.push(asset);
    }
    
    export function getAssetByID(assetId: number): PackagerAsset {
      return assets[assetId - 1];
    }
    `;
    fs.writeFileSync(path+'registry.ts', registry_content);
    const content = fs.readFileSync(path+'path-support.js', 'utf-8');
    fs.removeSync(path+'path-support.js')
    fs.writeFileSync(path+'path-support.ts', content);
    await readAndReplaceFileContent(path+'path-support.ts', (content)=>{
      return content.replace(`import type {PackagerAsset} from './registry.js';`,`import type {PackagerAsset} from './registry.ts';`)
      .replace(` | $TEMPORARY$string<'raw'> `, '');
    });
  }

  async patchReactNativeWeb() {
    const path = `${__dirname}/../node_modules/react-native-web/dist/cjs/exports/render/index.js`;
    await readAndReplaceFileContent(path, (content) => {
        return content.replace(
            'var _reactDom = require("react-dom");',
            `var React = require("react");
            var _reactDom = require("react-dom");
            var _reactDomClient = require("react-dom/client");`
        ).replace('hydrate(element', 'hydrateOld(element')
        .replace('render(element', 'renderOld(element') + 
        `
        function hydrate(element, root, callback) {
          (0, _dom.createSheet)(root);
          return (0, _reactDomClient.hydrateRoot)(element, root, callback);
        }
        
        function AppWithCallbackAfterRender(props) {
          React.useEffect(() => {props.callback && props.callback()});
          return props.element;
        }
        
        function render(element, root, callback) {
          (0, _dom.createSheet)(root);
          return (0, _reactDomClient.createRoot)(root).render(
            React.createElement(AppWithCallbackAfterRender, 
              {
                "callback": callback,
                element: element
              }
            )
          );
        }`;
    });
    const target = `${__dirname}/../node_modules/react-native-web/dist/cjs/exports/BackHandler/index.js`;
    await readAndReplaceFileContent(target, content => {
      return content.replace('console.error', '// console.error');
    });
    console.log('Patched react-native-web');
  }

  patchWithDevTools(path) {
    const packageFile = `${__dirname}/../node_modules/${path}`;
    if (fs.existsSync(packageFile)) {
      const _package = "import * as React from 'react';export function withDevTools(AppRootComponent) {return (props) => React.createElement(AppRootComponent, { ...props });}"
      fs.writeFileSync(packageFile, _package);
    }
  }

  async patchReactNativeAssetRegistry() {
    const content = `
    /**
     * 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';
    
    const assets = [];
    
    function registerAsset(asset) {
      // push returns new array length, so the first asset will
      // get id 1 (not 0) to make the value truthy
      return assets.push(asset);
    }
    
    function getAssetByID(assetId) {
      return assets[assetId - 1];
    }
    
    module.exports = {registerAsset, getAssetByID};
    
    `;
    fs.writeFileSync(`${__dirname}/../node_modules/@react-native/assets-registry/registry.js`, content);
    const expoAssetRegistry = `${__dirname}/../node_modules/expo-asset/node_modules/@react-native/assets-registry/registry.js`;
    readAndReplaceFileContent(expoAssetRegistry, () => content);
    console.log('Patched @react-native/assets-registry');
  }

  async patchBabelExpoPreset() {
    const target = `${__dirname}/../node_modules/babel-preset-expo/build/index.js`;
    await readAndReplaceFileContent(target, content => {
      return content.replace(
        `(0, common_1.hasModule)('react-native-reanimated') &&`,
        `//(0, common_1.hasModule)('react-native-reanimated') &&`)
        .replace(
          `platformOptions.reanimated !== false && [require.resolve('react-native-reanimated/plugin')],`,
          `//platformOptions.reanimated !== false && [require.resolve('react-native-reanimated/plugin')],`
        )
    });
    console.log('Patched babel-expo-preset');
  }

  async patchUUID() {
    const target = `${__dirname}/../node_modules/expo-modules-core/build/uuid/uuid.web.js`;
    await readAndReplaceFileContent(target, content => {
      return content.replace(`require('crypto')`, 'crypto');
    });
    console.log('Patched UUID.');
  }

  async patchExpoRootComponent() {
    const target = `${__dirname}/../node_modules/expo/build/launch/registerRootComponent.js`;
    await readAndReplaceFileContent(target, content => {
      return content.replace(`export default`, 
      `
      const process = {env: {
        NODE_ENV: 'development',
        EXPO_PUBLIC_USE_STATIC: '0'
      }};
      export default `);
    });
    console.log('Patched Expo Root Component.');
  }

  async patchReactNavigation() {
    // https://github.com/react-navigation/react-navigation/pull/11496/files
    fs.copyFileSync(
      `${__dirname}/@react-navigation/drawer/6.6.3/lib/module/views/modern/Drawer.js`,
      `${__dirname}/../node_modules/@react-navigation/drawer/lib/module/views/modern/Drawer.js`
    );
    fs.copyFileSync(
      `${__dirname}/@react-navigation/drawer/6.6.3/lib/module/views/modern/Overlay.js`,
      `${__dirname}/../node_modules/@react-navigation/drawer/lib/module/views/modern/Overlay.js`
    );
    console.log('Patched React Navigation.');
  }

  async patchReactNativeReanimated() {
    await readAndReplaceFileContent(`${__dirname}/../node_modules/react-native-reanimated/lib/module/reanimated2/PlatformChecker.js`, (c) => {
      return c.replace(
        '!!process.env.JEST_WORKER_ID',
        'false; // !!process.env.JEST_WORKER_ID'
      )
    });
    await readAndReplaceFileContent(`${__dirname}/../node_modules/react-native-reanimated/src/reanimated2/hook/useSharedValue.ts`, (c) => {
      return c.replace(
        'useRef<SharedValue<Value>>',
        'useRef'
      )
    });
    console.log('Patched React Native Reanimated.');
  }

  async patchExpoVideo() {
    await readAndReplaceFileContent(`${__dirname}/../node_modules/expo-video/build/index.js`, (c) => {
      return c.replace(
        '{ VideoThumbnail }',
        '{ default as VideoThumbnail }'
      )
    });
    console.log('Patched Expo video.')
  }


  buildLibraries() {
    const promises = [{
      name: 'expo-web-browser',
      build: (path) => {
        fs.writeFileSync(`${path}/build/WebBrowser.js`, `
        export const openBrowserAsync = (url) => {
          window.open(url, '_blank');
        };`);
      }
    }].map(p => {
      let libPath = `${__dirname}/../node_modules/${p.name}`;
      if (!fs.existsSync(libPath)) {
        libPath = `${__dirname}/../node_modules/@wavemaker/app-rn-runtime/node_modules/${p.name}`;
      }
      if (!fs.existsSync(libPath)) {
        const msg = `${p.name} library is not found`;
        console.error(msg);
        return Promise.reject(msg);
      }
      const destPath = `${__dirname}/node_modules/${p.name}`;
      if (fs.existsSync(destPath)) {
        fs.removeSync(destPath);
      }
      fs.copySync(libPath, destPath);
      return p.build && p.build(destPath);
    });
    return Promise.all(promises)
        .then(() => this.patchPackageLib(['@koale/useworker', 'victory',
          'd3-array', 'd3-color', 'd3-ease', 'd3-format', 'd3-interpolate', 
          'd3-path', 'd3-scale', 'd3-shape', 'd3-time', 'd3-time-format',
          'd3-timer', 'd3-voronoi']))
        .then(() => this.patchWithDevTools('expo/build/launch/withDevTools.js'))
        .then(() => this.patchReactNativeWeb())
        .then(() => this.patchReactNativeAssets())
        .then(() => this.patchBabelExpoPreset())
        .then(() => this.patchReactNavigation())
        .then(() => this.patchReactNativeReanimated())
        .then(() => this.patchExpoVideo())
        .then(() => {
          console.log('*********** LIBRARIES ARE BUILT FOR ESBUILD **************');
        }, (e) => {
          console.error(e);
          console.log('*********** FAILED TO BUILD LIBRARIES FOR ESBUILD **************');
        });
  }

  build() {
    console.log('excluded react-native-vector-icons module in esbuild');
    return esbuild.build({
      entryPoints: ['./node_modules/expo/AppEntry.js'],
      external:['./node_modules/react-native-vector-icons'],
      bundle: true,
      entryNames: 'bundle-[hash]',
      outdir: 'web-build',
      jsxFragment: 'React.Fragment',
      tsconfig: './esbuild/jsconfig.json',
      define: {'process.env.NODE_ENV': '"development"', '__DEV__': false, global:'window', 'process.env.EXPO_OS': '"web"'},
      resolveExtensions: ['.web.tsx','.web.ts','.web.jsx','.web.js','.tsx','.ts','.jsx','.js',],
      loader: {".png": "file", ".jpeg": "file", ".ttf": "file", ".js": "jsx", ".gif": "file", ".svg": "file" },
      minifyWhitespace: true,
      minifySyntax: true,
      metafile: true,
      sourcemap: true,
      plugins: [babel({
        filter: /\/src\/.*jsx?/,
        config : {
          targets: {
            browsers: 'last 2 versions',
            esmodules: true
          },
          sourceMaps: 'inline',
          plugins: [
            __dirname + '/../scripts/wm-babel.transform.plugin.js', 
            '@babel/plugin-proposal-export-namespace-from',
            'react-native-reanimated/plugin',
          ]
        }
      }), resolve({
        'victory-native': 'victory'
      })]
    })
  }

  triggerBuild() {
    console.log('*********** REACT NATIVE ESBUILD STARTED **************');
    console.time('react native with esbuild');
    this.cleanBuildDir();
    this.build()
    .then((result) => this.prepareIndexHtml(result))
    .then(() => {
      console.timeEnd('react native with esbuild');
      console.log('*********** REACT NATIVE ESBUILD SUCCESSFUL **************');
    }).catch((error) => {
      console.log('*********** REACT PROJECT ESBUILD ERROR **************');
      console.error(error);
      console.timeEnd('react native with esbuild');
      console.log('*********** REACT PROJECT ESBUILD FAILED **************');
    });
  }
}

if (process.argv[2] === '--prepare-lib') {
  (new EsBuilder()).buildLibraries();
} else {
  (new EsBuilder()).triggerBuild();
}
