/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global require, exports */
'use strict';

const {
  Cc, Ci
} = require('chrome');
const {
  Class
} = require('sdk/core/heritage');
const CP_NS = require('sdk/core/namespace').ns();
const {
  ensure
} = require('sdk/system/unload');
const {
  validateOptions
} = require('sdk/deprecated/api-utils');
const {
  id: ADDON_ID
} = require('sdk/self');
const xpcom = require('sdk/platform/xpcom');

const CM = Cc["@mozilla.org/categorymanager;1"]
  .getService(Ci.nsICategoryManager);

const ACCEPT = exports.ACCEPT = Ci.nsIContentPolicy.ACCEPT;
const REJECT = exports.REJECT = Ci.nsIContentPolicy.REJECT_REQUEST;

const accept = function () ACCEPT;

let ContentPolicy_ID = 0;

const RULES = {
  description: {
    map: function (v) {
      return v ? v : '';
    },
    is: ['string']
  },
  contract: {
    map: function (v) {
      if (v === undefined) {
        v = '@erikvold.com/content-policy.' + ADDON_ID + ';' + ContentPolicy_ID++;
      }
      return v;
    },
    is: ['string']
  },
  entry: {
    is: ['string', 'undefined']
  },
  shouldLoad: {
    is: ['function', 'undefined']
  },
  shouldProcess: {
    is: ['function', 'undefined']
  },
};

function getType(aType) {
  switch (aType) {
  case Ci.nsIContentPolicy.TYPE_SCRIPT:
    return 'script';
  case Ci.nsIContentPolicy.TYPE_IMAGE:
    return 'image';
  case Ci.nsIContentPolicy.TYPE_STYLESHEET:
    return 'stylesheet';
  case Ci.nsIContentPolicy.TYPE_OBJECT:
    return 'object';
  case Ci.nsIContentPolicy.TYPE_DOCUMENT:
    return 'document';
  case Ci.nsIContentPolicy.TYPE_SUBDOCUMENT:
    return 'subdocument';
  case Ci.nsIContentPolicy.TYPE_REFRESH:
    return 'refresh';
  case Ci.nsIContentPolicy.TYPE_XBL:
    return 'xbl';
  case Ci.nsIContentPolicy.TYPE_XMLHTTPREQUEST:
    return 'xhr';
  case Ci.nsIContentPolicy.TYPE_PING:
    return 'ping';
    // TODO: support more types
  }
  return 'other';
}
const getTypeMemod = memoize(getType, 12, 1);

function makeDetails(aContentType, aContentLocation, aRequestOrigin, aContext, aMimeTypeGuess) {
  return {
    type: getTypeMemod(aContentType),
    location: aContentLocation,
    origin: aRequestOrigin.spec,
    context: null, // TODO: support this in a safe way somehow..
    mimeTypeGuess: String(aMimeTypeGuess)
  };
}

let ContentPolicy = exports.ContentPolicy = Class({
  initialize: function (options) {
    const self = this;
    options = CP_NS(self).options = validateOptions(options, RULES);
    CP_NS(self).shouldLoad = options.shouldLoad || accept;
    CP_NS(self).shouldProcess = options.shouldProcess || accept;

    let factory = CP_NS(this).factory = xpcom.Factory({
      Component: getProvider(self),
      description: options.description,
      contract: options.contract
    });

    let entry = options.entry || options.contract;
    CM.addCategoryEntry('content-policy', entry, factory.contract, false, true);
    ensure(this, 'destroy');
  },
  destroy: function () {
    // already destroyed?
    if (!CP_NS(this).options)
      return;

    let options = CP_NS(this).options;
    CP_NS(this).options = null;
    CP_NS(this).shouldLoad = accept;
    CP_NS(this).shouldProcess = accept;

    CM.deleteCategoryEntry('content-policy', options.entry || options.contract, false);
  }
});

function getProvider(self) {
  return Class({
    extends: xpcom.Unknown,
    interfaces: ['nsIContentPolicy'],
    shouldLoad: function (aContentType, aContentLocation, aRequestOrigin, aContext, aMimeTypeGuess, aExtra) {
      let load = CP_NS(self).shouldLoad(makeDetails.apply(null, arguments));
      return (load == REJECT || (!load && load !== undefined)) ? REJECT : ACCEPT;
    },
    shouldProcess: function (aContentType, aContentLocation, aRequestOrigin, aContext, aMimeTypeGuess, aExtra) {
      let load = CP_NS(self).shouldProcess(makeDetails.apply(null, arguments));
      return (load == REJECT || (!load && load !== undefined)) ? REJECT : ACCEPT;
    }
  });
}

function memoize(func) {
  let cache = Object.create(null);
  return function (a) {
    let key = a.toString();
    if (!(key in cache)) {
      cache[key] = func.call(null, a);
    }
    return cache[key];
  };
}
