/**
 * Copyright 2017 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

'use strict';
var cls = require('./cls.js');
var constants = require('./constants.js');
var extend = require('extend');
var is = require('is');
var TraceLabels = require('./trace-labels.js');

/**
 * This file describes an interface for third-party plugins to enable tracing
 * for arbitrary modules.
 */

/**
 * An object that represents a single child span. It exposes functions for
 * adding labels to or closing the span.
 * @param {TraceAgent} agent The underlying trace agent object.
 * @param {SpanData} span The internal data structure backing the child span.
 */
function ChildSpan(agent, span) {
  this.agent_ = agent;
  this.span_ = span;
  this.serializedTraceContext_ = agent.generateTraceContext(span, true);
}

/**
 * Adds a label to the child span.
 * @param {string} key The name of the label to add.
 * @param {*} value The value of the label to add.
 */
ChildSpan.prototype.addLabel = function(key, value) {
  this.span_.addLabel(key, value);
};

/**
 * Ends the child span. This function should only be called once.
 */
ChildSpan.prototype.endSpan = function() {
  this.span_.close();
};

/**
 * Gets the trace context serialized as a string. This string can be set as the
 * 'x-cloud-trace-context' field in an HTTP request header to support
 * distributed tracing.
 */
ChildSpan.prototype.getTraceContext = function() {
  return this.serializedTraceContext_;
};

/**
 * An object that represents a single root span. It exposes functions for adding
 * labels to or closing the span.
 * @param {TraceAgent} agent The underlying trace agent object.
 * @param {SpanData} span The internal data structure backing the root span.
 */
function RootSpan(agent, span) {
  this.agent_ = agent;
  this.span_ = span;
  this.serializedTraceContext_ = agent.generateTraceContext(span, true);
}

/**
 * Adds a label to the span.
 * @param {string} key The name of the label to add.
 * @param {*} value The value of the label to add.
 */
RootSpan.prototype.addLabel = function(key, value) {
  this.span_.addLabel(key, value);
};

/**
 * Ends the span. This function should only be called once.
 */
RootSpan.prototype.endSpan = function() {
  this.span_.close();
};

/**
 * Gets the trace context serialized as a string. This string can be set as the
 * 'x-cloud-trace-context' field in an HTTP request header to support
 * distributed tracing.
 */
RootSpan.prototype.getTraceContext = function() {
  return this.serializedTraceContext_;
};

// A sentinal stored in CLS to indicate that the current request was not sampled.
var nullSpan = {};

/**
 * The functional implementation of the Trace API
 */
function TraceApiImplementation(agent, pluginName) {
  this.agent_ = agent;
  this.logger_ = agent.logger;
  this.pluginName_ = pluginName;
}

/**
 * Gets the value of enhancedDatabaseReporting in the trace agent's
 * configuration object.
 * @returns A boolean value indicating whether the trace agent was configured
 * to have an enhanced level of reporting enabled.
 */
TraceApiImplementation.prototype.enhancedDatabaseReportingEnabled = function() {
  return this.agent_.config_.enhancedDatabaseReporting;
};

/**
 * Runs the given function in a root span corresponding to an incoming request,
 * possibly passing it an object that exposes an interface for adding labels
 * and closing the span.
 * @param {object} options An object that specifies options for how the root
 * span is created and propogated.
 * @param {string} options.name The name to apply to the root span.
 * @param {?string} options.url A URL associated with the root span, if
 * applicable.
 * @param {?string} options.traceContext The serialized form of an object that
 * contains information about an existing trace context.
 * @param {?number} options.skipFrames The number of stack frames to skip when
 * collecting call stack information for the root span, starting from the top;
 * this should be set to avoid including frames in the plugin. Defaults to 0.
 * @param {function(?RootSpan)} fn A function that will be called exactly
 * once. If the incoming request should be traced, a root span will be created,
 * and this function will be called with a RootSpan object exposing functions
 * operating on the root span; otherwise, it will be called with null as an
 * argument.
 * @returns The return value of calling fn.
 */
TraceApiImplementation.prototype.runInRootSpan = function(options, fn) {
  var that = this;
  if (!this.agent_.namespace) {
    this.logger_.warn(this.pluginName_ + ': CLS namespace not present; not ' +
      'running in root span.');
    return fn(null);
  }
  if (cls.getRootContext()) {
    this.logger_.warn(this.pluginName_ + ': Cannot create nested root spans.');
    return fn(null);
  }
  return this.agent_.namespace.runAndReturn(function() {
    var skipFrames = options.skipFrames ? options.skipFrames + 3 : 3;
    var rootSpan = createRootSpan_(that, options, skipFrames);
    return fn(rootSpan);
  });
};

/**
 * Creates and returns a new ChildSpan object nested within the root span. If
 * there is no current RootSpan object, this function returns null.
 * @param {object} options An object that specifies options for how the child
 * span is created and propogated.
 * @param {string} options.name The name to apply to the child span.
 * @param {?number} options.skipFrames The number of stack frames to skip when
 * collecting call stack information for the root span, starting from the top;
 * this should be set to avoid including frames in the plugin. Defaults to 0.
 * @returns A new ChildSpan object, or null if there is no active root span.
 */
TraceApiImplementation.prototype.createChildSpan = function(options) {
  var rootSpan = cls.getRootContext();
  if (!rootSpan) {
    // Lost context
    this.logger_.warn(this.pluginName_ + ': Attempted to create child span ' +
      'without root');
    return null;
  } else if (rootSpan === nullSpan) {
    // Chose not to sample
    return null;
  } else {
    options = options || {};
    var childContext = this.agent_.startSpan(options.name, {},
      options.skipFrames ? options.skipFrames + 2 : 2);
    return new ChildSpan(this.agent_, childContext);
  }
};

/**
 * Generates a stringified trace context that should be set as the trace context
 * header in a response to an incoming web request. This value is based on
 * the trace context header value in the corresponding incoming request, as well
 * as the result from the local trace policy on whether this request will be
 * traced or not.
 * @param {string} incomingTraceContext The trace context that was attached to
 * the incoming web request, or null if the incoming request didn't have one.
 * @param {boolean} isTraced Whether the incoming was traced. This is determined
 * by the local tracing policy.
 * @returns {string} If the response should contain the trace context within its
 * header, the string to be set as this header's value. Otherwise, an empty
 * string.
 */
TraceApiImplementation.prototype.getResponseTraceContext = function(
    incomingTraceContext, isTraced) {
  var traceContext = this.agent_.parseContextFromHeader(incomingTraceContext);
  if (!traceContext) {
    return '';
  }
  traceContext.options = traceContext.options & isTraced;
  return traceContext.traceId + '/' + traceContext.spanId + ';o=' +
    traceContext.options;
};

/**
 * Binds the trace context to the given function.
 * This is necessary in order to create child spans correctly in functions
 * that are called asynchronously (for example, in a network response handler).
 * @param {function} fn A function to which to bind the trace context.
 */
TraceApiImplementation.prototype.wrap = function(fn) {
  if (!this.agent_.namespace) {
    this.logger_.warn(this.pluginName_ + ': No CLS namespace to bind ' +
      'function');
    return fn;
  }
  return this.agent_.namespace.bind(fn);
};

/**
 * Binds the trace context to the given event emitter.
 * This is necessary in order to create child spans correctly in event handlers.
 * @param {EventEmitter} emitter An event emitter whose handlers should have
 * the trace context binded to them.
 */
TraceApiImplementation.prototype.wrapEmitter = function(emitter) {
  if (!this.agent_.namespace) {
    this.logger_.warn(this.pluginName_ + ': No CLS namespace to bind ' +
      'emitter to');
  }
  this.agent_.namespace.bindEmitter(emitter);
};

TraceApiImplementation.prototype.constants = constants;

TraceApiImplementation.prototype.labels = TraceLabels;

/**
 * Phantom implementation of the trace api. This allows API users to decouple
 * the enable/disable logic from the calls to the tracing API. The phantom API
 * has a lower overhead than isEnabled checks inside the API functions.
 * @private
 */
var phantomApiImpl = {
  enhancedDatabaseReportingEnabled: function() { return false; },
  runInRootSpan: function(opts, fn) { return fn(null); },
  createChildSpan: function(opts) { return null; },
  getResponseTraceContext: function(context, traced) { return ''; },
  wrap: function(fn) { return fn; },
  wrapEmitter: function(ee) {},
  constants: constants,
  labels: TraceLabels
};

/**
 * Creates an object that provides an interface to the trace agent
 * implementation.
 * Upon creation, the object is in an "uninitialized" state, corresponding
 * to its intended (no-op) behavior before the trace agent is started.
 * When the trace agent is started, the interface object becomes
 * "initialized", and its underlying implementation is switched to that of
 * the actual agent implementation.
 * Finally, when the trace agent is stopped, this object enters the "disabled"
 * state, and its underlying implementation is switched back to no-op.
 * Currently, this only happens when the application's GCP project ID could
 * not be determined from the GCP metadata service.
 * This object's state changes strictly from uninitialized to initialized,
 * and from initialized to disabled.
 */
module.exports = function TraceApi(pluginName) {
  var impl = phantomApiImpl;
  extend(this, {
    enhancedDatabaseReportingEnabled: function() {
      return impl.enhancedDatabaseReportingEnabled();
    },
    runInRootSpan: function(opts, fn) {
      return impl.runInRootSpan(opts, fn);
    },
    createChildSpan: function(opts) {
      return impl.createChildSpan(opts);
    },
    getResponseTraceContext: function(incomingTraceContext, isTraced) {
      return impl.getResponseTraceContext(incomingTraceContext, isTraced);
    },
    wrap: function(fn) {
      return impl.wrap(fn);
    },
    wrapEmitter: function(ee) {
      return impl.wrapEmitter(ee);
    },
    constants: impl.constants,
    labels: impl.labels,
    isActive: function() {
      return impl !== phantomApiImpl;
    },
    enable_: function(agent) {
      impl = new TraceApiImplementation(agent, pluginName);
    },
    disable_: function() {
      impl = phantomApiImpl;
    },
    private_: function() { return impl.agent_; }
  });
  return this;
};

// Module-private functions

function createRootSpan_(api, options, skipFrames) {
  options = options || {};
  // If the options object passed in has the getTraceContext field set,
  // try to retrieve the header field containing incoming trace metadata.
  var incomingTraceContext;
  if (is.string(options.traceContext)) {
    incomingTraceContext = api.agent_.parseContextFromHeader(options.traceContext);
  }
  incomingTraceContext = incomingTraceContext || {};
  if (!api.agent_.shouldTrace(options, incomingTraceContext.options)) {
    cls.setRootContext(nullSpan);
    return null;
  }
  var rootContext = api.agent_.createRootSpanData(options.name,
    incomingTraceContext.traceId,
    incomingTraceContext.spanId,
    skipFrames + 1);
  return new RootSpan(api.agent_, rootContext);
}
