/**
 * Copyright 2017-present, Callstack.
 * All rights reserved.
 *
 * @flow
 */
import type { Command } from '../types';

const path = require('path');
const webpack = require('webpack');
const clear = require('clear');
const { DEFAULT_CONFIG_FILENAME } = require('../constants');

const { MessageError } = require('../errors');
const messages = require('../messages');
const getWebpackConfigPath = require('../utils/getWebpackConfigPath');
const getConfig = require('../utils/getConfig');
const { getHaulConfig } = require('../utils/getHaulConfig');
const logger = require('../logger');
const SimpleProgressWebpackPlugin = require('simple-progress-webpack-plugin');

/**
 * Bundles your application code
 */
async function bundle(opts: *) {
  const directory = process.cwd();
  const configPath = getWebpackConfigPath(directory, opts.config);

  const config = getConfig(
    configPath,
    {
      root: directory,
      dev: opts.dev,
      minify: opts.minify,
      bundle: true,
    },
    opts.platform,
    logger
  );

  if (opts.assetsDest) {
    config.output.path = path.isAbsolute(opts.assetsDest)
      ? opts.assetsDest
      : path.join(directory, opts.assetsDest);
  }

  if (opts.bundleOutput) {
    config.output.filename = path.isAbsolute(opts.bundleOutput)
      ? path.relative(config.output.path, opts.bundleOutput)
      : path.relative(
          config.output.path,
          path.join(directory, opts.bundleOutput)
        );
  }

  logger.info(`Assets Destination: ${config.output.path}`);
  logger.info(`Bundle output: ${config.output.filename}`);
  logger.info(
    `Bundle output (resolved): ${path.resolve(config.output.filename)}`
  );

  // attach progress plugin
  if (opts.progress !== 'none') {
    config.plugins = config.plugins.concat([
      new SimpleProgressWebpackPlugin({
        format: opts.progress,
      }),
    ]);
  }

  const compiler = webpack(config);

  logger.info(
    messages.initialBundleInformation({
      entry: config.entry,
      dev: opts.dev,
    })
  );

  const stats = await new Promise((resolve, reject) =>
    compiler.run((err, info) => {
      if (err || info.hasErrors()) {
        reject(
          new MessageError(
            messages.bundleFailed({
              errors: err
                ? [err.message]
                : info.toJson({ errorDetails: true }).errors,
            })
          )
        );
      } else if (info.hasWarnings()) {
        logger.warn(info.toJson().warnings);
        resolve(info);
      } else {
        resolve(info);
      }
    })
  );

  clear();

  logger.done(
    messages.bundleBuilt({
      stats,
      platform: opts.platform,
      assetsPath: config.output.path,
      bundlePath: config.output.filename,
    })
  );
}

// Allow config file to override the list of availiable platforms
function adjustOptions(options) {
  const directory = process.cwd();
  const configPath = getWebpackConfigPath(directory, options.config);
  const haulOptions = getHaulConfig(configPath, logger);

  if (haulOptions.platforms) {
    const platformOption =
      command.options && command.options.find(_ => _.name === 'platform');
    if (platformOption) {
      platformOption.choices = [];
      for (const platformName in haulOptions.platforms) {
        if (
          Object.prototype.hasOwnProperty.call(
            haulOptions.platforms,
            platformName
          )
        ) {
          if (platformOption.choices) {
            platformOption.choices.push({
              value: platformName,
              description: `Builds ${haulOptions.platforms[
                platformName
              ]} bundle`,
            });
          }
        }
      }
    }
  }
}

let command = ({
  name: 'bundle',
  description:
    'Builds the app bundle for packaging. Run with `--platform` flag to specify the platform [ios|android].',
  action: bundle,
  adjustOptions,
  options: [
    {
      name: 'dev',
      description: 'Whether to build in development mode',
      default: false,
      parse: (val: string) => val !== 'false',
      choices: [
        {
          value: true,
          description: 'Builds in development mode',
        },
        {
          value: false,
          description: 'Builds in production mode',
        },
      ],
    },
    {
      name: 'minify',
      description: `Whether to minify the bundle, 'true' by default when dev=false`,
      default: ({ dev }: *) => !dev,
      parse: (val: string) => val !== 'false',
      choices: [
        {
          value: true,
          description: 'Enables minification for the bundle',
        },
        {
          value: false,
          description: 'Disables minification for the bundle',
        },
      ],
    },
    {
      name: 'platform',
      description: 'Platform to bundle for',
      required: true,
      choices: [
        {
          value: 'ios',
          description: 'Builds iOS bundle',
        },
        {
          value: 'android',
          description: 'Builds Android bundle',
        },
      ],
      example: 'haul bundle --platform ios',
    },
    {
      name: 'bundleOutput',
      description: 'Path to use for the bundle file, eg. index.ios.bundle',
    },
    {
      name: 'assetsDest',
      description: 'Path to directory where to store assets, eg. /tmp/dist',
    },
    {
      name: 'config',
      description: `Path to config file, eg. ${DEFAULT_CONFIG_FILENAME}`,
      default: DEFAULT_CONFIG_FILENAME,
    },
    {
      name: 'progress',
      description:
        'Display bundle compilation progress with different verbosity levels',
      default: () => {
        // Ensure that we don't trip Xcode's error detection. 'verbose' is the
        // only level that doesn't make Xcode think that the bundle failed.
        return !process.stdin.isTTY ? 'verbose' : 'compact';
      },
      example: 'haul bundle --progress minimal',
      choices: [
        {
          value: 'none',
          description: 'no progress',
        },
        {
          value: 'minimal',
          description: 'minimalistic, one line progress',
        },
        {
          value: 'compact',
          description: 'show stages and compilation progress',
        },
        {
          value: 'expanded',
          description: 'sho more information and phases of compilation',
        },
        {
          value: 'verbose',
          description: 'show all',
        },
      ],
    },
  ],
}: Command);

module.exports = command;


/**
 * Copyright 2017-present, Callstack.
 * All rights reserved.
 *
 * @flow
 */
const child_process = require('child_process'); // eslint-disable-line camelcase

function exec(args: string): Promise<void> {
  return new Promise((resolve, reject) => {
    child_process.exec(args, (err, stdout, stderr) => {
      if (err) {
        reject(new Error(stderr));
      } else {
        resolve();
      }
    });
  });
}

module.exports = exec;


/**
 * Copyright 2017-present, Callstack.
 * All rights reserved.
 *
 * haulProgressBar.js
 *
 * @flow
 */

const multiProgress = require('multi-progress');
const chalk = require('chalk');

const progressBarFactory = new multiProgress(process.stderr);

const progressBar = {};
const barOptions = {
  complete: chalk.bold('='),
  incomplete: ' ',
  width: 20,
  total: 100,
};
const labelOptions = {
  android: {
    color: 'green',
    label: 'Android',
  },
  ios: {
    color: 'cyan',
    label: 'iOS',
  },
};

/*
 * Create label based on passed platform
 * We use `labelOptions` to customize know platforms
 * If platform is not know, returns default styles
 */
function createLabel(platform: string) {
  if (labelOptions[platform]) {
    const { color, label } = labelOptions[platform];
    return `${chalk.bold[color](label)}`.padEnd(30);
  }
  return `${chalk.bold.magenta(platform)}`.padEnd(30);
}

/*
 * Create progress bar itself
 */
function createBarFormat(platform: string) {
  const label = createLabel(platform);

  const leftBar = chalk.bold('[');
  const rightBar = chalk.bold(']');
  const percent = chalk.bold.blue(':percent');

  return `${label}${leftBar}:bar${rightBar} ${percent}`;
}

module.exports = function updateProgressBar(platform: string, percent: number) {
  if (!progressBar[platform]) {
    progressBar[platform] = progressBarFactory.newBar(
      createBarFormat(platform),
      barOptions
    );
  }

  progressBar[platform].update(percent);
};


/**
 * Copyright 2017-present, Callstack.
 * All rights reserved.
 */

function cleanPathMiddleware(req, res, next) {
  req.cleanPath = req.path.replace(/\/$/, '');

  next();
}

module.exports = cleanPathMiddleware;


/**
 * Copyright 2017-present, Callstack.
 * All rights reserved.
 *
 * @flow
 */

const express = require('express');
const http = require('http');
const path = require('path');

const Compiler = require('../compiler/Compiler');
const initUI = require('./ui');

/**
 * Custom made middlewares
 */

const createCompilerMiddleware = require('./middleware/compilerMiddleware.js');
const hotMiddleware = require('./middleware/hotMiddleware');
const devToolsMiddleware = require('./middleware/devToolsMiddleware');
const liveReloadMiddleware = require('./middleware/liveReloadMiddleware');
const statusPageMiddleware = require('./middleware/statusPageMiddleware');
const symbolicateMiddleware = require('./middleware/symbolicateMiddleware');
const openInEditorMiddleware = require('./middleware/openInEditorMiddleware');
const missingBundleMiddleware = require('./middleware/missingBundleMiddleware');
const systraceMiddleware = require('./middleware/systraceMiddleware');
const rawBodyMiddleware = require('./middleware/rawBodyMiddleware');
const cleanPathMiddleware = require('./middleware/cleanPathMiddleware');

const WebSocketServer = require('ws').Server;

/**
 * Temporarily loaded from React Native to get debugger running. Soon to be replaced.
 */
const webSocketProxy = require('./util/websocketProxy');
const WebSocketDebuggerProxy = require('./util/WebsocketDebuggerProxy');

/**
 * Packager-like Server running on top of Webpack
 */
function createServer(config: { configPath: ?string, configOptions: Object }) {
  const appHandler = express();
  appHandler.disable('etag');

  const { configPath, configOptions } = config;

  const compiler = new Compiler({
    configPath,
    configOptions,
  });

  const loggerMiddleware = initUI(compiler, configOptions);

  process.on('uncaughtException', err => {
    compiler.terminate();
    throw err;
  });

  process.on('SIGINT', () => {
    compiler.terminate();
    process.exit(0);
  });

  process.on('SIGTERM', () => {
    compiler.terminate();
    process.exit(2);
  });

  const compilerMiddleware = createCompilerMiddleware(compiler, {
    configPath,
    configOptions,
  });

  const httpServer = http.createServer(appHandler);

  const webSocketServer = new WebSocketServer({ server: httpServer });
  const debuggerProxy = new WebSocketDebuggerProxy(
    webSocketProxy(webSocketServer, '/debugger-proxy')
  );

  if (!configOptions.disableHotReloading) {
    hotMiddleware(compiler, {
      nativeProxy: webSocketProxy(webSocketServer, '/hot'),
      haulProxy: webSocketProxy(webSocketServer, '/haul-hmr'),
    });
  }

  // Middlewares
  appHandler
    .use(express.static(path.join(__dirname, '/assets/public')))
    .use(rawBodyMiddleware)
    .use(cleanPathMiddleware)
    .use(devToolsMiddleware(debuggerProxy))
    .use(liveReloadMiddleware(compiler))
    .use(statusPageMiddleware)
    .use(
      symbolicateMiddleware(compiler, {
        configPath,
        configOptions,
      })
    )
    .use(openInEditorMiddleware())
    .use('/systrace', systraceMiddleware)
    .use(loggerMiddleware)
    .use(compilerMiddleware)
    .use(missingBundleMiddleware);

  if (configOptions.eager) {
    if (
      !Array.isArray(configOptions.eager) &&
      typeof configOptions.eager === 'boolean'
    ) {
      // implicitly true...
      // TODO: Eager loading for all platforms
      configOptions.eager = ['android', 'ios' /* , ... potentially more? */];
    }

    configOptions.eager.forEach(platform => {
      compiler.emit(Compiler.Events.REQUEST_BUNDLE, {
        filename: `/index.${platform}.bundle`, // XXX: maybe the entry bundle is arbitary
        platform,
        callback() {},
      });
    });
  }

  return httpServer;
}

module.exports = createServer;


/**
 * Copyright 2017-present, Callstack.
 * All rights reserved.
 */
const fs = require('fs');
const path = require('path');
const opn = require('opn');
const select = require('platform-select');
const logger = require('../../logger');

/**
 * Launches given `url` in browser based on platform
 */
const launchBrowser = url => {
  const open = app => () => opn(url, { app });

  /**
   * Run Chrome (Chrome Canary) or supported platform.
   * In case of macOS, we can eventually fallback to Safari.
   *
   * select(attemp1, attemp2, attemp3,...) // attempt to run is from left to right
   */
  select(
    {
      // try to find & run Google Chrome
      darwin: open('google chrome'),
      win32: open('chrome'),
      _: open('google-chrome'),
    },
    {
      // On macOS let's try to find & run Canary
      darwin: open('google chrome canary'),
    },
    {
      // No Canary / Chrome, let's run Safari
      darwin: open('safari'),
    }
  ).catch(e => {
    console.log(e); // print error to artifacts
    logger.warn(
      `Cannot start browser for debugging. Navigate manually to "${url}"`
    );
  });
};

/**
 * Devtools middleware compatible with default React Native implementation
 */
function devToolsMiddleware(debuggerProxy) {
  return (req, res, next) => {
    switch (req.cleanPath) {
      /**
       * Request for the debugger frontend
       */
      case '/debugger-ui/':
      case '/debugger-ui': {
        const readStream = fs.createReadStream(
          path.join(__dirname, '../assets/debugger.html')
        );
        res.writeHead(200, { 'Content-Type': 'text/html' });
        readStream.pipe(res);
        break;
      }

      /**
       * Request for the debugger worker
       */
      case '/debugger-ui/debuggerWorker.js':
      case '/debuggerWorker.js': {
        const readStream = fs.createReadStream(
          path.join(__dirname, '../assets/debuggerWorker.js')
        );
        res.writeHead(200, { 'Content-Type': 'application/javascript' });
        readStream.pipe(res);
        break;
      }

      /**
       * Request for (maybe) launching devtools
       */
      case '/launch-js-devtools': {
        if (!debuggerProxy.isDebuggerConnected()) {
          launchBrowser(`http://localhost:${req.socket.localPort}/debugger-ui`);
        }
        res.end('OK');
        break;
      }

      default:
        next();
    }
  };
}

module.exports = devToolsMiddleware;


/**
 * Copyright 2017-present, Callstack.
 * All rights reserved.
 *
 * @flow
 * global WebSocket, MessageEvent
 */

const processUpdate = require('webpack-hot-middleware/process-update');
// $FlowFixMe
const { Platform } = require('react-native'); // eslint-disable-line import/no-unresolved

function normalizeOptions({ path, quiet, overlay, reload, name }) {
  const shouldLog = !quiet;
  const options = {
    path,
    overlay: true,
    reload: false,
    name: '',
    logger: {
      shouldLog,
      log(...args) {
        if (shouldLog) {
          console.log(...args);
        }
      },
      warn(...args) {
        if (shouldLog) {
          console.warn(...args);
        }
      },
      error(...args) {
        if (shouldLog) {
          console.error(...args);
        }
      },
    },
  };

  if (overlay) {
    options.overlay = overlay !== 'false';
  }
  if (reload) {
    options.reload = reload !== 'false';
  }
  if (name) {
    options.name = name;
  }
  return options;
}

function processPayload(payload, { logger, reporter, ...opts }) {
  switch (payload.action) {
    case 'building':
      logger.log(
        `[Haul HMR] Bundle ${payload.name
          ? `'${payload.name}' `
          : ''}rebuilding`
      );
      break;
    case 'built':
      logger.log(
        `[Haul HMR] Bundle ${payload.name
          ? `'${payload.name}' `
          : ''}rebuilt in ${payload.time}ms`
      );
    // fall through
    case 'sync':
      if (payload.name && opts.name && payload.name !== opts.name) {
        return;
      }

      if (payload.errors.length > 0 && reporter) {
        reporter.problems('errors', payload);
      } else if (payload.warnings.length > 0 && reporter) {
        reporter.problems('warnings', payload);
      } else if (reporter) {
        reporter.cleanProblemsCache();
        reporter.success();
      }

      processUpdate(payload.hash, payload.modules, {
        ...opts,
        log: logger.shouldLog,
        warn: logger.shouldLog,
        error: logger.shouldLog,
      });
      break;
    default:
      logger.warn(`[HMR] Invalid action ${payload.action}`);
  }
}

/**
 * Custom HMR client with WebSocket support instead of EventSource as `webpack-hot-middleware`
 */
module.exports = function connect(options: Object) {
  const { logger, ...opts } = normalizeOptions(options);
  const ws = new WebSocket(`${opts.path}?platform=${Platform.OS}`);

  ws.onopen = () => {
    logger.log(
      '[Haul HMR] Client connected, however until you `Enable Hot Reloading`, ' +
        'you will not get any updates'
    );
  };

  ws.onerror = error => {
    logger.error(
      `[Haul HMR] Client could not connect to the server ${opts.path}`,
      error
    );
  };

  ws.onmessage = (message: MessageEvent) => {
    if (typeof message.data !== 'string') {
      throw new Error(
        `[Haul HMR] Data from websocker#onmessage must be a string`
      );
    }
    const payload = JSON.parse(message.data);
    try {
      processPayload(payload, { logger, ...opts });
    } catch (error) {
      logger.warn(`[Haul HMR] Invalid message: ${payload}`, error);
    }
  };
};


/**
 * Copyright 2017-present, Callstack.
 * All rights reserved.
 *
 * @flow
 *
 * --- OVERVIEW ---
 *
 *   When running in dev mode, React Native handles source map lookups by
 *   asking the packager to do it.
 *
 *   It does a POST to /symbolicate and passes the call stack and expects
 *   back the same structure, but with the appropriate lines, columns & files.
 *
 *   This is the express middleware which will handle that endpoint by reading
 *   the source map that is tucked away inside webpack's in-memory filesystem.
 *
 */
import type { $Request, Middleware } from 'express';
import type {
  ReactNativeStackFrame,
  ReactNativeStack,
  Platform,
} from '../../types';

const SourceMapConsumer = require('source-map').SourceMapConsumer;
const path = require('path');
const fetch = require('node-fetch');
const getConfig = require('../../utils/getConfig');
const Compiler = require('../../compiler/Compiler');
const messages = require('../../messages');
const logger = require('../../logger');

type ReactNativeSymbolicateRequest = {
  stack: ReactNativeStack,
};

type ReactNativeSymbolicateResponse = {
  stack: ReactNativeStack,
};

/**
 * Creates a SourceMapConsumer so we can query it.
 */
async function createSourceMapConsumer(compiler: Compiler, url: string) {
  const response = await fetch(url);
  const sourceMap = await response.text();

  // we stop here if we couldn't find that map
  if (!sourceMap) {
    logger.warn(messages.sourceMapFileNotFound());
    return null;
  }

  // feed the raw source map into our consumer
  try {
    return new SourceMapConsumer(sourceMap);
  } catch (err) {
    logger.error(messages.sourceMapInvalidFormat());
    return null;
  }
}

/**
 * Gets the stack frames that React Native wants us to convert.
 */
function getRequestedFrames(req: $Request): ?ReactNativeStack {
  if (typeof req.rawBody !== 'string') {
    return null;
  }

  let stack;

  try {
    const payload: ReactNativeSymbolicateRequest = JSON.parse(req.rawBody);
    stack = payload.stack;
  } catch (err) {
    // should happen, but at least we won't die
    stack = null;
  }

  if (!stack) return null;

  const newStack = stack.filter(stackLine => {
    const { methodName } = stackLine;
    const unwantedStackRegExp = new RegExp(
      /(__webpack_require__|haul|eval(JS){0,1})/
    );

    if (unwantedStackRegExp.test(methodName)) return false; // we don't need those

    const evalLine = methodName.indexOf('Object../');
    if (evalLine > -1) {
      const newMethodName = methodName.slice(evalLine + 9); // removing this prefix in method names
      stackLine.methodName = newMethodName; // eslint-disable-line
    }
    return true;
  });

  return newStack;
}

/**
 * Create an Express middleware for handling React Native symbolication requests
 */
function create(
  compiler: Compiler,
  { configOptions, configPath }: *
): Middleware {
  /**
   * The Express middleware for symbolicating'.
   */
  async function symbolicateMiddleware(req: $Request, res, next) {
    if (req.cleanPath !== '/symbolicate') return next();

    // grab the source stack frames
    const unconvertedFrames = getRequestedFrames(req);
    if (!unconvertedFrames || unconvertedFrames.length === 0) return next();

    // grab the platform and filename from the first frame (e.g. index.ios.bundle?platform=ios&dev=true&minify=false:69825:16)
    const filenameMatch = unconvertedFrames[0].file.match(/\/(\D+)\?/);
    const platformMatch = unconvertedFrames[0].file.match(
      /platform=([a-zA-Z]*)/
    );

    const filename: ?string = filenameMatch && filenameMatch[1];
    const platform: ?Platform = (platformMatch && platformMatch[1]: any);

    if (!filename || !platform) {
      return next();
    }

    const [name, ...rest] = filename.split('.');
    const bundleName = `${name}.${platform}.${rest[rest.length - 1]}`;

    // grab our source map consumer
    const consumer = await createSourceMapConsumer(
      compiler,
      // $FlowFixMe
      `http://localhost:${req.get('host').split(':')[1]}/${bundleName}.map`
    );
    // console.log('C', consumer);
    if (!consumer) return next();

    // the base directory
    const root = getConfig(configPath, configOptions, platform).context;

    // error error on the wall, who's the fairest stack of all?
    const convertedFrames = unconvertedFrames.map(
      (originalFrame): ReactNativeStackFrame => {
        // find the original home of this line of code.
        const lookup = consumer.originalPositionFor({
          line: originalFrame.lineNumber,
          column: originalFrame.column,
        });

        // If lookup fails, we get the same shape object, but with
        // all values set to null
        if (lookup.source == null) {
          // It is better to gracefully return the original frame
          // than to throw an exception
          return originalFrame;
        }

        // convert the original source into an absolute path
        const mappedFile = lookup.source
          .replace('webpack:///~', path.resolve(root, 'node_modules'))
          .replace('webpack://', root);

        // convert these to a format which React Native wants
        return {
          lineNumber: lookup.line,
          column: lookup.column,
          file: mappedFile,
          methodName: originalFrame.methodName,
        };
      }
    );

    // send it back to React Native
    const responseObject: ReactNativeSymbolicateResponse = {
      stack: convertedFrames,
    };
    const response = JSON.stringify(responseObject);
    res.end(response);

    return null;
  }

  return symbolicateMiddleware;
}

module.exports = create;


/**
 * Copyright 2017-present, Callstack.
 * All rights reserved.
 *
 * @noflow
 */

function fixRequireIssue() {
  return {
    visitor: {
      AssignmentExpression(path) {
        if (path.node.operator === '=') {
          const { left } = path.node;

          if (left.type !== 'MemberExpression') {
            return;
          }

          const { object } = left;

          if (
            // require.xxx
            (object.type === 'Identifier' && object.name === 'require') ||
            // (require: any).xxx
            (object.type === 'TypeCastExpression' &&
              object.expression.name === 'require')
          ) {
            path.remove();
          }
        }
      },
    },
  };
}

module.exports = fixRequireIssue;


/**
 * Copyright (c) 2015-present, Facebook, Inc.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @providesModule Array.prototype.es6
 * @polyfill
 * @nolint
 */

/* eslint-disable no-bitwise, no-extend-native, radix, no-self-compare */

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex
function findIndex(predicate, context) {
  if (this == null) {
    throw new TypeError(
      'Array.prototype.findIndex called on null or undefined'
    );
  }
  if (typeof predicate !== 'function') {
    throw new TypeError('predicate must be a function');
  }
  const list = Object(this);
  const length = list.length >>> 0;
  for (let i = 0; i < length; i++) {
    if (predicate.call(context, list[i], i, list)) {
      return i;
    }
  }
  return -1;
}

if (!Array.prototype.findIndex) {
  Object.defineProperty(Array.prototype, 'findIndex', {
    enumerable: false,
    writable: true,
    configurable: true,
    value: findIndex,
  });
}

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
if (!Array.prototype.find) {
  Object.defineProperty(Array.prototype, 'find', {
    enumerable: false,
    writable: true,
    configurable: true,
    value(predicate, context) {
      if (this == null) {
        throw new TypeError('Array.prototype.find called on null or undefined');
      }
      const index = findIndex.call(this, predicate, context);
      return index === -1 ? undefined : this[index];
    },
  });
}

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes
if (!Array.prototype.includes) {
  Object.defineProperty(Array.prototype, 'includes', {
    enumerable: false,
    writable: true,
    configurable: true,
    value(searchElement) {
      const O = Object(this);
      const len = parseInt(O.length) || 0;
      if (len === 0) {
        return false;
      }
      const n = parseInt(arguments[1]) || 0;
      let k;
      if (n >= 0) {
        k = n;
      } else {
        k = len + n;
        if (k < 0) {
          k = 0;
        }
      }
      let currentElement;
      while (k < len) {
        currentElement = O[k];
        if (
          searchElement === currentElement ||
          (searchElement !== searchElement && currentElement !== currentElement)
        ) {
          return true;
        }
        k++;
      }
      return false;
    },
  });
}


import { DemoManager } from "three-demo";
import { Vector2, WebGLRenderer } from "three";
import { EffectComposer } from "../../src";

import { BloomDemo } from "./demos/BloomDemo.js";
import { BlurDemo } from "./demos/BlurDemo.js";
import { BokehDemo } from "./demos/BokehDemo.js";
import { RealisticBokehDemo } from "./demos/RealisticBokehDemo.js";
import { ColorDepthDemo } from "./demos/ColorDepthDemo.js";
import { ColorGradingDemo } from "./demos/ColorGradingDemo.js";
import { GlitchDemo } from "./demos/GlitchDemo.js";
import { GodRaysDemo } from "./demos/GodRaysDemo.js";
import { OutlineDemo } from "./demos/OutlineDemo.js";
import { PatternDemo } from "./demos/PatternDemo.js";
import { PixelationDemo } from "./demos/PixelationDemo.js";
import { ShockWaveDemo } from "./demos/ShockWaveDemo.js";
import { SMAADemo } from "./demos/SMAADemo.js";
import { SSAODemo } from "./demos/SSAODemo.js";
import { TextureDemo } from "./demos/TextureDemo.js";
import { ToneMappingDemo } from "./demos/ToneMappingDemo.js";
import { VignetteDemo } from "./demos/VignetteDemo.js";
import { PerformanceDemo } from "./demos/PerformanceDemo.js";

/**
 * A renderer.
 *
 * @type {WebGLRenderer}
 * @private
 */

let renderer;

/**
 * An effect composer.
 *
 * @type {EffectComposer}
 * @private
 */

let composer;

/**
 * A demo manager.
 *
 * @type {DemoManager}
 * @private
 */

let manager;

/**
 * The main render loop.
 *
 * @private
 * @param {DOMHighResTimeStamp} now - The current time.
 */

function render(now) {

	requestAnimationFrame(render);
	manager.render(now);

}

/**
 * Handles demo change events.
 *
 * @private
 * @param {Event} event - An event.
 */

function onChange(event) {

	const demo = event.demo;

	// Make sure that the main renderer is being used and update it just in case.
	const size = composer.getRenderer().getSize(new Vector2());
	renderer.setSize(size.width, size.height);
	composer.replaceRenderer(renderer);
	composer.reset();
	composer.addPass(demo.renderPass);

	document.getElementById("viewport").children[0].style.display = "initial";

}

/**
 * Handles demo load events.
 *
 * @private
 * @param {Event} event - An event.
 */

function onLoad(event) {

	// Prepare the render pass.
	event.demo.renderPass.camera = event.demo.camera;

	document.getElementById("viewport").children[0].style.display = "none";

}

/**
 * Starts the program.
 *
 * @private
 * @param {Event} event - An event.
 */

window.addEventListener("load", function main(event) {

	// Clean up.
	this.removeEventListener("load", main);

	const viewport = document.getElementById("viewport");

	// Create a custom renderer.
	renderer = new WebGLRenderer({
		logarithmicDepthBuffer: true,
		antialias: false
	});

	renderer.debug.checkShaderErrors = true;
	renderer.setSize(viewport.clientWidth, viewport.clientHeight);
	renderer.setPixelRatio(window.devicePixelRatio);
	renderer.setClearColor(0x000000, 0.0);

	// Create an effect composer.
	composer = new EffectComposer(renderer, {
		stencilBuffer: true
	});

	// Initialise the demo manager.
	manager = new DemoManager(viewport, {
		aside: document.getElementById("aside"),
		renderer: renderer
	});

	// Setup demo switch and load event handlers.
	manager.addEventListener("change", onChange);
	manager.addEventListener("load", onLoad);

	const demos = [
		new BloomDemo(composer),
		new BlurDemo(composer),
		new BokehDemo(composer),
		new RealisticBokehDemo(composer),
		new ColorDepthDemo(composer),
		new ColorGradingDemo(composer),
		new GlitchDemo(composer),
		new GodRaysDemo(composer),
		new OutlineDemo(composer),
		new PatternDemo(composer),
		new PixelationDemo(composer),
		new ShockWaveDemo(composer),
		new SMAADemo(composer),
		new SSAODemo(composer),
		new TextureDemo(composer),
		new ToneMappingDemo(composer),
		new VignetteDemo(composer),
		new PerformanceDemo(composer)
	];

	if(demos.map((demo) => demo.id).indexOf(window.location.hash.slice(1)) === -1) {

		// Invalid URL hash: demo doesn't exist.
		window.location.hash = "";

	}

	// Register demos.
	for(const demo of demos) {

		manager.addDemo(demo);

	}

	// Start rendering.
	render();

});

/**
 * Handles browser resizing.
 *
 * @private
 * @param {Event} event - An event.
 */

window.addEventListener("resize", (function() {

	let timeoutId = 0;

	function handleResize(event) {

		const width = event.target.innerWidth;
		const height = event.target.innerHeight;

		manager.setSize(width, height);
		composer.setSize(width, height);

		timeoutId = 0;

	}

	return function onResize(event) {

		if(timeoutId === 0) {

			timeoutId = setTimeout(handleResize, 66, event);

		}

	};

}()));

/**
 * Toggles the visibility of the interface on Alt key press.
 *
 * @private
 * @param {Event} event - An event.
 */

document.addEventListener("keydown", function onKeyDown(event) {

	const aside = this.getElementById("aside");

	if(event.altKey && aside !== null) {

		event.preventDefault();
		aside.style.visibility = (aside.style.visibility === "hidden") ? "visible" : "hidden";

	}

});