'use strict';

const Stream = require('stream');
const Readable = Stream.Readable;
const binding = process.binding('http2');
const constants = binding.constants;
const errors = require('internal/errors');

const kFinish = Symbol('finish');
const kBeginSend = Symbol('begin-send');
const kState = Symbol('state');
const kStream = Symbol('stream');
const kRequest = Symbol('request');
const kResponse = Symbol('response');
const kHeaders = Symbol('headers');
const kTrailers = Symbol('trailers');

let statusMessageWarned = false;

// Defines and implements an API compatibility layer on top of the core
// HTTP/2 implementation, intended to provide an interface that is as
// close as possible to the current require('http') API

function assertValidHeader(name, value) {
  if (isPseudoHeader(name))
    throw new errors.Error('ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED');
  if (value === undefined || value === null)
    throw new errors.TypeError('ERR_HTTP2_INVALID_HEADER_VALUE');
}

function isPseudoHeader(name) {
  switch (name) {
    case ':status':
      return true;
    case ':method':
      return true;
    case ':path':
      return true;
    case ':authority':
      return true;
    case ':scheme':
      return true;
    default:
      return false;
  }
}

function onStreamData(chunk) {
  const request = this[kRequest];
  if (!request.push(chunk))
    this.pause();
}

function onStreamEnd() {
  // Cause the request stream to end as well.
  const request = this[kRequest];
  request.push(null);
}

function onStreamError(error) {
  const request = this[kRequest];
  request.emit('error', error);
}

function onRequestPause() {
  const stream = this[kStream];
  stream.pause();
}

function onRequestResume() {
  const stream = this[kStream];
  stream.resume();
}

function onRequestDrain() {
  if (this.isPaused())
    this.resume();
}

function onStreamResponseDrain() {
  const response = this[kResponse];
  response.emit('drain');
}

function onStreamResponseError(error) {
  const response = this[kResponse];
  response.emit('error', error);
}

function onStreamClosedRequest() {
  const req = this[kRequest];
  req.push(null);
}

function onStreamClosedResponse() {
  const res = this[kResponse];
  res.writable = false;
  res.emit('finish');
}

function onAborted(hadError, code) {
  if ((this.writable) ||
      (this._readableState && !this._readableState.ended)) {
    this.emit('aborted', hadError, code);
  }
}

class Http2ServerRequest extends Readable {
  constructor(stream, headers, options) {
    super(options);
    this[kState] = {
      statusCode: null,
      closed: false,
      closedCode: constants.NGHTTP2_NO_ERROR
    };
    this[kHeaders] = headers;
    this[kStream] = stream;
    stream[kRequest] = this;

    // Pause the stream..
    stream.pause();
    stream.on('data', onStreamData);
    stream.on('end', onStreamEnd);
    stream.on('error', onStreamError);
    stream.on('close', onStreamClosedRequest);
    stream.on('aborted', onAborted.bind(this));
    const onfinish = this[kFinish].bind(this);
    stream.on('streamClosed', onfinish);
    stream.on('finish', onfinish);
    this.on('pause', onRequestPause);
    this.on('resume', onRequestResume);
    this.on('drain', onRequestDrain);
  }

  get closed() {
    const state = this[kState];
    return Boolean(state.closed);
  }

  get code() {
    const state = this[kState];
    return Number(state.closedCode);
  }

  get stream() {
    return this[kStream];
  }

  get statusCode() {
    return this[kState].statusCode;
  }

  get headers() {
    return this[kHeaders];
  }

  get rawHeaders() {
    const headers = this[kHeaders];
    if (headers === undefined)
      return [];
    const tuples = Object.entries(headers);
    const flattened = Array.prototype.concat.apply([], tuples);
    return flattened.map(String);
  }

  get trailers() {
    return this[kTrailers];
  }

  get httpVersionMajor() {
    return 2;
  }

  get httpVersionMinor() {
    return 0;
  }

  get httpVersion() {
    return '2.0';
  }

  get socket() {
    return this.stream.session.socket;
  }

  get connection() {
    return this.socket;
  }

  _read(nread) {
    const stream = this[kStream];
    if (stream) {
      stream.resume();
    } else {
      this.emit('error', new errors.Error('ERR_HTTP2_STREAM_CLOSED'));
    }
  }

  get method() {
    const headers = this[kHeaders];
    if (headers === undefined)
      return;
    return headers[constants.HTTP2_HEADER_METHOD];
  }

  get authority() {
    const headers = this[kHeaders];
    if (headers === undefined)
      return;
    return headers[constants.HTTP2_HEADER_AUTHORITY];
  }

  get scheme() {
    const headers = this[kHeaders];
    if (headers === undefined)
      return;
    return headers[constants.HTTP2_HEADER_SCHEME];
  }

  get url() {
    return this.path;
  }

  set url(url) {
    this.path = url;
  }

  get path() {
    const headers = this[kHeaders];
    if (headers === undefined)
      return;
    return headers[constants.HTTP2_HEADER_PATH];
  }

  set path(path) {
    let headers = this[kHeaders];
    if (headers === undefined)
      headers = this[kHeaders] = Object.create(null);
    headers[constants.HTTP2_HEADER_PATH] = path;
  }

  setTimeout(msecs, callback) {
    const stream = this[kStream];
    if (stream === undefined) return;
    stream.setTimeout(msecs, callback);
  }

  [kFinish](code) {
    const state = this[kState];
    if (state.closed)
      return;
    state.closedCode = code;
    state.closed = true;
    this.push(null);
    this[kStream] = undefined;
  }
}

class Http2ServerResponse extends Stream {
  constructor(stream, options) {
    super(options);
    this[kState] = {
      sendDate: true,
      statusCode: constants.HTTP_STATUS_OK,
      headerCount: 0,
      trailerCount: 0,
      closed: false,
      closedCode: constants.NGHTTP2_NO_ERROR
    };
    this[kStream] = stream;
    stream[kResponse] = this;
    this.writable = true;
    stream.on('drain', onStreamResponseDrain);
    stream.on('error', onStreamResponseError);
    stream.on('close', onStreamClosedResponse);
    stream.on('aborted', onAborted.bind(this));
    const onfinish = this[kFinish].bind(this);
    stream.on('streamClosed', onfinish);
    stream.on('finish', onfinish);
  }

  get finished() {
    const stream = this[kStream];
    return stream === undefined || stream._writableState.ended;
  }

  get closed() {
    const state = this[kState];
    return Boolean(state.closed);
  }

  get code() {
    const state = this[kState];
    return Number(state.closedCode);
  }

  get stream() {
    return this[kStream];
  }

  get headersSent() {
    const stream = this[kStream];
    return stream.headersSent;
  }

  get sendDate() {
    return Boolean(this[kState].sendDate);
  }

  set sendDate(bool) {
    this[kState].sendDate = Boolean(bool);
  }

  get statusCode() {
    return this[kState].statusCode;
  }

  set statusCode(code) {
    const state = this[kState];
    code |= 0;
    if (code >= 100 && code < 200)
      throw new errors.RangeError('ERR_HTTP2_INFO_STATUS_NOT_ALLOWED');
    if (code < 200 || code > 599)
      throw new errors.RangeError('ERR_HTTP2_STATUS_INVALID', code);
    state.statusCode = code;
  }

  addTrailers(headers) {
    let trailers = this[kTrailers];
    const keys = Object.keys(headers);
    if (keys.length === 0)
      return;
    if (trailers === undefined)
      trailers = this[kTrailers] = Object.create(null);
    for (var i = 0; i < keys.length; i++) {
      trailers[keys[i]] = String(headers[keys[i]]);
    }
  }

  getHeader(name) {
    const headers = this[kHeaders];
    if (headers === undefined)
      return;
    name = String(name).trim().toLowerCase();
    return headers[name];
  }

  getHeaderNames() {
    const headers = this[kHeaders];
    if (headers === undefined)
      return [];
    return Object.keys(headers);
  }

  getHeaders() {
    const headers = this[kHeaders];
    return Object.assign({}, headers);
  }

  hasHeader(name) {
    const headers = this[kHeaders];
    if (headers === undefined)
      return false;
    name = String(name).trim().toLowerCase();
    return Object.prototype.hasOwnProperty.call(headers, name);
  }

  removeHeader(name) {
    const headers = this[kHeaders];
    if (headers === undefined)
      return;
    name = String(name).trim().toLowerCase();
    delete headers[name];
  }

  setHeader(name, value) {
    name = String(name).trim().toLowerCase();
    assertValidHeader(name, value);
    let headers = this[kHeaders];
    if (headers === undefined)
      headers = this[kHeaders] = Object.create(null);
    headers[name] = String(value);
  }

  get statusMessage() {
    if (statusMessageWarned === false) {
      process.emitWarning(
        'Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)',
        'UnsupportedWarning'
      );
      statusMessageWarned = true;
    }

    return '';
  }

  flushHeaders() {
    if (this[kStream].headersSent === false)
      this[kBeginSend]();
  }

  writeHead(statusCode, statusMessage, headers) {
    if (typeof statusMessage === 'string' && statusMessageWarned === false) {
      process.emitWarning(
        'Status message is not supported by HTTP/2 (RFC7540 8.1.2.4)',
        'UnsupportedWarning'
      );
      statusMessageWarned = true;
    }
    if (headers === undefined && typeof statusMessage === 'object') {
      headers = statusMessage;
    }

    const stream = this[kStream];
    if (stream.headersSent === true) {
      throw new errors.Error('ERR_HTTP2_INFO_HEADERS_AFTER_RESPOND');
    }

    if (headers) {
      const keys = Object.keys(headers);
      let key = '';
      for (var i = 0; i < keys.length; i++) {
        key = keys[i];
        this.setHeader(key, headers[key]);
      }
    }

    this.statusCode = statusCode;
    this[kBeginSend]();
  }

  write(chunk, encoding, cb) {
    const stream = this[kStream];

    if (typeof encoding === 'function') {
      cb = encoding;
      encoding = 'utf8';
    }

    if (stream === undefined) {
      const err = new errors.Error('ERR_HTTP2_STREAM_CLOSED');
      if (cb)
        process.nextTick(cb, err);
      else
        throw err;
      return;
    }
    this[kBeginSend]();
    return stream.write(chunk, encoding, cb);
  }

  end(chunk, encoding, cb) {
    const stream = this[kStream];

    if (typeof chunk === 'function') {
      cb = chunk;
      chunk = null;
      encoding = 'utf8';
    } else if (typeof encoding === 'function') {
      cb = encoding;
      encoding = 'utf8';
    }
    if (chunk !== null && chunk !== undefined) {
      this.write(chunk, encoding);
    }

    if (typeof cb === 'function' && stream !== undefined) {
      stream.once('finish', cb);
    }

    this[kBeginSend]({ endStream: true });

    if (stream !== undefined) {
      stream.end();
    }
  }

  destroy(err) {
    const stream = this[kStream];
    if (stream === undefined) {
      // nothing to do, already closed
      return;
    }
    stream.destroy(err);
  }

  setTimeout(msecs, callback) {
    const stream = this[kStream];
    if (stream === undefined) return;
    stream.setTimeout(msecs, callback);
  }

  createPushResponse(headers, callback) {
    const stream = this[kStream];
    if (stream === undefined) {
      process.nextTick(callback, new errors.Error('ERR_HTTP2_STREAM_CLOSED'));
      return;
    }
    stream.pushStream(headers, {}, function(stream, headers, options) {
      const response = new Http2ServerResponse(stream);
      callback(null, response);
    });
  }

  [kBeginSend](options) {
    const stream = this[kStream];
    options = options || Object.create(null);
    if (stream !== undefined && stream.headersSent === false) {
      const state = this[kState];
      const headers = this[kHeaders] || Object.create(null);
      headers[constants.HTTP2_HEADER_STATUS] = state.statusCode;
      if (stream.finished === true)
        options.endStream = true;
      options.getTrailers = (trailers) => {
        Object.assign(trailers, this[kTrailers]);
      };
      if (stream.destroyed === false) {
        stream.respond(headers, options);
      }
    }
  }

  [kFinish](code) {
    const state = this[kState];
    if (state.closed)
      return;
    state.closedCode = code;
    state.closed = true;
    this.end();
    this[kStream] = undefined;
    this.emit('finish');
  }

  writeContinue() {
    // TODO mcollina check what is the continue flow
    throw new Error('not implemented yet');
  }
}

function onServerStream(stream, headers, flags) {
  const server = this;
  const request = new Http2ServerRequest(stream, headers);
  const response = new Http2ServerResponse(stream);

  // Check for the CONNECT method
  const method = headers[constants.HTTP2_HEADER_METHOD];
  if (method === 'CONNECT') {
    if (!server.emit('connect', request, response)) {
      response.statusCode = constants.HTTP_STATUS_METHOD_NOT_ALLOWED;
      response.end();
    }
    return;
  }

  // Check for Expectations
  if (headers.expect !== undefined) {
    if (headers.expect === '100-continue') {
      if (server.listenerCount('checkContinue')) {
        server.emit('checkContinue', request, response);
      } else {
        response.sendContinue();
        server.emit('request', request, response);
      }
    } else if (server.listenerCount('checkExpectation')) {
      server.emit('checkExpectation', request, response);
    } else {
      response.statusCode = constants.HTTP_STATUS_EXPECTATION_FAILED;
      response.end();
    }
    return;
  }

  server.emit('request', request, response);
}

module.exports = { onServerStream };
