/* eslint-disable no-console, max-classes-per-file */
import isStream from 'is-stream';
import getStream from 'get-stream';
import Stream from 'stream';
import path from 'path';
import mimeTypes from 'mime-types';
import { Context, Request } from 'koa';
import { isBinaryFile } from 'isbinaryfile';
import { virtualFilePrefix } from '../constants';

let _debug = false;

export function setDebug(debug: boolean) {
  _debug = debug;
}

export function logDebug(...messages: any[]) {
  if (_debug) {
    console.log('[es-dev-server]: ', ...messages.map(m => JSON.stringify(m)));
  }
}

export class RequestCancelledError extends Error {}
export class IsBinaryFileError extends Error {}

/**
 * koa-static stores the original served file path on ctx.body.path,
 * we need this path for file transformation but we overwrite body
 * with a string, so we keep a reference with a weakmap
 */
const filePathsForRequests = new WeakMap<Request, string>();

export function isUtf8(context: Context) {
  const charSet = mimeTypes.charset(context.response.get('content-type'));
  return charSet === false ? false : charSet.toLowerCase() === 'utf-8';
}

/**
 * Returns the body value as string. If the response is a stream, the
 * stream is drained and the result is returned. Because koa-static stores
 * a path variable on the stream, we need to create a new stream with
 * the same variable to preserve correct behavior.
 *
 */
export async function getBodyAsString(ctx: Context): Promise<string> {
  let requestCanceled;
  ctx.req.on('close', () => {
    requestCanceled = true;
  });

  if (Buffer.isBuffer(ctx.body)) {
    return ctx.body.toString();
  }

  if (typeof ctx.body === 'string') {
    return ctx.body;
  }

  if (isStream(ctx.body)) {
    // cache request path, see above
    // @ts-ignore
    if (ctx.body.path) {
      // @ts-ignore
      filePathsForRequests.set(ctx.request, ctx.body.path);
    }

    // a stream can only be read once, so after reading it assign
    // the string response to the body so that it can be accessed
    // again later
    try {
      const bodyBuffer = await getStream.buffer(ctx.body);
      const contentLength = Number(ctx.response.get('content-length'));

      if (await isBinaryFile(bodyBuffer, contentLength)) {
        ctx.body = bodyBuffer;
        throw new IsBinaryFileError();
      }

      const bodyString = bodyBuffer.toString();
      ctx.body = bodyString;
      return bodyString;
    } catch (error) {
      if (requestCanceled) {
        throw new RequestCancelledError();
      }
      throw error;
    }
  }

  return ctx.body;
}

/**
 * Turns a file path into a path suitable for browsers, with a / as seperator.
 * @param {string} filePath
 * @returns {string}
 */
export function toBrowserPath(filePath: string) {
  return filePath.replace(new RegExp(path.sep === '\\' ? '\\\\' : path.sep, 'g'), '/');
}

/**
 * Transforms a file system path to a browser URL. For example windows uses `\` on the file system,
 * but it should use `/` in the browser.
 */
export function toFilePath(browserPath: string) {
  return browserPath.replace(new RegExp('/', 'g'), path.sep);
}

export class SSEStream extends Stream.Transform {
  sendMessage(name: string, data = '') {
    this.write(`event: ${name}\ndata: ${data}\n\n`);
  }

  _transform(data: string, enc: string, cb: () => void) {
    this.push(data.toString());
    cb();
  }
}

/**
 * Returns whether this is a HTML document response
 *
 * TODO: Rename to isHTMLResponse
 */
export function isIndexHTMLResponse(ctx: Context, appIndex?: string): boolean {
  if (ctx.status < 200 || ctx.status >= 300) {
    return false;
  }

  // if we're serving the app index, it's an index html response
  if (appIndex && ctx.url === appIndex) {
    return true;
  }

  // make the check based on content-type and `accept` request header value
  const contentType = ctx.response.header && ctx.response.header['content-type'];
  const acceptType = ctx.request.header && ctx.request.header.accept;

  return (
    contentType &&
    contentType.includes('text/html') &&
    acceptType &&
    acceptType.includes('text/html')
  );
}

export function isPolyfill(url: string) {
  return url.includes('/polyfills/');
}

export function shouldTransformToModule(url: string) {
  return url.includes('transform-systemjs');
}

export function isInlineScript(url: string) {
  return url.includes(`inline-script-`) && url.includes('?source=');
}

/**
 * Returns whether this is a file generated generated by the dev server
 */
export function isGeneratedFile(url: string) {
  return url.startsWith(virtualFilePrefix) || isPolyfill(url) || isInlineScript(url);
}

export function getRequestFilePath(ctx: Context, rootDir: string): string | null {
  if (isPolyfill(ctx.url)) {
    return null;
  }

  // inline module requests have the source in a query string
  if (isInlineScript(ctx.url)) {
    const url = ctx.url.split('?')[0];
    const indexPath = toFilePath(url);
    return path.join(rootDir, indexPath);
  }

  // otherwise koa-static adds the original file path on the body
  if (ctx.body && ctx.body.path) {
    return ctx.body.path;
  }

  return filePathsForRequests.get(ctx.request) || null;
}
