// The purpose of the `Content` object is to abstract away the data conversions // to and from raw content entities as strings. For example, you want to be able // to pass in a Javascript object and have it be automatically converted into a // JSON string if the `content-type` is set to a JSON-based media type. // Conversely, you want to be able to transparently get back a Javascript object // in the response if the `content-type` is a JSON-based media-type.

// One limitation of the current implementation is that it [assumes the `charset` is UTF-8](github.com/spire-io/shred/issues/5).

// The `Content` constructor takes an options object, which must have either a // `body` or `data` property and may have a `type` property indicating the // media type. If there is no `type` attribute, a default will be inferred. var Content = function(options) {

this.body = options.body;
this.data = options.data;
this.type = options.type;

};

Content.prototype = {

// Treat `toString()` as asking for the `content.body`. That is, the raw content entity.
//
//     toString: function() { return this.body; }
//
// Commented out, but I've forgotten why. :/

};

// `Content` objects have the following attributes: Object.defineProperties(Content.prototype,{

// - type. Typically accessed as `content.type`, reflects the `content-type` // header associated with the request or response. If not passed as an options // to the constructor or set explicitly, it will infer the type the `data` // attribute, if possible, and, failing that, will default to `text/plain`.

type: {
  get: function() {
    if (this._type) {
      return this._type;
    } else {
      if (this._data) {
        switch(typeof this._data) {
          case "string": return "text/plain";
          case "object": return "application/json";
        }
      }
    }
    return "text/plain";
  },
  set: function(value) {
    this._type = value;
    return this;
  },
  enumerable: true
},

// - data. Typically accessed as `content.data`, reflects the content entity // converted into Javascript data. This can be a string, if the `type` is, say, // `text/plain`, but can also be a Javascript object. The conversion applied is // based on the `processor` attribute. The `data` attribute can also be set // directly, in which case the conversion will be done the other way, to infer // the `body` attribute.

data: {
  get: function() {
    if (this._body) {
      return this.processor.parser(this._body);
    } else {
      return this._data;
    }
  },
  set: function(data) {
    if (this._body&&data) Errors.setDataWithBody(this);
    this._data = data;
    return this;
  },
  enumerable: true
},

// - body. Typically accessed as `content.body`, reflects the content entity // as a UTF-8 string. It is the mirror of the `data` attribute. If you set the // `data` attribute, the `body` attribute will be inferred and vice-versa. If // you attempt to set both, an exception is raised.

body: {
  get: function() {
    if (this._data) {
      return this.processor.stringify(this._data);
    } else {
      return this._body.toString();
    }
  },
  set: function(body) {
    if (this._data&&body) Errors.setBodyWithData(this);
    this._body = body;
    return this;
  },
  enumerable: true
},

// - processor. The functions that will be used to convert to/from `data` and // `body` attributes. You can add processors. The two that are built-in are for // `text/plain`, which is basically an identity transformation and // `application/json` and other JSON-based media types (including custom media // types with `+json`). You can add your own processors. See below.

processor: {
  get: function() {
    var processor = Content.processors[this.type];
    if (processor) {
      return processor;
    } else {
      // Return the first processor that matches any part of the
      // content type. ex: application/vnd.foobar.baz+json will match json.
      var main = this.type.split(";")[0];
      var parts = main.split(/\+|\//);
      for (var i=0, l=parts.length; i < l; i++) {
        processor = Content.processors[parts[i]]
      }
      return processor || {parser:identity,stringify:toString};
    }
  },
  enumerable: true
},

// - length. Typically accessed as `content.length`, returns the length in // bytes of the raw content entity.

length: {
  get: function() {
    if (typeof Buffer !== 'undefined') {
      return Buffer.byteLength(this.body);
    }
    return this.body.length;
  }
}

});

Content.processors = {};

// The `registerProcessor` function allows you to add your own processors to // convert content entities. Each processor consists of a Javascript object with // two properties: // - parser. The function used to parse a raw content entity and convert it // into a Javascript data type. // - stringify. The function used to convert a Javascript data type into a // raw content entity. Content.registerProcessor = function(types,processor) {

// You can pass an array of types that will trigger this processor, or just one. // We determine the array via duck-typing here.

if (types.forEach) {
  types.forEach(function(type) {
    Content.processors[type] = processor;
  });
} else {
  // If you didn't pass an array, we just use what you pass in.
  Content.processors[types] = processor;
}

};

// Register the identity processor, which is used for text-based media types. var identity = function(x) { return x; }

, toString = function(x) { return x.toString(); }

Content.registerProcessor(

["text/html","text/plain","text"],
{ parser: identity, stringify: toString });

// Register the JSON processor, which is used for JSON-based media types. Content.registerProcessor(

["application/json; charset=utf-8","application/json","json"],
{
  parser: function(string) {
    return JSON.parse(string);
  },
  stringify: function(data) {
    return JSON.stringify(data); }});

var qs = require('querystring'); // Register the post processor, which is used for JSON-based media types. Content.registerProcessor(

["application/x-www-form-urlencoded"],
{ parser : qs.parse, stringify : qs.stringify });

// Error functions are defined separately here in an attempt to make the code // easier to read. var Errors = {

setDataWithBody: function(object) {
  throw new Error("Attempt to set data attribute of a content object " +
      "when the body attributes was already set.");
},
setBodyWithData: function(object) {
  throw new Error("Attempt to set body attribute of a content object " +
      "when the data attributes was already set.");
}

} module.exports = Content;