(function() {
  'use strict';

  angular.module('ml.common')
    .provider('MLRest', function() {
      this.$get = ['$http', MLRest];
    });

  /**
   * @class MLRest
   * @classdesc low-level angular service, encapsulates REST API builtins and normalizes the responses.
   *
   * @param {Object} $http - angular {@link https://docs.angularjs.org/api/ng/service/$http $http service}
   */
  function MLRest($http) {
    var defaults = { apiVersion: 'v1' };

    var service = {
      search: search,
      getDocument: getDocument,
      createDocument: createDocument,
      updateDocument: updateDocument,
      patchDocument: patchDocument,
      deleteDocument: deleteDocument,
      sparql: sparql,
      suggest: suggest,
      values: values,
      extension: extension,
      queryConfig: queryConfig,
      request: request,

      // DEPRECATED: TODO: remove
      getSearchOptions: queryConfig,
      callExtension: extension,
      patch: patchDocument
    };

    // private function for checking if a method is supported
    function isSupportedMethod(method) {
      var supported = [ 'GET', 'PUT', 'POST', 'DELETE' ];
      return supported.indexOf(method) > -1;
    }

    /**
     * Makes a REST API request (all other methods wrap this)
     * @method MLRest#request
     *
     * @param {String} endpoint - the request endpoint: can be version agnostic (`/search`) or specific (`/v1/search`)
     * @param {Object} settings - angular `$http` service {@link https://docs.angularjs.org/api/ng/service/$http#usage settings}
     * @return {Promise} a promise resolved with an angular `$http` service {@link https://docs.angularjs.org/api/ng/service/$http#general-usage response object}
     */
    function request(endpoint, settings) {
      var url;

      if (/^\/v1\//.test(endpoint)) {
        url = endpoint;
      } else {
        url = '/' + defaults.apiVersion + endpoint;
      }

      settings = settings || {};
      settings.method = settings.method || 'GET';

      if (!isSupportedMethod(settings.method)) {
        settings.headers = settings.headers || {};
        settings.headers['X-HTTP-Method-Override'] = settings.method;
        settings.method = 'POST';
      }

      return $http({
        url: url,
        data: settings.data,
        method: settings.method,
        params: settings.params,
        headers: settings.headers
      });
    }

    /**
     * Makes a resource extension request
     * - {@link http://docs.marklogic.com/REST/GET/v1/resources/[name]}
     * @method MLRest#extension
     *
     * @param {String} name - resource extension name
     * @param {Object} settings - angular `$http` service {@link https://docs.angularjs.org/api/ng/service/$http#usage settings}
     * @return {Promise} a promise resolved with an angular `$http` service {@link https://docs.angularjs.org/api/ng/service/$http#general-usage response object}
     */
    function extension(name, settings) {
      if ( !/^\//.test(name) ) {
        name = '/' + name;
      }
      return request('/resources' + name, settings);
    }

    /**
     * Makes a search request (POST if combined query, GET otherwise)
     * - {@link http://docs.marklogic.com/REST/GET/v1/search}
     * - {@link http://docs.marklogic.com/REST/POST/v1/search}
     * @method MLRest#search
     *
     * @param {Object} [options] - URL params
     * @param {Object} [combined] - a combined search object (identified by a `search` property)
     * @return {Promise} a promise resolved with an angular `$http` service {@link https://docs.angularjs.org/api/ng/service/$http#general-usage response object}
     */
    function search(options, combined) {
      var settings = {};

      if ( !combined && options && options.search ) {
        combined = options;
        options = {};
      } else {
        options = options || {};
      }

      if (!options.format) {
        options.format = 'json';
      }

      settings.params = options;

      if ( combined ) {
        settings.method = 'POST';
        settings.data = combined;
      }

      return request('/search', settings);
    }

    /**
     * Retrieves a document at the specified URI
     * - {@link http://docs.marklogic.com/REST/GET/v1/documents}
     * @method MLRest#getDocument
     *
     * @param {String} uri - document URI
     * @param {Object} options - URL params
     * @return {Promise} a promise resolved with an angular `$http` service {@link https://docs.angularjs.org/api/ng/service/$http#general-usage response object}
     */
    function getDocument(uri, options) {
      options = options || {};
      options.uri = uri;

      if (!options.format) {
        options.format = 'json';
      }

      return request('/documents', { params: options });
    }

    /**
     * Creates a document, returning the new URI
     * - {@link http://docs.marklogic.com/REST/POST/v1/documents}
     * @method MLRest#createDocument
     *
     * @param {Object|String} doc - document contents
     * @param {Object} [options] - URL params
     * @return {Promise} a promise resolved with the new document URI
     */
    function createDocument(doc, options) {
      return request('/documents', {
        method: 'POST',
        params: options,
        data: doc
      }).then(function(response) {
        return response.headers('location');
      });
    }

    /**
     * Creates or updates a document at the specified URI (`options.uri`)
     * - {@link http://docs.marklogic.com/REST/PUT/v1/documents}
     * @method MLRest#updateDocument
     *
     * @param {Object|String} doc - document contents
     * @param {Object} options - URL params
     * @return {Promise} a promise resolved with the new document URI
     */
    // TODO: uri param?
    // TODO: shouldn't resolve location?
    function updateDocument(doc, options) {
      return request('/documents', {
        method: 'PUT',
        params: options,
        data: doc
      }).then(function(response) {
        return response.headers('location');
      });
    }

    /**
     * Applies the provided patch to the document at the specified URI
     * - {@link http://docs.marklogic.com/REST/PATCH/v1/documents}
     * @method MLRest#patchDocument
     *
     * @param {String} uri - document URI
     * @param {Object} patch - a document patch definition
     * @return {Promise} a promise resolved with the new document URI
     */
    // TODO: shouldn't resolve location?
    function patchDocument(uri, patch) {
      // TODO: support XML patches

      // var headers = {};
      //
      // if (isObject(patch)) {
      //   headers = { 'Content-Type': 'application/json' }
      // } else {
      //   headers = { 'Content-Type': 'application/xml' }
      // }

      return request('/documents', {
        method: 'PATCH',
        params: { uri: uri },
        // headers: headers,
        data: patch
      })
      .then(function(response) {
        return response.headers('location');
      });
    }

    /**
     * Deletes a document at the specified URI
     * - {@link http://docs.marklogic.com/REST/DELETE/v1/documents}
     * @method MLRest#deleteDocument
     *
     * @param {String} uri - document uri
     * @param {Object} options - URL params
     * @return {Promise} a promise resolved with an angular `$http` service {@link https://docs.angularjs.org/api/ng/service/$http#general-usage response object}
     */
    function deleteDocument(uri, options) {
      options = options || {};
      options.uri = uri;

      return request('/documents', {
        method: 'DELETE',
        params: options
      });
    }

    /**
     * Evaluates a SPARQL query
     * - {@link http://docs.marklogic.com/REST/GET/v1/graphs/sparql}
     * @method MLRest#sparql
     *
     * @param {String} query - a SPARQL query
     * @param {Object} [params] - URL params
     * @return {Promise} a promise resolved with an angular `$http` service {@link https://docs.angularjs.org/api/ng/service/$http#general-usage response object}
     */
    function sparql(query, params) {
      var accept = [
        'application/sparql-results+json'
        // TODO: file bug against REST API for not supporting multiple Accept mime-types
        // 'application/rdf+json'
      ];

      params = params || {};

      // TODO: POST query?
      params.query = query;

      return request('/graphs/sparql', {
        params: params,
        headers: { 'Accept': accept.join(', ') }
      });
    }

    /**
     * Retrieves search phrase suggestions
     * - {@link http://docs.marklogic.com/REST/GET/v1/suggest}
     * - {@link http://docs.marklogic.com/REST/POST/v1/suggest}
     * @method MLRest#suggest
     *
     * @param {Object} [params] - URL params
     * @param {Object} [combined] - combined query
     * @return {Promise} a promise resolved with an angular `$http` service {@link https://docs.angularjs.org/api/ng/service/$http#general-usage response object}
     */
    function suggest(params, combined) {
      var settings = { params: params };

      if (combined) {
        settings.method = 'POST';
        settings.data = combined;
      }

      return request('/suggest', settings);
    }

    /**
     * Retrieves lexicon values
     * - {@link http://docs.marklogic.com/REST/GET/v1/values/[name]}
     * - {@link http://docs.marklogic.com/REST/POST/v1/values/[name]}
     * @method MLRest#values
     *
     * @param {String} name - values definition name (from stored or combined search options)
     * @param {Object} [params] - URL params
     * @param {Object} [combined] - combined query
     * @return {Promise} a promise resolved with an angular `$http` service {@link https://docs.angularjs.org/api/ng/service/$http#general-usage response object}
     */
    function values(name, params, combined) {
      var settings = { params: params };

      if (combined) {
        settings.method = 'POST';
        settings.data = combined;
      }

      return request('/values/' + name, settings);
    }

    /**
     * Retrieves stored search options
     * - {@link http://docs.marklogic.com/REST/GET/v1/config/query/['default'-or-name]}
     * - {@link http://docs.marklogic.com/REST/GET/v1/config/query/['default'-or-name]/[child-element]}
     * @method MLRest#queryConfig
     *
     * @param {String} name - stored search options name
     * @param {String} [section] - options section to retrieve
     * @return {Promise} a promise resolved with an angular `$http` service {@link https://docs.angularjs.org/api/ng/service/$http#general-usage response object}
     */
    function queryConfig(name, section) {
      var url = '/config/query/' + name;

      if (section) {
        url += '/' + section;
      }
      return request(url, {
        params: { format: 'json' }
      });
    }

    return service;
  }
}());