text: 'use strict';
  
  var Requests = require('./requests'),
      Cookies = require('./cookies'),
      documents = require('./documents'),
      ApiCache = require('./cache'),
      Predicates = require('./predicates'),
      experiments = require('./experiments');
  
  var Experiments = experiments.Experiments,
      Document = documents.Document;
  
  var experimentCookie = "io.prismic.experiment",
      previewCookie = "io.prismic.preview";
  
  /**
   * Initialisation of the API object.
   * This is for internal use, from outside this kit, you should call Prismic.Api()
   * @private
   */
  function Api(url, options) {
    var opts = options || {};
    this.accessToken = opts.accessToken;
    this.url = url + (this.accessToken ? (url.indexOf('?') > -1 ? '&' : '?') + 'access_token=' + this.accessToken : '');
    this.req = opts.req;
    this.apiCache = opts.apiCache || globalCache();
    this.requestHandler = opts.requestHandler || Requests.request;
    this.apiCacheKey = this.url + (this.accessToken ? ('#' + this.accessToken) : '');
    this.apiDataTTL = opts.apiDataTTL || 5;
    return this;
  }
  
  Api.prototype = {
  
    // Predicates
    AT: "at",
    ANY: "any",
    SIMILAR: "similar",
    FULLTEXT: "fulltext",
    NUMBER: {
      GT: "number.gt",
      LT: "number.lt"
    },
    DATE: {
      // Other date operators are available: see the documentation.
      AFTER: "date.after",
      BEFORE: "date.before",
      BETWEEN: "date.between"
    },
  
    // Fragment: usable as the second element of a query array on most predicates (except SIMILAR).
    // You can also use "my.*" for your custom fields.
    DOCUMENT: {
      ID: "document.id",
      TYPE: "document.type",
      TAGS: "document.tags"
    },
  
    data: null,
  
    /**
     * Fetches data used to construct the api client, from cache if it's
     * present, otherwise from calling the prismic api endpoint (which is
     * then cached).
     *
     * @param {function} callback - Callback to receive the data. Optional, you can use the promise result.
     * @returns {Promise} Promise holding the data or error
     */
    get: function(callback) {
      var self = this;
      var cacheKey = this.apiCacheKey;
  
      return new Promise(function (resolve, reject) {
        var cb = function(err, value, xhr, ttl) {
          if (callback) callback(err, value, xhr, ttl);
          if (value) resolve(value);
          if (err) reject(err);
        };
        self.apiCache.get(cacheKey, function (err, value) {
          if (err || value) {
            cb(err, value);
            return;
          }
  
          self.requestHandler(self.url, function(err, data, xhr, ttl) {
            if (err) {
              cb(err, null, xhr, ttl);
              return;
            }
  
            var parsed = self.parse(data);
            ttl = ttl || self.apiDataTTL;
  
            self.apiCache.set(cacheKey, parsed, ttl, function (err) {
              cb(err, parsed, xhr, ttl);
            });
          });
        });
      });
    },
  
    /**
     * Cleans api data from the cache and fetches an up to date copy.
     *
     * @param {function} callback - Optional callback function that is called after the data has been refreshed
     * @returns {Promise}
     */
    refresh: function (callback) {
      var self = this;
      var cacheKey = this.apiCacheKey;
  
      return new Promise(function(resolve, reject) {
        var cb = function(err, value, xhr) {
          if (callback) callback(err, value, xhr);
          if (value) resolve(value);
          if (err) reject(err);
        };
        self.apiCache.remove(cacheKey, function (err) {
          if (err) { cb(err); return; }
  
          self.get(function (err, data) {
            if (err) { cb(err); return; }
  
            self.data = data;
            self.bookmarks = data.bookmarks;
            self.experiments = new Experiments(data.experiments);
  
            cb();
          });
        });
      });
    },
  
    /**
     * Parses and returns the /api document.
     * This is for internal use, from outside this kit, you should call Prismic.Api()
     *
     * @param {string} data - The JSON document responded on the API's endpoint
     * @returns {Api} - The Api object that can be manipulated
     * @private
     */
    parse: function(data) {
      var refs,
          master,
          forms = {},
          form,
          types,
          tags,
          f,
          i;
  
      // Parse the forms
      for (i in data.forms) {
        if (data.forms.hasOwnProperty(i)) {
          f = data.forms[i];
  
          if(this.accessToken) {
            f.fields['access_token'] = {};
            f.fields['access_token']['type'] = 'string';
            f.fields['access_token']['default'] = this.accessToken;
          }
  
          form = new Form(
            f.name,
            f.fields,
            f.form_method,
            f.rel,
            f.enctype,
            f.action
          );
  
          forms[i] = form;
        }
      }
  
      refs = data.refs.map(function (r) {
        return new Ref(
          r.ref,
          r.label,
          r.isMasterRef,
          r.scheduledAt,
          r.id
        );
      }) || [];
  
      master = refs.filter(function (r) {
        return r.isMaster === true;
      });
  
      types = data.types;
  
      tags = data.tags;
  
      if (master.length === 0) {
        throw ("No master ref.");
      }
  
      return {
        bookmarks: data.bookmarks || {},
        refs: refs,
        forms: forms,
        master: master[0],
        types: types,
        tags: tags,
        experiments: data.experiments,
        oauthInitiate: data['oauth_initiate'],
        oauthToken: data['oauth_token'],
        quickRoutes: data.quickRoutes
      };
  
    },
  
    /**
     * @deprecated use form() now
     * @param {string} formId - The id of a form, like "everything", or "products"
     * @returns {SearchForm} - the SearchForm that can be used.
     */
    forms: function(formId) {
      return this.form(formId);
    },
  
    /**
     * Returns a useable form from its id, as described in the RESTful description of the API.
     * For instance: api.form("everything") works on every repository (as "everything" exists by default)
     * You can then chain the calls: api.form("everything").query('[[:d = at(document.id, "UkL0gMuvzYUANCpf")]]').ref(ref).submit()
     *
     * @param {string} formId - The id of a form, like "everything", or "products"
     * @returns {SearchForm} - the SearchForm that can be used.
     */
    form: function(formId) {
      var form = this.data.forms[formId];
      if(form) {
        return new SearchForm(this, form, {});
      }
      return null;
    },
  
    /**
     * The ID of the master ref on this prismic.io API.
     * Do not use like this: searchForm.ref(api.master()).
     * Instead, set your ref once in a variable, and call it when you need it; this will allow to change the ref you're viewing easily for your entire page.
     *
     * @returns {string}
     */
    master: function() {
      return this.data.master.ref;
    },
  
    /**
     * Returns the ref ID for a given ref's label.
     * Do not use like this: searchForm.ref(api.ref("Future release label")).
     * Instead, set your ref once in a variable, and call it when you need it; this will allow to change the ref you're viewing easily for your entire page.
     *
     * @param {string} label - the ref's label
     * @returns {string}
     */
    ref: function(label) {
      for(var i=0; i<this.data.refs.length; i++) {
        if(this.data.refs[i].label == label) {
          return this.data.refs[i].ref;
        }
      }
      return null;
    },
  
    /**
     * The current experiment, or null
     * @returns {Experiment}
     */
    currentExperiment: function() {
      return this.experiments.current();
    },
  
    quickRoutesEnabled: function() {
      return this.data.quickRoutes.enabled;
    },
  
    /**
     * Retrieve quick routes definitions
     */
    quickRoutes: function(callback) {
      var self = this;
      return new Promise(function(resolve, reject) {
        self.requestHandler(self.data.quickRoutes.url, function(err, data, xhr) {
          if (callback) callback(err, data, xhr);
          if (err) reject(err);
          if (data) resolve(data);
        });
      });
    },
  
    /**
     * Query the repository
     * @param {string|array|Predicate} the query itself
     * @param {object} additional parameters. In NodeJS, pass the request as 'req'.
     * @param {function} callback(err, response)
     */
    query: function(q, options, callback) {
      if (typeof options === 'function') {
        callback = options;
        options = undefined;
      }
      var opts = options || {};
      var form = this.form('everything');
      for (var key in opts) {
        form = form.set(key, options[key]);
      }
      // Don't override the ref if the caller specified one in the options
      if (!opts['ref']) {
        // Look in cookies if we have a ref (preview or experiment)
        var cookieString = '';
        if (this.req) { // NodeJS
          cookieString = this.req.headers["cookie"] || '';
        } else if (typeof window !== 'undefined') { // Browser
          cookieString = window.document.cookie || '';
        }
        var cookies = Cookies.parse(cookieString);
        var previewRef = cookies[previewCookie];
        var experimentRef = this.experiments.refFromCookie(cookies[experimentCookie]);
        form = form.ref(previewRef || experimentRef || this.master());
      }
      if (q) {
        form.query(q);
      }
      return form.submit(callback);
    },
  
    /**
     * Retrieve the document returned by the given query
     * @param {string|array|Predicate} the query
     * @param {object} additional parameters. In NodeJS, pass the request as 'req'.
     * @param {function} callback(err, doc)
     */
    queryFirst: function(q, options, callback) {
      if (typeof options === 'function') {
        callback = options;
        options = undefined;
      }
      var opts = {};
      for (var key in (options || {})) {
        opts[key] = options[key];
      }
      opts.page = 1;
      opts.pageSize = 1;
      return this.query(q, opts, function(err, response) {
        if (callback) {
          var result = response && response.results && response.results[0];
          callback(err, result);
        }
      }).then(function(response){
        return response && response.results && response.results[0];
      });
    },
  
    /**
     * Retrieve the document with the given id
     * @param {string} id
     * @param {object} additional parameters
     * @param {function} callback(err, doc)
     */
    getByID: function(id, options, callback) {
      options = options || {};
      if(!options.lang) options.lang = '*';
      return this.queryFirst(Predicates.at('document.id', id), options, callback);
    },
  
    /**
     * Retrieve multiple documents from an array of id
     * @param {array} ids
     * @param {object} additional parameters
     * @param {function} callback(err, response)
     */
    getByIDs: function(ids, options, callback) {
      options = options || {};
      if(!options.lang) options.lang = '*';
      return this.query(['in', 'document.id', ids], options, callback);
    },
  
    /**
     * Retrieve the document with the given uid
     * @param {string} type the custom type of the document
     * @param {string} uid
     * @param {object} additional parameters
     * @param {function} callback(err, response)
     */
    getByUID: function(type, uid, options, callback) {
      options = options || {};
      if(!options.lang) options.lang = '*';
      return this.queryFirst(Predicates.at('my.'+type+'.uid', uid), options, callback);
    },
  
    /**
     * Retrieve the singleton document with the given type
     * @param {string} type the custom type of the document
     * @param {object} additional parameters
     * @param {function} callback(err, response)
     */
    getSingle: function(type, options, callback) {
      return this.queryFirst(Predicates.at('document.type', type), options, callback);
    },
  
    /**
     * Retrieve the document with the given bookmark
     * @param {string} bookmark name
     * @param {object} additional parameters
     * @param {function} callback(err, response)
     * @returns {Promise}
     */
    getBookmark: function(bookmark, options, callback) {
      return new Promise(function(resolve, reject) {
        var id = this.bookmarks[bookmark];
        if (id) {
          resolve(id);
        } else {
          var err = new Error("Error retrieving bookmarked id");
          if (callback) callback(err);
          reject(err);
        }
      }).then(function(id) {
        return this.getByID(id, options, callback);
      });
    },
  
    /**
     * Return the URL to display a given preview
     * @param {string} token as received from Prismic server to identify the content to preview
     * @param {function} linkResolver the link resolver to build URL for your site
     * @param {string} defaultUrl the URL to default to return if the preview doesn't correspond to a document
     *                (usually the home page of your site)
     * @param {function} callback to get the resulting URL (optional, you can get it from the Promise result)
     * @returns {Promise}
     */
    previewSession: function(token, linkResolver, defaultUrl, callback) {
      var api = this;
      return new Promise(function(resolve, reject) {
        var cb = function(err, value, xhr) {
          if (callback) callback(err, value, xhr);
          if (err) {
            reject(err);
          } else {
            resolve(value);
          }
        };
        api.requestHandler(token, function (err, result, xhr) {
          if (err) {
            cb(err, defaultUrl, xhr);
            return;
          }
          try {
            var mainDocumentId = result.mainDocument;
            if (!mainDocumentId) {
              cb(null, defaultUrl, xhr);
            } else {
              api.form("everything").query(Predicates.at("document.id", mainDocumentId)).ref(token).lang('*').submit(function(err, response) {
                if (err) {
                  cb(err);
                }
                try {
                  if (response.results.length === 0) {
                    cb(null, defaultUrl, xhr);
                  } else {
                    cb(null, linkResolver(response.results[0]), xhr);
                  }
                } catch (e) {
                  cb(e);
                }
              });
            }
          } catch (e) {
            cb(e, defaultUrl, xhr);
          }
        });
      });
    },
  
    /**
     * Fetch a URL corresponding to a query, and parse the response as a Response object
     */
    request: function(url, callback) {
      var api = this;
      var cacheKey = url + (this.accessToken ? ('#' + this.accessToken) : '');
      var cache = this.apiCache;
      function run(cb) {
        cache.get(cacheKey, function (err, value) {
          if (err || value) {
            cb(err, api.response(value));
            return;
          }
          api.requestHandler(url, function (err, documents, xhr, ttl) {
            if (err) {
              cb(err, null, xhr);
              return;
            }
  
            if (ttl) {
              cache.set(cacheKey, documents, ttl, function (err) {
                cb(err, api.response(documents));
              });
            } else {
              cb(null, api.response(documents));
            }
          });
        });
      }
      return new Promise(function(resolve, reject) {
        run(function(err, value, xhr) {
          if (callback) callback(err, value, xhr);
          if (err) reject(err);
          if (value) resolve(value);
        });
      });
    },
  
    getNextPage: function(nextPage, callback) {
      return this.request(nextPage + (this.accessToken ? '&access_token=' + this.accessToken : ''), callback);
    },
  
    /**
     * JSON documents to Response object
     */
    response: function(documents){
      var results = documents.results.map(parseDoc);
      return new Response(
        documents.page,
        documents.results_per_page,
        documents.results_size,
        documents.total_results_size,
        documents.total_pages,
        documents.next_page,
        documents.prev_page,
        results || []);
    }
  
  };
  
  /**
   * Embodies a submittable RESTful form as described on the API endpoint (as per RESTful standards)
   * @constructor
   * @private
   */
  function Form(name, fields, form_method, rel, enctype, action) {
    this.name = name;
    this.fields = fields;
    this.form_method = form_method;
    this.rel = rel;
    this.enctype = enctype;
    this.action = action;
  }
  
  Form.prototype = {};
  
  /**
   * Parse json as a document
   *
   * @returns {Document}
   */
  var parseDoc = function(json) {
    var fragments = {};
    for(var field in json.data[json.type]) {
      fragments[json.type + '.' + field] = json.data[json.type][field];
    }
  
    var slugs = [];
    if (json.slugs !== undefined) {
      for (var i = 0; i < json.slugs.length; i++) {
        slugs.push(decodeURIComponent(json.slugs[i]));
      }
    }
  
    return new Document(
      json.id,
      json.uid || null,
      json.type,
      json.href,
      json.tags,
      slugs,
      json.first_publication_date,
      json.last_publication_date,
      json.lang,
      json.alternate_languages,
      fragments,
      json.data
    );
  };
  
  /**
   * Embodies a SearchForm object. To create SearchForm objects that are allowed in the API, please use the API.form() method.
   * @constructor
   * @global
   * @alias SearchForm
   */
  function SearchForm(api, form, data) {
    this.api = api;
    this.form = form;
    this.data = data || {};
  
    for(var field in form.fields) {
      if(form.fields[field]['default']) {
        this.data[field] = [form.fields[field]['default']];
      }
    }
  }
  
  SearchForm.prototype = {
  
    /**
     * Set an API call parameter. This will only work if field is a valid field of the
     * RESTful form in the first place (as described in the /api document); otherwise,
     * an "Unknown field" error is thrown.
     * Please prefer using dedicated methods like query(), orderings(), ...
     *
     * @param {string} field - The name of the field to set
     * @param {string} value - The value that gets assigned
     * @returns {SearchForm} - The SearchForm itself
     */
    set: function(field, value) {
      var fieldDesc = this.form.fields[field];
      if(!fieldDesc) throw new Error("Unknown field " + field);
      var values= this.data[field] || [];
      if(value === '' || value === undefined) {
        // we must compare value to null because we want to allow 0
        value = null;
      }
      if(fieldDesc.multiple) {
        if (value) values.push(value);
      } else {
        values = value && [value];
      }
      this.data[field] = values;
      return this;
    },
  
    /**
     * Sets a ref to query on for this SearchForm. This is a mandatory
     * method to call before calling submit(), and api.form('everything').submit()
     * will not work.
     *
     * @param {Ref} ref - The Ref object defining the ref to query
     * @returns {SearchForm} - The SearchForm itself
     */
    ref: function(ref) {
      return this.set("ref", ref);
    },
  
    /**
     * Sets a predicate-based query for this SearchForm. This is where you
     * paste what you compose in your prismic.io API browser.
     *
     * @example form.query(Prismic.Predicates.at("document.id", "foobar"))
     * @param {string|...array} query - Either a query as a string, or as many predicates as you want. See Prismic.Predicates.
     * @returns {SearchForm} - The SearchForm itself
     */
    query: function(query) {
      if (typeof query === 'string') {
        return this.set("q", query);
      } else {
        var predicates;
        if (query.constructor === Array && query.length > 0 && query[0].constructor === Array) {
          predicates = query;
        } else {
          predicates = [].slice.apply(arguments); // Convert to a real JS array
        }
        var stringQueries = [];
        predicates.forEach(function (predicate) {
          stringQueries.push(Predicates.toQuery(predicate));
        });
        return this.query("[" + stringQueries.join("") + "]");
      }
    },
  
    /**
     * Sets a page size to query for this SearchForm. This is an optional method.
     *
     * @param {number} size - The page size
     * @returns {SearchForm} - The SearchForm itself
     */
    pageSize: function(size) {
      return this.set("pageSize", size);
    },
  
    /**
     * Restrict the results document to the specified fields
     *
     * @param {string|array} fields - The list of fields, array or comma separated string
     * @returns {SearchForm} - The SearchForm itself
     */
    fetch: function(fields) {
      if (fields instanceof Array) {
        fields = fields.join(",");
      }
      return this.set("fetch", fields);
    },
  
    /**
     * Include the requested fields in the DocumentLink instances in the result
     *
     * @param {string|array} fields - The list of fields, array or comma separated string
     * @returns {SearchForm} - The SearchForm itself
     */
    fetchLinks: function(fields) {
      if (fields instanceof Array) {
        fields = fields.join(",");
      }
      return this.set("fetchLinks", fields);
    },
  
    /**
     * Sets the language to query for this SearchForm. This is an optional method.
     *
     * @param {string} fields - The language code
     * @returns {SearchForm} - The SearchForm itself
     */
    lang: function(fields) {
      return this.set("lang", fields);
    },
  
    /**
     * Sets the page number to query for this SearchForm. This is an optional method.
     *
     * @param {number} p - The page number
     * @returns {SearchForm} - The SearchForm itself
     */
    page: function(p) {
      return this.set("page", p);
    },
  
    /**
     * Sets the orderings to query for this SearchForm. This is an optional method.
     *
     * @param {array} orderings - Array of string: list of fields, optionally followed by space and desc. Example: ['my.product.price desc', 'my.product.date']
     * @returns {SearchForm} - The SearchForm itself
     */
    orderings: function(orderings) {
      if (typeof orderings === 'string') {
        // Backward compatibility
        return this.set("orderings", orderings);
      } else if (!orderings) {
        // Noop
        return this;
      } else {
        // Normal usage
        return this.set("orderings", "[" + orderings.join(",") + "]");
      }
    },
  
    /**
     * Submits the query, and calls the callback function.
     *
     * @param {function} callback - Optional callback function that is called after the query was made,
     * to which you may pass three parameters: a potential error (null if no problem),
     * a Response object (containing all the pagination specifics + the array of Docs),
     * and the XMLHttpRequest
     */
    submit: function(callback) {
      var self = this;
      var url = this.form.action;
  
      if (this.data) {
        var sep = (url.indexOf('?') > -1 ? '&' : '?');
        for(var key in this.data) {
          if (this.data.hasOwnProperty(key)) {
            var values = this.data[key];
            if (values) {
              for (var i = 0; i < values.length; i++) {
                url += sep + key + '=' + encodeURIComponent(values[i]);
                sep = '&';
              }
            }
          }
        }
      }
  
      return self.api.request(url, callback);
    }
  };
  
  /**
   * Embodies the response of a SearchForm query as returned by the API.
   * It includes all the fields that are useful for pagination (page, total_pages, total_results_size, ...),
   * as well as the field "results", which is an array of {@link Document} objects, the documents themselves.
   *
   * @constructor
   * @global
   */
  function Response(page, results_per_page, results_size, total_results_size, total_pages, next_page, prev_page, results) {
    /**
     * The current page
     * @type {number}
     */
    this.page = page;
    /**
     * The number of results per page
     * @type {number}
     */
    this.results_per_page = results_per_page;
    /**
     * The size of the current page
     * @type {number}
     */
    this.results_size = results_size;
    /**
     * The total size of results across all pages
     * @type {number}
     */
    this.total_results_size = total_results_size;
    /**
     * The total number of pages
     * @type {number}
     */
    this.total_pages = total_pages;
    /**
     * The URL of the next page in the API
     * @type {string}
     */
    this.next_page = next_page;
    /**
     * The URL of the previous page in the API
     * @type {string}
     */
    this.prev_page = prev_page;
    /**
     * Array of {@link Document} for the current page
     * @type {Array}
     */
    this.results = results;
  }
  
  /**
   * Embodies a prismic.io ref (a past or future point in time you can query)
   * @constructor
   * @global
   */
  function Ref(ref, label, isMaster, scheduledAt, id) {
    /**
     * @field
     * @description the ID of the ref
     */
    this.ref = ref;
    /**
     * @field
     * @description the label of the ref
     */
    this.label = label;
    /**
     * @field
     * @description is true if the ref is the master ref
     */
    this.isMaster = isMaster;
    /**
     * @field
     * @description the scheduled date of the ref
     */
    this.scheduledAt = scheduledAt;
    /**
     * @field
     * @description the name of the ref
     */
    this.id = id;
  }
  Ref.prototype = {};
  function globalCache() {
    var g;
    if (typeof global == 'object') {
      g = global; // NodeJS
    } else {
      g = window; // browser
    }
    if (!g.prismicCache) {
      g.prismicCache = new ApiCache();
    }
    return g.prismicCache;
  }
  
  module.exports = {
    experimentCookie: experimentCookie,
    previewCookie: previewCookie,
    Api: Api,
    Form: Form,
    SearchForm: SearchForm,
    Ref: Ref,
    parseDoc: parseDoc
  };
  