/*! drupalgap 2016-08-15 */
// Initialize the drupalgap json object.
var drupalgap = drupalgap || drupalgap_init(); // Do not remove this line.

// Init _GET for url path query strings.
var _dg_GET = _dg_GET || {};

/**
 * Initializes the drupalgap json object.
 * @return {Object}
 */
function drupalgap_init() {
    var dg = {
      modules: {
        core: [
           { name: 'comment' },
           { name: 'contact' },
           { name: 'entity' },
           { name: 'field' },
           { name: 'file' },
           { name: 'image' },
           { name: 'menu' },
           { name: 'mvc' },
           { name: 'node' },
           { name: 'search' },
           { name: 'system' },
           { name: 'taxonomy' },
           { name: 'user' },
           { name: 'views' }
         ]
      },
      module_paths: [],
      includes: [
          { name: 'block' },
          { name: 'common' },
          { name: 'form' },
          { name: 'go' },
          { name: 'menu' },
          { name: 'page' },
          { name: 'region' },
          { name: 'theme' },
          { name: 'title' }
      ],
      online: false,
      destination: '',
      api: {},
      back: false, /* moving backwards or not */
      back_path: [], /* paths to move back to */
      blocks: [],
      connected: false, // Becomes true once DrupalGap performs a System Connect call.
      content_types_list: {}, /* holds info about each content type */
      date_formats: { }, /* @see system_get_date_formats() in Drupal core */
      date_types: { }, /* @see system_get_date_types() in Drupal core */
      entity_info: {},
      field_info_fields: {},
      field_info_instances: {},
      field_info_extra_fields: {},
      form_errors: {},
      form_states: [],
      loading: false, /* indicates if the loading message is shown or not */
      loader: 'loading', /* used to determine the jQM loader mode */
      locale: {}, /* holds onto language json objects, keyed by language code */
      messages: [],
      menus: {},
      menu_links: {},
      menu_router: {}, /* @todo - doesn't appear to be used at all, remove it */
      mvc: {
        models: {},
        views: {},
        controllers: {}
      },
      output: '', /* hold output generated by menu_execute_active_handler() */
      page: {
        jqm_events: [],
        title: '',
        variables: {},
        process: true,
        options: {} /* holds the current page's options, eg. reloadPage, etc. */
      },
      pages: [], /* Collection of page ids that are loaded into the DOM. */
      path: '', /* The current menu path. */
      remote_addr: null, /* php's $_SERVER['REMOTE_ADDR'] via system connect */
      router_path: '', /* The current menu router path. */
      services: {},
      sessid: null,
      settings: {},
      site_settings: {}, /* holds variable settings from the Drupal site */
      taxonomy_vocabularies: false, /* holds vocabs from system connect */
      theme_path: '',
      themes: [],
      theme_registry: {},
      toast: {
        shown: false
      },
      views: {
        ids: []
      },
      views_datasource: {}
    };

  // Extend jDrupal as needed...

  // Forms will expire upon install and don't have an expiration time.
  if (!Drupal.cache_expiration) { Drupal.cache_expiration = {}; }
  if (!Drupal.cache_expiration.forms) { Drupal.cache_expiration.forms = {}; }

  // Finally return the JSON object.
  return dg;
}

/**
 * This is called once the <body> element's onload is fired.
 */
function drupalgap_onload() {
  try {

    // Remove any hash in case the app is restarting.
    window.location.hash = '';

    // At this point, the Drupal object has been initialized by jDrupal and the
    // app/settings.js file was loaded in <head>. Let's add DrupalGap's modules
    // onto the Drupal JSON object. Remember, all of the module source code is
    // included via the makefile's bin generation. However, the core modules
    // hook_install() implementations haven't been called yet, so we add them to
    // the module listing so they can be invoked later on.
    var modules = [
      'drupalgap',
      'block',
      'comment',
      'contact',
      'entity',
      'field',
      'file',
      'image',
      'menu',
      'mvc',
      'node',
      'search',
      'system',
      'taxonomy',
      'user',
      'views'
    ];
    for (var i = 0; i < modules.length; i++) {
      var module = modules[i];
      Drupal.modules.core[module] = module_object_template(module);
    }

    // Depending on the mode, we'll move on to _drupalgap_deviceready()
    // accordingly. By default we'll assume the mode is for phonegap, unless
    // otherwise specified by the settings.js file. If it is for phonegap, we'll
    // attach its device ready listener, otherwise we'll just move on.
    if (typeof drupalgap.settings.mode === 'undefined') {
      drupalgap.settings.mode = 'phonegap';
    }
    switch (drupalgap.settings.mode) {
      case 'phonegap':
        document.addEventListener('deviceready', _drupalgap_deviceready, false);
        break;
      case 'web-app':
        _drupalgap_deviceready();
        break;
      default:
        console.log(
          'drupalgap_onload - unknown mode (' + drupalgap.settings.mode + ')'
        );
        return;
        break;
    }

  }
  catch (error) { console.log('drupalgap_onload - ' + error); }
}

/**
 * Implements PhoneGap's deviceready().
 */
function _drupalgap_deviceready() {
  try {

    // Set some jQM properties to better handle the back button on iOS9.
    if (
      typeof device !== 'undefined' &&
      device.platform === "iOS" &&
      parseInt(device.version) === 9
    ) {
      $.mobile.hashListeningEnabled = false;
      $.mobile.pushStateEnabled = false;
    }

    // The device is now ready, it is now safe for DrupalGap to start...
    drupalgap_bootstrap();

    // Verify site path is set.
    if (!Drupal.settings.site_path || Drupal.settings.site_path == '') {
      var msg = t('No site_path to Drupal set in the app/settings.js file!');
      drupalgap_alert(msg, {
          title: t('Error')
      });
      return;
    }

    // Device is ready, let's call any implementations of hook_deviceready(). If any implementation returns
    // false, that means they would like to take over the rest of the deviceready procedure (aka the System
    // Connect call)
    var proceed = true;
    var invocation_results = module_invoke_all('deviceready');
    if (invocation_results && invocation_results.length > 0) {
      for (var i = 0; i < invocation_results.length; i++) {
        if (!invocation_results[i]) {
          proceed = false;
          break;
        }
      }
    }

    // If the device is offline, warn the user and then go to the offline page, unless someone implemented
    // hook_offline, then let them handle it.
    if (!drupalgap_has_connection()) {
      if (!module_implements('device_offline')) {
        if (drupalgap.settings.offline_message) {
          drupalgap_alert(drupalgap.settings.offline_message, {
            title: t('Offline'),
            alertCallback: function() { drupalgap_goto('offline'); }
          });
        }
        else { drupalgap_goto('offline'); }
      }
      else { setTimeout(function() { module_invoke_all('device_offline'); }, 1); }
    }
    else if (proceed) {

      // Device is online and no one has taken over the deviceready, continue with the System Connect call.
      system_connect(_drupalgap_deviceready_options());

    }

  }
  catch (error) { console.log('_drupalgap_deviceready - ' + error); }
}

/**
 * Builds the default system connect options.
 * @return {Object}
 */
function _drupalgap_deviceready_options() {
  try {
    var page_options = arguments[0] ? arguments[0] : {};
    return {
      success: function(result) {
        drupalgap.connected = true;
        // Call all hook_device_connected implementations then go to
        // the front page.
        module_invoke_all('device_connected');
        drupalgap_goto('', page_options);
      },
      error: function(jqXHR, textStatus, errorThrown) {
        // Build an informative error message and display it.
        var msg = t('Failed connection to') + ' ' +
          Drupal.settings.site_path;
        if (errorThrown != '') { msg += ' - ' + errorThrown; }
        msg += ' - ' + t('Check your device\'s connection and check that') +
          ' ' + Drupal.settings.site_path + ' ' + t('is online.');
       drupalgap_alert(msg, {
           title: t('Unable to Connect'),
           alertCallback: function() { drupalgap_goto('offline'); }
       });
      }
    };
  }
  catch (error) { console.log('_drupalgap_deviceready_options - ' + error); }
}

/**
 * Loads up all necessary assets to make DrupalGap ready.
 */
function drupalgap_bootstrap() {
  try {
    // Load up any contrib and/or custom modules (the DG core moodules have
    // already been loaded at this point), load the theme and all blocks. Then
    // build the menu router, load the menus, and build the theme registry.
    drupalgap_load_modules();
    drupalgap_load_theme();
    drupalgap_load_blocks();
    drupalgap_load_locales();
    menu_router_build();
    drupalgap_menus_load();
    drupalgap_theme_registry_build();

    // Attach device back button handler (Android).
    document.addEventListener('backbutton', drupalgap_back, false);
  }
  catch (error) { console.log('drupalgap_bootstrap - ' + error); }
}

/**
 * Loads any contrib or custom modules specifed in the settings.js file. Then
 * invoke hook_install() on all modules, including core.
 */
function drupalgap_load_modules() {
  try {
    var module_types = ['contrib', 'custom'];
    // We only need to load contrib and custom modules because core modules are
    // already included in the binary.
    for (var index in module_types) {
        if (!module_types.hasOwnProperty(index)) { continue; }
        var bundle = module_types[index];
        // Let's be nice and try to load any old drupalgap.modules declarations
        // in developers settings.js files for a while, but throw a warning to
        // encourage them to update. This code can be removed after a few
        // releases to help developers get caught up without angering them.
        if (
          drupalgap.modules &&
          drupalgap.modules[bundle] &&
          drupalgap.modules[bundle].length != 0
        ) {
          for (var index in drupalgap.modules[bundle]) {
              if (!drupalgap.modules[bundle].hasOwnProperty(index)) { continue; }
              var module = drupalgap.modules[bundle][index];
              if (module.name) {
                var msg = 'WARNING: The module "' + module.name + '" defined ' +
                  'in settings.js needs to be added to ' +
                  'Drupal.modules[\'' + bundle + '\'] instead! See ' +
                  'default.settings.js for examples on the new syntax!';
                console.log(msg);
                Drupal.modules[bundle][module.name] = module;
              }
          }
        }
        for (var module_name in Drupal.modules[bundle]) {
            if (!Drupal.modules[bundle].hasOwnProperty(module_name)) { continue; }
            var module = Drupal.modules[bundle][module_name];
            // If the module object is empty, initialize a module object.
            if ($.isEmptyObject(module)) {
              Drupal.modules[bundle][module_name] =
                module_object_template(module_name);
              module = Drupal.modules[bundle][module_name];
            }
            // If the module's name isn't set, set it.
            if (!module.name) {
              Drupal.modules[bundle][module_name].name = module_name;
              module = Drupal.modules[bundle][module_name];
            }
            // Determine module directory.
            var dir = drupalgap_modules_get_bundle_directory(bundle);
            module_base_path = dir + '/' + module.name;
            // Add module .js file to array of paths to load.
            var extension = module.minified ? '.min.js' : '.js';
            module_path = module_base_path + '/' + module.name + extension;
            modules_paths = [module_path];
            // If there are any includes with this module, add them to the
            // list of paths to include.
            if (module.includes != null && module.includes.length != 0) {
              for (var include_index in module.includes) {
                if (!module.includes.hasOwnProperty(include_index)) { continue; }
                var include_object = module.includes[include_index];
                modules_paths.push(
                  module_base_path + '/' + include_object.name + '.js'
                );
              }
            }
            // Now load all the paths for this module.
            for (var modules_paths_index in modules_paths) {
                if (!modules_paths.hasOwnProperty(modules_paths_index)) { continue; }
                var modules_paths_object = modules_paths[modules_paths_index];
                jQuery.ajax({
                    async: false,
                    type: 'GET',
                    url: modules_paths_object,
                    data: null,
                    success: function() {
                      if (Drupal.settings.debug) { dpm(modules_paths_object); }
                    },
                    dataType: 'script',
                    error: function(xhr, textStatus, errorThrown) {
                      var msg = t('Failed to load module!') +
                        ' (' + module.name + ')';
                      dpm(msg);
                      console.log(modules_paths_object);
                      dpm(textStatus);
                      dpm(errorThrown.message);
                      drupalgap_alert(msg);
                    }
                });
            }
        }
    }
    // Now invoke hook_install on all modules, including core.
    module_invoke_all('install');
  }
  catch (error) { console.log('drupalgap_load_modules - ' + error); }
}

/**
 * Load the theme specified by drupalgap.settings.theme into drupalgap.theme
 * Returns true on success, false if it fails.
 * @return {Boolean}
 */
function drupalgap_load_theme() {
  try {
    if (!drupalgap.settings.theme) {
      var msg = 'drupalgap_load_theme - ' +
        t('no theme specified in settings.js');
      drupalgap_alert(msg);
    }
    else {
      // Pull the theme name from the settings.js file.
      var theme_name = drupalgap.settings.theme;
      var theme_path = 'themes/' + theme_name + '/' + theme_name + '.js';
      if (theme_name != 'easystreet3' && theme_name != 'ava') {
        theme_path = 'app/themes/' + theme_name + '/' + theme_name + '.js';
      }
      if (!drupalgap_file_exists(theme_path)) {
        var error_msg = 'drupalgap_theme_load - ' + t('Failed to load theme!') +
          ' ' + t('The theme\'s JS file does not exist') + ': ' + theme_path;
        drupalgap_alert(error_msg);
        return false;
      }
      // We found the theme's js file, add it to the page.
      drupalgap_add_js(theme_path);
      // Call the theme's template_info implementation.
      var template_info_function = theme_name + '_info';
      if (drupalgap_function_exists(template_info_function)) {
        var fn = window[template_info_function];
        drupalgap.theme = fn();
        // For each region in the name, set the 'name' value on the region JSON.
        for (var name in drupalgap.theme.regions) {
            if (!drupalgap.theme.regions.hasOwnProperty(name)) { continue; }
            var region = drupalgap.theme.regions[name];
            drupalgap.theme.regions[name].name = name;
        }
        // Make sure the theme implements the required regions.
        var regions = system_regions_list();
        for (var i = 0; i < regions.length; i++) {
          var region = regions[i];
          if (typeof drupalgap.theme.regions[region] === 'undefined') {
            console.log('WARNING: drupalgap_load_theme() - The "' +
                        theme_name + '" theme does not have the "' + region +
                        '" region specified in "' + theme_name + '_info()."');
          }
        }
        // Theme loaded successfully! Set the drupalgap.theme_path and return
        // true.
        drupalgap.theme_path = theme_path.replace('/' + theme_name + '.js', '');
        return true;
      }
      else {
        var error_msg = 'drupalgap_load_theme() - ' + t('failed') + ' - ' +
          template_info_function + '() ' + t('does not exist!');
        drupalgap_alert(error_msg);
      }
    }
    return false;
  }
  catch (error) { console.log('drupalgap_load_theme - ' + error); }
}

/**
 * Given a path to a javascript file relative to the app's www directory,
 * this will load the javascript file so it will be available in scope.
 */
function drupalgap_add_js() {
  try {
    var data;
    if (arguments[0]) { data = arguments[0]; }
    jQuery.ajax({
      async: false,
      type: 'GET',
      url: data,
      data: null,
      success: function() {
        if (Drupal.settings.debug) {
          // Print the js path to the console.
          console.log(data);
        }
      },
      dataType: 'script',
      error: function(xhr, textStatus, errorThrown) {
        console.log(
          'drupalgap_add_js - error - (' +
            data + ' : ' + textStatus +
          ') ' + errorThrown
        );
      }
    });
  }
  catch (error) {
    console.log('drupalgap_add_js - ' + error);
  }
}

/**
 * Given a path to a css file relative to the app's www directory, this will
 * attempt to load the css file so it will be available in scope.
 */
function drupalgap_add_css() {
  try {
    var data;
    if (arguments[0]) { data = arguments[0]; }
    $('<link/>', {rel: 'stylesheet', href: data}).appendTo('head');
  }
  catch (error) { console.log('drupalgap_add_css - ' + error); }
}

/**
 * Rounds up all blocks defined by hook_block_info and places them in the
 * drupalgap.blocks array.
 */
function drupalgap_load_blocks() {
  try {
    drupalgap.blocks = module_invoke_all('block_info');
  }
  catch (error) { console.log('drupalgap_load_blocks - ' + error); }
}

/**
 * Loads language files.
 */
function drupalgap_load_locales() {
  try {

    // Load any drupalgap.settings.locale specified language files.
    if (typeof drupalgap.settings.locale === 'undefined') { return; }
    for (var language_code in drupalgap.settings.locale) {
      if (!drupalgap.settings.locale.hasOwnProperty(language_code)) {
        continue;
      }
      var language = drupalgap.settings.locale[language_code];
      var file_path = 'locale/' + language_code + '.json';
      if (!drupalgap_file_exists(file_path)) { continue; }
      drupalgap.locale[language_code] = drupalgap_file_get_contents(
        file_path,
        { dataType: 'json' }
      );
    }

    // Load any language files specified by modules, and merge them into the
    // global language file (or create a new one if it doesn't exist).
    var modules = module_implements('locale');
    for (var i = 0; i < modules.length; i++) {
      var module = modules[i];
      var fn = window[module + '_locale'];
      var languages = fn();
      for (var j = 0; j < languages.length; j++) {
        var language_code = languages[j];
        var file_path = drupalgap_get_path('module', module) + '/locale/' + language_code + '.json';
        var translations = drupalgap_file_get_contents(
          file_path,
          { dataType: 'json' }
        );
        if (typeof drupalgap.locale[language_code] === 'undefined') {
          drupalgap.locale[language_code] = translations;
        }
        else {
          $.extend(
            drupalgap.locale[language_code],
            drupalgap.locale[language_code],
            translations
          );
        }
      }
    }

  }
  catch (error) { console.log('drupalgap_load_locales - ' + error); }
}

/**
 * Checks for an Internet connection, returns true if connected, false otherwise.
 * @returns {boolean}
 */
function drupalgap_has_connection() {
  try {
    drupalgap_check_connection();
    module_invoke_all('device_connection');
    return drupalgap.online;
  }
  catch (error) { console.log('drupalgap_has_connection - ' + error); }
}

/**
 * Checks the devices connection and sets drupalgap.online to true if the
 * device has a connection, false otherwise.
 * @return {String}
 *   A string indicating the type of connection according to PhoneGap.
 */
function drupalgap_check_connection() {
  try {
    // If we're not in phonegap, just use the navigator.onLine value.
    if (drupalgap.settings.mode != 'phonegap' || typeof parent.window.ripple === 'function' ) {
      drupalgap.online = navigator.onLine;
      return 'Ethernet connection'; // @TODO detect real connection type.
    }

    // Determine what connection phonegap has.
    var networkState = navigator.connection.type;
    var states = {};
    states[Connection.UNKNOWN] = 'Unknown connection';
    states[Connection.ETHERNET] = 'Ethernet connection';
    states[Connection.WIFI] = 'WiFi connection';
    states[Connection.CELL_2G] = 'Cell 2G connection';
    states[Connection.CELL_3G] = 'Cell 3G connection';
    states[Connection.CELL_4G] = 'Cell 4G connection';
    states[Connection.NONE] = 'No network connection';
    drupalgap.online = states[networkState] != 'No network connection';
    return states[networkState];
  }
  catch (error) { console.log('drupalgap_check_connection - ' + error); }
}

/**
 * @deprecated Use empty() instead.
 * Returns true if given value is empty. A generic way to test for emptiness.
 * @param {*} value
 * @return {Boolean}
 */
function drupalgap_empty(value) {
  try {
    console.log(
      'WARNING: drupalgap_empty() is deprecated! ' +
      'Use empty() instead.'
    );
    return empty(value);
  }
  catch (error) { console.log('drupalgap_empty - ' + error); }
}

/**
 * Checks if a given file exists, returns true or false.
 * @param  {string} path
 *   A path to a file
 * @return {bool}
 *   True if file exists, else false.
 */
function drupalgap_file_exists(path) {
  try {
    var file_exists = false;
    jQuery.ajax({
      async: false,
      type: 'HEAD',
      dataType: 'text',
      url: path,
      success: function() { file_exists = true; },
      error: function(xhr, textStatus, errorThrown) { }
    });
    return file_exists;
  }
  catch (error) { console.log('drupalgap_file_exists - ' + error); }
}

/**
 * Reads entire file into a string and returns the string. Returns false if
 * it fails.
 * @param {String} path
 * @param {Object} options
 * @return {String}
 */
function drupalgap_file_get_contents(path, options) {
  try {
    var file = false;
    var default_options = {
      type: 'GET',
      url: path,
      dataType: 'html',
      data: null,
      async: false,
      success: function(data) { file = data; },
      error: function(xhr, textStatus, errorThrown) {
        console.log(
          'drupalgap_file_get_contents - failed to load file (' + path + ')'
        );
      }
    };
    $.extend(default_options, options);
    jQuery.ajax(default_options);
    return file;
  }
  catch (error) { console.log('drupalgap_file_get_contents - ' + error); }
}

/**
 * @see https://api.drupal.org/api/drupal/includes!common.inc/function/format_interval/7
 * @param {Number} interval The length of the interval in seconds.
 * @return {String}
 */
function drupalgap_format_interval(interval) {
  try {
    // @TODO - deprecate this and move it to jDrupal as format_interval().
    var granularity = 2; if (arguments[1]) { granularity = arguments[1]; }
    var langcode = null; if (arguments[2]) { langcode = langcode[2]; }
    var units = {
      '1 year|@count years': 31536000,
      '1 month|@count months': 2592000,
      '1 week|@count weeks': 604800,
      '1 day|@count days': 86400,
      '1 hour|@count hours': 3600,
      '1 min|@count min': 60,
      '1 sec|@count sec': 1
    };
    var output = '';
    for (var key in units) {
      if (!units.hasOwnProperty(key)) { continue; }
      var value = units[key];
      var key = key.split('|');
      if (interval >= value) {
        var count = Math.floor(interval / value);
        output +=
          (output ? ' ' : '') +
          drupalgap_format_plural(
            count,
            key[0],
            key[1]
          );
        if (output.indexOf('@count') != -1) {
          output = output.replace('@count', count);
        }
        interval %= value;
        granularity--;
      }
      if (granularity == 0) { break; }
    }
    return output ? output : '0 sec';
  }
  catch (error) { console.log('drupalgap_format_interval - ' + error); }
}

/**
 * @see http://api.drupal.org/api/drupal/includes%21common.inc/function/format_plural/7
 * @param {Number} count
 * @param {String} singular
 * @param {String} plural
 * @return {String}
 */
function drupalgap_format_plural(count, singular, plural) {
    // @TODO - deprecate this and move it to jDrupal as format_plural().
    if (count == 1) { return singular; }
    return plural;
}

/**
 * @deprecated - Use function_exists() instead.
 * @param {String} name
 * @return {Boolean}
 */
function drupalgap_function_exists(name) {
  try {
    return function_exists(name);
  }
  catch (error) { console.log('drupalgap_function_exists - ' + error); }
}

/**
 * Given an html string from a *.tpl.html file, this will extract all of the
 * placeholders names and return them in an array. Returns false otherwise.
 * @param {String} html
 * @return {*}
 */
function drupalgap_get_placeholders_from_html(html) {
  try {
    var placeholders = false;
    if (html) {
      placeholders = html.match(/(?!{:)([\w]+)(?=:})/g);
    }
    return placeholders;
  }
  catch (error) {
    console.log('drupalgap_get_placeholders_from_html - ' + error);
  }
}

/**
 * Returns the current page's title.
 * @return {String}
 */
function drupalgap_get_title() {
  try {
    return drupalgap.page.title;
  }
  catch (error) { console.log('drupalgap_get_title - ' + error); }
}

/**
 * Returns the IP Address of the current user as reported by PHP via the last
 * System Connect call's $_SERVER['REMOTE_ADDR'] value.
 * @return {String|Null}
 */
function drupalgap_get_ip() {
  try {
    return drupalgap.remote_addr;
  }
  catch (error) { console.log('drupalgap_get_ip - ' + error); }
}

/**
 * Given a router path, this will return an array containing the indexes of
 * where the wildcards (%) are present in the router path. Returns false if
 * there are no wildcards present.
 * @param {String} router_path
 * @return {Boolean}
 */
function drupalgap_get_wildcards_from_router_path(router_path) {
    // @todo - Is this function even used? Doesn't look like it.
    var wildcards = false;
    return wildcards;
}


/**
 * Given a drupal image file uri, this will return the path to the image on the
 * Drupal site.
 * @param {String} uri
 * @return {String}
 */
function drupalgap_image_path(uri) {
  try {
    var altered = false;
    // If any modules want to alter the path, let them do it.
    var modules = module_implements('image_path_alter');
    if (modules) {
      for (var index in modules) {
          if (!modules.hasOwnProperty(index)) { continue; }
          var module = modules[index];
          var result = module_invoke(module, 'image_path_alter', uri);
          if (result) {
            altered = true;
            uri = result;
            break;
          }
      }
    }
    if (!altered) {
      // No one modified the image path, we'll use the default approach to
      // generating the image src path.
      var src = Drupal.settings.site_path + Drupal.settings.base_path + uri;
      if (src.indexOf('public://') != -1) {
        src = src.replace('public://', Drupal.settings.file_public_path + '/');
      }
      else if (src.indexOf('private://') != -1) {
        src = src.replace(
          'private://',
          Drupal.settings.file_private_path + '/'
        );
      }
      return src;
    }
    else { return uri; }
  }
  catch (error) { console.log('drupalgap_image_path - ' + error); }
}

/**
 * @deprecated - This is no longer needed since the includes are built via the
 * makefile. Loads the js files in includes specified by drupalgap.includes.
 */
function drupalgap_includes_load() {
  try {
    if (drupalgap.includes != null && drupalgap.includes.length != 0) {
      for (var index in drupalgap.includes) {
          if (!drupalgap.includes.hasOwnProperty(index)) { continue; }
          var include = drupalgap.includes[index];
          var include_path = 'includes/' + include.name + '.inc.js';
          jQuery.ajax({
              async: false,
              type: 'GET',
              url: include_path,
              data: null,
              success: function() {
                if (Drupal.settings.debug) {
                  // Print the include path to the console.
                  dpm(include_path);
                }
              },
              dataType: 'script',
              error: function(xhr, textStatus, errorThrown) {
                console.log(errorThrown);
              }
          });
      }
    }
  }
  catch (error) { console.log('drupalgap_includes_load - ' + error); }
}

/**
 * Given an html list element id and an array of items, this will clear the
 * list, populate it with the items, and then refresh the list.
 * @param {String} list_css_selector
 * @param {Array} items
 */
function drupalgap_item_list_populate(list_css_selector, items) {
  try {
    // @todo - This could use some validation and alerts for improper input.
    $(list_css_selector).html('');
    for (var i = 0; i < items.length; i++) {
      $(list_css_selector).append($('<li></li>', { html: items[i] }));
    }
    $(list_css_selector).listview('refresh').listview();
  }
  catch (error) { console.log('drupalgap_item_list_populate - ' + error); }
}

/**
 * Given an html table element id and an array of rows, this will clear the
 * table, populate it with the rows, and then refresh the table.
 * @param {String} table_css_selector
 * @param {Array} rows
 * rows follow the.
 */
function drupalgap_table_populate(table_css_selector, rows) {
  try {
    // Select only the body. Other things are already setup
    table_css_selector = table_css_selector + '> tbody ';
    $(table_css_selector).html('');
    for (var i = 0; i < rows.length; i++) {
      var row = rows[i];
      var rowhtml = '';
      for (var j = 0; j < row.length; j++) {
          rowhtml = rowhtml + '<td>' + row[j] + '</td>';
      }
      $('<tr>').html(rowhtml).appendTo($(table_css_selector));
    }
    $(table_css_selector).rebuild();
  }
  catch (error) { console.log('drupalgap_table_populate - ' + error); }
}

/**
 * Given a jQM page event, and the corresponding callback function name that
 * handles the event, this function will call the callback function, if it has
 * not already been called on the current page. This really is only used by
 * menu_execute_active_handler() to prevent jQM from firing inline page event
 * handlers more than once. You may optionally pass in a 4th argument, a string,
 * to append to the suffix of the unique key of recorded fired page events.
 * @param {String} event
 * @param {String} callback
 * @param {*} page_arguments
 */
function drupalgap_jqm_page_event_fire(event, callback, page_arguments) {
  try {
    // Concatenate the event name and the callback name together into a unique
    // key so multiple callbacks can handle the same event.
    var key = event + '-' + callback;
    // Is there an optional 4th argument coming in (the suffix)?
    if (typeof arguments[3] !== 'undefined') {
      if (arguments[3]) { key += '-' + arguments[3]; }
    }
    if ($.inArray(key, drupalgap.page.jqm_events) == -1 &&
      drupalgap_function_exists(callback)) {
      drupalgap.page.jqm_events.push(key);
      var fn = window[callback];
      if (page_arguments) {
        // If the page arguments aren't an array, place them into an array so
        // they can be applied to the callback function.
        if (!$.isArray(page_arguments)) { page_arguments = [page_arguments]; }
        fn.apply(null, Array.prototype.slice.call(page_arguments));
      }
      else { fn(); }
    }
  }
  catch (error) { console.log('drupalgap_jqm_page_event_fire - ' + error); }
}

/**
 * Returns array of jQM Page event names.
 * @return {Array}
 * @see http://api.jquerymobile.com/category/events/
 */
function drupalgap_jqm_page_events() {
  return [
    'pagebeforechange',
    'pagebeforecreate',
    'pagebeforehide',
    'pagebeforeload',
    'pagebeforeshow',
    'pagechange',
    'pagechangefailed',
    'pagecreate',
    'pagehide',
    'pageinit',
    'pageload',
    'pageloadfailed',
    'pageremove',
    'pageshow'
  ];
}

/**
 * Given a JSON object with a page id, a jQM page event name, a callback
 * function to handle the jQM page event and any page arguments (as a JSON
 * string), this function will return the inline JS code needed to handle the
 * event. You may optionally pass in a unique second argument (string) to
 * allow the same page event handler to be fired more than once on a page.
 * @param {Object} options
 * @return {String}
 */
function drupalgap_jqm_page_event_script_code(options) {
  try {
    // Build the arguments to send to the event fire handler.
    var event_fire_args = '"' + options.jqm_page_event + '", "' +
      options.jqm_page_event_callback + '", ' +
      options.jqm_page_event_args;
    if (arguments[1]) { event_fire_args += ', "' + arguments[1] + '"'; }
    // Build the inline JS and return it.
    var script_code = '<script type="text/javascript">' +
      '$("#' + options.page_id + '").on("' +
        options.jqm_page_event + '", drupalgap_jqm_page_event_fire(' +
          event_fire_args +
        '));' +
    '</script>';
    return script_code;
  }
  catch (error) {
    console.log('drupalgap_jqm_page_event_script_code - ' + error);
  }
}

/**
 * Returns the suggested max width for elements within the content area.
 * @return {Number}
 */
function drupalgap_max_width() {
  try {
    var padding = parseInt($('.ui-content').css('padding'));
    if (isNaN(padding)) { padding = 16; } // use a 16px default if needed
    return $(document).width() - padding * 2;
  }
  catch (error) { console.log('drupalgap_max_width - ' + error); }
}

/**
 * Checks to see if the current user has access to the given path. Returns true
 * if the user has access, false otherwise. You may optionally pass in a user
 * account object as the second argument to check access on a specific user.
 * Also, you may optionally pass in an entity object as the third argument, if
 * that entity needs to be passed along to an 'access_callback' handler.
 * @param {String} path
 * @return {Boolean}
 */
function drupalgap_menu_access(path) {
  try {

    // User #1 is allowed to do anything, I mean anything.
    if (Drupal.user.uid == 1) { return true; }
    // Everybody else will not have access unless we prove otherwise.
    var access = false;
    if (drupalgap.menu_links[path]) {
      // Check to see if there is an access callback specified with the menu
      // link.
      if (typeof drupalgap.menu_links[path].access_callback === 'undefined') {
        // No access call back specified, if there are any access arguments
        // on the menu link, then it is assumed they are user permission machine
        // names, so check that user account's role(s) for that permission to
        // grant access.
        if (drupalgap.menu_links[path].access_arguments) {
          if ($.isArray(drupalgap.menu_links[path].access_arguments)) {
            for (var index in drupalgap.menu_links[path].access_arguments) {
              if (!drupalgap.menu_links[path].access_arguments.hasOwnProperty(index)) { continue; }
              var permission = drupalgap.menu_links[path].access_arguments[index];
              access = user_access(permission);
              if (access) { break; }
            }
          }
        }
        else {
          // There is no access callback and no access arguments specified with
          // the menu link, so we'll assume everyone has access.
          access = true;
        }
      }
      else {

        // An access callback function is specified for this path...
        var function_name = drupalgap.menu_links[path].access_callback;
        if (drupalgap_function_exists(function_name)) {
          // Grab the access callback function. If there are any access args
          // send them along, or just call the function directly.
          // access arguments.
          var fn = window[function_name];
          if (drupalgap.menu_links[path].access_arguments) {
            var access_arguments =
              drupalgap.menu_links[path].access_arguments.slice(0);
            // If we have an entity loaded, replace the first integer we find
            // in the page arguments with the loaded entity.
            if (arguments[2]) {
              var entity = arguments[2];
              for (var index in access_arguments) {
                  if (!access_arguments.hasOwnProperty(index)) { continue; }
                  var page_argument = access_arguments[index];
                  if (is_int(parseInt(page_argument))) {
                    access_arguments[index] = entity;
                    break;
                  }
              }
            }
            return fn.apply(null, Array.prototype.slice.call(access_arguments));
          }
          else { return fn(); }
        }
        else {
          console.log('drupalgap_menu_access - access call back (' +
            function_name + ') does not exist'
          );
        }
      }
    }
    else {
      console.log('drupalgap_menu_access - path (' + path + ') does not exist');
    }
    return access;
  }
  catch (error) { console.log('drupalgap_menu_access - ' + error); }
}

/**
 * @deprecated Use module_load() instead.
 * @param {String} name
 * @return {Object}
 */
function drupalgap_module_load(name) {
  try {
    return module_load(name);
  }
  catch (error) { console.log('drupalgap_module_load - ' + error); }
}

/**
 * Given a module bundle type, this will return the path to that module bundle's
 * directory.
 * @param {String} bundle
 * @return {String}
 */
function drupalgap_modules_get_bundle_directory(bundle) {
  try {
    dir = '';
    if (bundle == 'core') { dir = 'modules'; }
    else if (bundle == 'contrib') { dir = 'app/modules'; }
    else if (bundle == 'custom') { dir = 'app/modules/custom'; }
    return dir;
  }
  catch (error) {
    console.log('drupalgap_modules_get_bundle_directory - ' + error);
  }
}

/**
 * Given a router path (and optional path, defaults to current drupalgap path if
 * one isn't provided), this takes the path's arguments and replaces any
 * wildcards (%) in the router path with the corresponding path argument(s). It
 * then returns the assembled path. Returns false otherwise.
 * @param {String} input_path
 * @return {*}
 */
function drupalgap_place_args_in_path(input_path) {
  try {
    var assembled_path = false;
    if (input_path) {

      // Determine path to use and break it up into its args.
      var path = drupalgap_path_get();
      if (arguments[1]) { path = arguments[1]; }
      var path_args = arg(null, path);

      // Grab wild cards from router path then replace each wild card with
      // the corresponding path arg.
      var wildcards;
      var input_path_args = arg(null, input_path);
      if (input_path_args && input_path_args.length > 0) {
        for (var index in input_path_args) {
            if (!input_path_args.hasOwnProperty(index)) { continue; }
            var _arg = input_path_args[index];
            if (_arg == '%') {
              if (!wildcards) { wildcards = []; }
              wildcards.push(index);
            }
        }
        if (wildcards && wildcards.length > 0) {
          for (var index in wildcards) {
              if (!wildcards.hasOwnProperty(index)) { continue; }
              var wildcard = wildcards[index];
              if (path_args[wildcard]) {
                input_path_args[wildcard] = path_args[wildcard];
              }
          }
          assembled_path = input_path_args.join('/');
        }
      }
    }
    return assembled_path;
  }
  catch (error) {
    console.log('drupalgap_place_args_in_path - ' + error);
  }
}

/**
 * Given an args array, this returns true if the path in the array will have an
 * entity (id) present in it.
 * @param {Array} args
 * @return {Boolean}
 */
function drupalgap_path_has_entity_arg(args) {
  try {
    if (args.length > 1 &&
        (
          args[0] == 'comment' ||
          args[0] == 'file' ||
          args[0] == 'node' ||
          (args[0] == 'taxonomy' &&
            (args[1] == 'vocabulary' || args[1] == 'term')
          ) ||
          args[0] == 'user' ||
          args[0] == 'item'
        )
    ) { return true; }
    return false;
  }
  catch (error) { console.log('drupalgap_path_has_entity_arg - ' + error); }
}

/**
 * Given a page id, this will remove it from the DOM.
 * @param {String} page_id
 */
function drupalgap_remove_page_from_dom(page_id) {
  try {
    $('#' + page_id).empty().remove();
  }
  catch (error) { console.log('drupalgap_remove_page_from_dom - ' + error); }
}

/**
 * Restart the app.
 */
function drupalgap_restart() {
  try {
    location.reload();
  }
  catch (error) { console.log('drupalgap_restart - ' + error); }
}

/**
 * Implementation of drupal_set_title().
 * @param {String} title
 */
function drupalgap_set_title(title) {
  try {
    if (title) { drupalgap.page.title = title; }
  }
  catch (error) { console.log('drupalgap_set_title - ' + error); }
}

/**
 * Implements hook_services_preprocess().
 * @param {Object} options
 */
function drupalgap_services_preprocess(options) {
  try {
    // Show the loading icon.
    drupalgap_loading_message_show();
  }
  catch (error) { console.log('drupalgap_services_preprocess - ' + error); }
}

/**
 * Implements hook_services_postprocess().
 * @param {Object} options
 * @param {Object} result
 */
function drupalgap_services_postprocess(options, result) {
  try {
    // Hide the loading icon.
    drupalgap_loading_message_hide();
  }
  catch (error) { console.log('drupalgap_services_postprocess - ' + error); }
}

/**
 * Implements hook_services_request_pre_postprocess_alter().
 * @param {Object} options
 * @param {*} result
 */
function drupalgap_services_request_pre_postprocess_alter(options, result) {
  try {
    // Extract drupalgap system connect service resource results.
    if (options.service == 'system' && options.resource == 'connect') {
      drupalgap.remote_addr = result.remote_addr;
      drupalgap.entity_info = result.entity_info;
      drupalgap.field_info_instances = result.field_info_instances;
      drupalgap.field_info_fields = result.field_info_fields;
      drupalgap.field_info_extra_fields = result.field_info_extra_fields;
      drupalgap.taxonomy_vocabularies =
        drupalgap_taxonomy_vocabularies_extract(
          result.taxonomy_vocabularies
        );
      drupalgap_service_resource_extract_results({
        service: options.service,
        resource: options.resource,
        data: result
      });
    }
    // Whenever a user logs in, out or registers, remove all pages from the DOM.
    else if (options.service == 'user' &&
      (options.resource == 'logout' || options.resource == 'login' ||
        options.resource == 'register')) {
      drupalgap_remove_pages_from_dom();
    }
    // Whenever an entity is created, updated or deleted, remove the
    // corresponing DrupalGap core page(s) from the DOM so the pages will be
    // rebuilt properly next time they are loaded.
    else if (
      in_array(options.resource, ['create', 'update', 'delete']) &&
      in_array(options.service, entity_types())
    ) {
      var entity_type = options.entity_type;
      var entity_id = options.entity_id;
      var bundle = options.bundle || null;
      var paths = [];
      if (options.resource != 'create') {
        var prefix = entity_type;
        if (in_array(entity_type, ['taxonomy_vocabulary', 'taxonomy_term'])) {
          prefix = prefix.replace('_', '/', prefix);
        }
        paths.push(prefix + '/' + entity_id);
        paths.push(prefix + '/' + entity_id + '/view');
        // @todo This page won't get removed since it is the current page...
        // maybe when an entity is updated or deleted, we need a transitional
        // page that says "Deleting [entity]..." that way we can remove this
        // page (since it is not possible to remove the current page in jQM).
        // Actually now that I think about, we should have a confirmation page
        // when deleting an entity just like Drupal, and that should take care
        // of the deletion case. Not sure what to do about the update case...
        // maybe some type of pageshow handler on entity view pages that can
        // remove the edit form for the entity.
        paths.push(prefix + '/' + entity_id + '/edit');
      }
      else {
        switch (entity_type) {
          case 'node':
            // @todo This page won't get removed since it is the current page...
            paths.push('node/add/' + bundle);
            break;
        }
      }
      // Add extras depending on the entity type.
      switch (entity_type) {
        case 'node': paths.push('node'); break;
        case 'taxonomy_vocabulary': paths.push('taxonomy/vocabularies'); break;
        case 'user': paths.push('user-listing'); break;
      }
      // Convert the paths to page ids, then remove them from the DOM.
      var pages = [];
      for (var index in paths) {
          if (!paths.hasOwnProperty(index)) { continue; }
          var path = paths[index];
          pages.push(drupalgap_get_page_id(path));
      }
      for (var index in pages) {
          if (!pages.hasOwnProperty(index)) { continue; }
          var page_id = pages[index];
          drupalgap_remove_page_from_dom(page_id);
      }
    }
  }
  catch (error) {
    console.log('drupalgap_services_request_pre_postprocess_alter - ' + error);
  }
}

/**
 * @deprecated - Loads the settings specified in app/settings.js into the app.
 */
function drupalgap_settings_load() {
  try {
    console.log('WARNING: drupalgap_settings_load() is deprecated!');
    //drupal_settings_load();
  }
  catch (error) {
    console.log('drupalgap_settings_load - ' + error);
  }
}

/**
 * This calls all implementations of hook_theme and builds the DrupalGap theme
 * registry.
 */
function drupalgap_theme_registry_build() {
  try {
    var modules = module_implements('theme');
    for (var index in modules) {
        if (!modules.hasOwnProperty(index)) { continue; }
        var module = modules[index];
        var function_name = module + '_theme';
        var fn = window[function_name];
        var hook_theme = fn();
        for (var element in hook_theme) {
            if (!hook_theme.hasOwnProperty(element)) { continue; }
            var variables = hook_theme[element];
            variables.path = drupalgap_get_path(
              'theme',
              drupalgap.settings.theme
            );
            drupalgap.theme_registry[element] = variables;
        }
    }
  }
  catch (error) { console.log('drupalgap_theme_registry_build - ' + error); }
}

/**
 * Given a variable name and value, this will save the value to local storage,
 * keyed by its name.
 * @param {String} name
 * @param {*} value
 * @return {*}
 */
function variable_set(name, value) {
  try {
    if (!value) { value = ' '; } // store null values as a single space*
    else if (is_int(value)) { value = value.toString(); }
    else if (typeof value === 'object') { value = JSON.stringify(value); }
    return window.localStorage.setItem(name, value);
    // * phonegap won't store an empty string in local storage
  }
  catch (error) { drupalgap_error(error); }
}

/**
 * Given a variable name and a default value, this will first attempt to load
 * the variable from local storage, if it can't then the default value will be
 * returned.
 * @param {String} name
 * @param {*} default_value
 * @return {*}
 */
function variable_get(name, default_value) {
  try {
    var value = window.localStorage.getItem(name);
    if (!value) { value = default_value; }
    if (value == ' ') { value = ''; } // Convert single spaces to empty strings.
    return value;
  }
  catch (error) { drupalgap_error(error); }
}

/**
 * Given a variable name, this will remove the value from local storage.
 * @param {String} name
 * @return {*}
 */
function variable_del(name) {
  try {
    return window.localStorage.removeItem(name);
  }
  catch (error) { drupalgap_error(error); }
}

/**
 * Returns the current time as a string with the format: "yyyy-mm-dd hh:mm:ss".
 * @return {String}
 */
function date_yyyy_mm_dd_hh_mm_ss() {
  try {
    var result;
    if (arguments[0]) { result = arguments[0]; }
    else { result = date_yyyy_mm_dd_hh_mm_ss_parts(); }
    return result['year'] + '-' + result['month'] + '-' + result['day'] + ' ' +
           result['hour'] + ':' + result['minute'] + ':' + result['second'];
  }
  catch (error) { console.log('date_yyyy_mm_dd_hh_mm_ss - ' + error); }
}

/**
 * Returns an array with the parts for the current time. You may optionally
 * pass in a JS date object to use that date instead.
 * @return {Array}
 */
function date_yyyy_mm_dd_hh_mm_ss_parts() {
  try {
    var result = [];
    var now = null;
    if (arguments[0]) { now = arguments[0]; }
    else { now = new Date(); }
    var year = '' + now.getFullYear();
    var month = '' + (now.getMonth() + 1);
    if (month.length == 1) { month = '0' + month; }
    var day = '' + now.getDate();
    if (day.length == 1) { day = '0' + day; }
    var hour = '' + now.getHours();
    if (hour.length == 1) { hour = '0' + hour; }
    var minute = '' + now.getMinutes();
    if (minute.length == 1) { minute = '0' + minute; }
    var second = '' + now.getSeconds();
    if (second.length == 1) { second = '0' + second; }
    result['year'] = year;
    result['month'] = month;
    result['day'] = day;
    result['hour'] = hour;
    result['minute'] = minute;
    result['second'] = second;
    return result;
  }
  catch (error) { console.log('date_yyyy_mm_dd_hh_mm_ss_parts - ' + error); }
}

/**
 * Given a year and month (0-11), this will return the number of days in that
 * month.
 * @see http://stackoverflow.com/a/1810990/763010
 * @param {Number} year
 * @param {Number} month
 * @return {Number}
 */
function date_number_of_days_in_month(year, month) {
  try {
    var d = new Date(year, month, 0);
    return d.getDate();
  }
  catch (error) { console.log('date_number_of_days_in_month - ' + error); }
}

/**
 * @see http://www.dconnell.co.uk/blog/index.php/2012/03/12/scroll-to-any-element-using-jquery/
 */
function scrollToElement(selector, time, verticalOffset) {
  try {
    time = typeof(time) != 'undefined' ? time : 1000;
    verticalOffset = typeof(verticalOffset) != 'undefined' ? verticalOffset : 0;
    element = $(selector);
    offset = element.offset();
    offsetTop = offset.top + verticalOffset;
    $('html, body').animate({
        scrollTop: offsetTop
    }, time);
  }
  catch (error) { console.log('scrollToElement - ' + error); }
}


/**
 * Autocomplete global variables. Used to hold onto various global variables
 * needed for an autocomplete.
 */
// The autocomplete text field input selector.
var _theme_autocomplete_input_selector = {};

// The autocomplete remote boolean.
var _theme_autocomplete_remote = {};

// The theme autocomplete variables.
var _theme_autocomplete_variables = {};

// The theme autocomplete variables.
var _theme_autocomplete_success_handlers = {};

/**
 * Themes an autocomplete.
 * @param {Object} variables
 * @return {String}
 */
function theme_autocomplete(variables) {
  try {
    var html = '';

    // We need to have a unique identifier for this autocomplete. If it is a
    // field, use the field name. Otherwise use the id attribute if it is
    // provided or generate a random one. Then finally attach the autocomplete
    // id to the variables so it can be passed along.
    var autocomplete_id = null;
    if (typeof variables.field_info_field !== 'undefined') {
      autocomplete_id = variables.field_info_field.field_name + '_' + variables.delta;
    }
    else if (typeof variables.attributes.id !== 'undefined') {
      autocomplete_id = variables.attributes.id;
    }
    else { autocomplete_id = user_password(); }
    variables.autocomplete_id = autocomplete_id;

    // Hold onto a copy of the variables.
    _theme_autocomplete_variables[autocomplete_id] = {};
    $.extend(true, _theme_autocomplete_variables[autocomplete_id], variables);

    // Are we dealing with a remote data set?
    var remote = false;
    if (variables.remote) { remote = true; }
    variables.remote = remote;
    _theme_autocomplete_remote[autocomplete_id] = variables.remote;

    // Make sure we have an id to use on the list.
    var id = null;
    if (variables.attributes.id) { id = variables.attributes.id; }
    else {
      id = 'autocomplete_' + user_password();
      variables.attributes.id = id;
    }

    // We need a hidden input to hold the value. If a default value
    // was provided by a form element, use it.
    var hidden_attributes = { id: id };
    if (
      variables.element &&
      typeof variables.element.default_value !== 'undefined'
    ) { hidden_attributes.value = variables.element.default_value; }
    html += theme('hidden', { attributes: hidden_attributes });

    // Now we need an id for the list.
    var list_id = id + '-list';

    // Build the widget variables.
    var widget = {
      attributes: {
        'id': list_id,
        'data-role': 'listview',
        'data-filter': 'true',
        'data-inset': 'true',
        'data-filter-placeholder': '...'
      }
    };

    // Handle a remote data set.
    var js = '';
    if (variables.remote) {
      widget.items = [];
      // We have a remote data set.
      js += '<script type="text/javascript">' +
        '$("#' + list_id + '").on("filterablebeforefilter", function(e, d) { ' +
          '_theme_autocomplete(this, e, d, "' + autocomplete_id + '"); ' +
        '});' +
      '</script>';
    }
    else {
      // Prepare the items then set the data filter reveal attribute.
      widget.items = _theme_autocomplete_prepare_items(variables);
      widget.attributes['data-filter-reveal'] = true;
    }

    // Save a reference to the autocomplete text field input.
    var selector = '#' + drupalgap_get_page_id() +
      ' #' + id + ' + form.ui-filterable' +
      ' input[data-type="search"]';
    js += '<script type="text/javascript">' +
      '_theme_autocomplete_input_selector["' + autocomplete_id + '"] = \'' +
        selector +
      '\';' +
    '</script>';

    // If there was a default value, set it's key title in the autocomplete's
    // text field.
    if (variables.default_value_label) {
      js += drupalgap_jqm_page_event_script_code({
          page_id: drupalgap_get_page_id(),
          jqm_page_event: 'pageshow',
          jqm_page_event_callback:
            '_theme_autocomplete_set_default_value_label',
          jqm_page_event_args: JSON.stringify({
              selector: selector,
              default_value_label: variables.default_value_label
          })
      }, id);
    }

    // Theme the list and add the js to it, then return the html.
    html += theme('item_list', widget);
    html += js;
    return html;
  }
  catch (error) { console.log('theme_autocomplete - ' + error); }
}

/**
 * An internal function used to handle remote data for an autocomplete.
 * @param {Object} list The unordered list that displays the items.
 * @param {Object} e
 * @param {Object} data
 * @param {String} autocomplete_id
 */
function _theme_autocomplete(list, e, data, autocomplete_id) {
  try {
    var autocomplete = _theme_autocomplete_variables[autocomplete_id];
    // Make sure a filter is present.
    if (typeof autocomplete.filter === 'undefined') {
      console.log(
        '_theme_autocomplete - A "filter" was not supplied.'
      );
      return;
    }

    // Make sure a value and/or label has been supplied so we know how to render
    // the items in the autocomplete list.
    var value_provided = typeof autocomplete.value !== 'undefined';
    var label_provided = typeof autocomplete.label !== 'undefined';
    if (!value_provided && !label_provided) {
      console.log(
        '_theme_autocomplete - A "value" and/or "label" was not supplied.'
      );
      return;
    }
    else {
      // We have a value and/or label. If one isn't provided, set it equal to
      // the other.
      if (!value_provided) { autocomplete.value = autocomplete.label; }
      else if (!label_provided) { autocomplete.label = autocomplete.value; }
    }
    // Setup the vars to handle this widget.
    var $ul = $(list),
        $input = $(data.input),
        value = $input.val(),
        html = '';
    // Clear the list.
    $ul.html('');
    // If a value has been input, start the autocomplete search.
    if (value && value.length > 0) {
      // Show the loader icon.
      $ul.html('<li><div class="ui-loader">' +
        '<span class="ui-icon ui-icon-loading"></span>' +
        '</div></li>');
      $ul.listview('refresh');

      // Let's first build the success handler that will place the items into
      // the autocomplete list.
      _theme_autocomplete_success_handlers[autocomplete_id] = function(
        _autocomplete_id, result_items, _wrapped, _child) {
        try {

          // If there are no results, and then if an empty callback handler was
          // provided, call it.
          // empty callback handler, then call it and return.
          if (result_items.length == 0) {
            if (autocomplete.empty_callback) {
              var fn = window[autocomplete.empty_callback];
              fn(value);
            }
          }
          else {

            // Convert the result into an items array for a list. Each item will
            // be a JSON object with a "value" and "label" properties.
            var items = [];
            var _value = autocomplete.value;
            var _label = autocomplete.label;
            for (var index in result_items) {
                if (!result_items.hasOwnProperty(index)) { continue; }
                var object = result_items[index];
                var _item = null;
                if (_wrapped) { _item = object[_child]; }
                else { _item = object; }
                var item = {
                  value: _item[_value],
                  label: _item[_label]
                };
                items.push(item);
            }

            // Now render the items, add them to list and refresh the list.
            if (items.length != 0) {
              autocomplete.items = items;
              var _items = _theme_autocomplete_prepare_items(autocomplete);
              for (var index in _items) {
                  if (!_items.hasOwnProperty(index)) { continue; }
                  var item = _items[index];
                  html += '<li>' + item + '</li>';
              }
              $ul.html(html);
              $ul.listview('refresh');
              $ul.trigger('updatelayout');
            }
          }

          // Anybody want to act on the completion of the autocomplete?
          if (autocomplete.finish_callback) {
            var fn = window[autocomplete.finish_callback];
            fn(value);
          }

        }
        catch (error) {
          console.log('_theme_autocomplete_success_handlers[' +
            _autocomplete_id +
          '] - ' + error);
        }
      };

      // Depending on the handler, build the path and call the Drupal site for
      // the data. If it's a custom path, see if a handler was provided,
      // otherwise just default to views.
      var handler = null;
      if (autocomplete.custom) {
        if (autocomplete.handler) { handler = autocomplete.handler; }
        else if (autocomplete.field_info_field && autocomplete.field_info_field.settings.handler) {
          handler = autocomplete.field_info_field.settings.handler;
        }
        else { handler = 'views'; }
      }
      else if (autocomplete.field_info_field) {
        handler = autocomplete.field_info_field.settings.handler;
      }
      else { handler = 'views'; }
      switch (handler) {

        // Views (and Organic Groups)
        case 'views':
          // Prepare the path to the view.
          var path = autocomplete.path + '?' + autocomplete.filter + '=' +
            encodeURIComponent(value);
          // Any extra params to send along?
          if (autocomplete.params) { path += '&' + autocomplete.params; }

          // Retrieve JSON results. Keep in mind, we use this for retrieving
          // Views JSON results and custom hook_menu() path results in Drupal.
          views_datasource_get_view_result(path, {
              success: function(results) {
                // If this was a custom path, don't use a wrapper around the
                // results like the one used by Views Datasource.
                var wrapped = true;
                if (autocomplete.custom) { wrapped = false; }

                // Extract the result items based on the presence of the wrapper
                // or not.
                var result_items = null;
                if (wrapped) { result_items = results[results.view.root]; }
                else { result_items = results; }

                // Finally call the sucess handler.
                var fn = _theme_autocomplete_success_handlers[autocomplete_id];
                fn(autocomplete_id, result_items, wrapped, results.view.child);
              }
          });
          break;

        // Simple entity selection mode (provided by the entity reference
        // module), use the Index resource for the entity type.
        case 'base':
        case 'og':
          var field_settings =
            autocomplete.field_info_field.settings;
          var index_resource = field_settings.target_type + '_index';
          if (!drupalgap_function_exists(index_resource)) {
            console.log('WARNING - _theme_autocomplete - ' +
              index_resource + '() does not exist!'
            );
            return;
          }
          var options = {
            fields: [autocomplete.value, autocomplete.filter],
            parameters: { },
            parameters_op: { }
          };
          options.parameters[autocomplete.filter] = '%' + value + '%';
          options.parameters_op[autocomplete.filter] = 'like';
          var bundles = entityreference_get_target_bundles(field_settings);
          if (bundles) { options.parameters[entity_get_bundle_name(field_settings.target_type)] = bundles.join(','); }
          window[index_resource](options, {
              success: function(results) {
                _theme_autocomplete_success_handlers[autocomplete_id](autocomplete_id, results, false);
              }
          });
          break;

        // An entity index resource call. Figure out which entity type index
        // to call, and build a default query if one wasn't provided.
        case 'index':
          if (!autocomplete.entity_type) {
            console.log(
              'WARNING - _theme_autocomplete - no entity_type provided'
            );
            return;
          }
          var function_name = autocomplete.entity_type + '_index';
          var fn = window[function_name];
          var query = null;
          if (autocomplete.query) { query = autocomplete.query; }
          else {
            query = {
              parameters: { },
              parameters_op: { }
            };
            var fields = [
              entity_primary_key(autocomplete.entity_type),
              entity_primary_key_title(autocomplete.entity_type)
            ];
            if (autocomplete.entity_type == 'taxonomy_term') {
              if (autocomplete.vid) {
                query.parameters['vid'] = autocomplete.vid;
              }
            }
            query.fields = fields;
            query.parameters[autocomplete.filter] = '%' + value + '%';
            query.parameters_op[autocomplete.filter] = 'like';
          }
          fn.apply(null, [query, {
              success: function(results) {
                var fn = _theme_autocomplete_success_handlers[autocomplete_id];
                fn(autocomplete_id, results, false, null);
              }
          }]);
          break;

        // The default handler...
        default:

          // If we made it this far, and don't have a handler, then warn the
          // developer.
          if (!handler) {
            console.log('WARNING - _theme_autocomplete - no handler provided');
            return;
          }

          break;

      }

    }
    else {
      // The autocomplete text field was emptied, clear out the hidden value.
      $('#' + autocomplete.id).val('');
    }
  }
  catch (error) { console.log('_theme_autocomplete - ' + error); }
}

/**
 * An internal function used to prepare the items for an autocomplete list.
 * @param {Object} variables
 * @return {*}
 */
function _theme_autocomplete_prepare_items(variables) {
  try {
    // Make sure we have an items array.
    var items = [];
    if (variables.items) { items = variables.items; }

    // Prepare the items, and return them.
    var _items = [];
    if (items.length > 0) {
      for (var index in items) {
          if (!items.hasOwnProperty(index)) { continue; }
          var item = items[index];
          var value = '';
          var label = '';
          if (typeof item === 'string') {
            value = item;
            label = item;
          }
          else {
            value = item.value;
            label = item.label;
          }
          var options = {
            attributes: {
              value: value,
              onclick: '_theme_autocomplete_click(\'' +
                variables.attributes.id +
              '\', this, \'' + variables.autocomplete_id + '\')'
            }
          };
          var _item = l(label, null, options);
          _items.push(_item);
      }
    }
    return _items;
  }
  catch (error) { console.log('_theme_autocomplete_prepare_items - ' + error); }
}

/**
 * An internal function used to handle clicks on items in autocomplete results.
 * @param {String} id The id of the hidden input that holds the value.
 * @param {Object} item The list item anchor that was just clicked.
 * @param {String} autocomplete_id
 */
function _theme_autocomplete_click(id, item, autocomplete_id) {
  try {
    // Set the hidden input with the value, and the text field with the text.
    var list_id = id + '-list';
    $('#' + id).val($(item).attr('value'));
    $(_theme_autocomplete_input_selector[autocomplete_id]).val($(item).html());
    if (_theme_autocomplete_remote[autocomplete_id]) {
      $('#' + list_id).html('');
    }
    else {
      $('#' + list_id + ' li').addClass('ui-screen-hidden');
      $('#' + list_id).listview('refresh');
    }
    // Now fire the item onclick handler, if one was provided.
    if (
      _theme_autocomplete_variables[autocomplete_id].item_onclick &&
      drupalgap_function_exists(
        _theme_autocomplete_variables[autocomplete_id].item_onclick
      )
    ) {
      var fn =
        window[_theme_autocomplete_variables[autocomplete_id].item_onclick];
      fn(id, $(item));
    }
  }
  catch (error) { console.log('_theme_autocomplete_click - ' + error); }
}

/**
 * Used to set a default value in an autocomplete's text field.
 * @param {Object} options
 */
function _theme_autocomplete_set_default_value_label(options) {
  try {
    setTimeout(function() {
        $(options.selector).val(options.default_value_label).trigger('create');
    }, 250);
  }
  catch (error) {
    console.log('_theme_autocomplete_set_default_value_label - ' + error);
  }
}


/**
 * Given a block delta, this will return the corresponding
 * block from drupalgap.blocks.
 * @param {String} delta
 * @return {Object}
 */
function drupalgap_block_load(delta) {
  try {
    var block = null;
    if (drupalgap.blocks) {
      for (var index in drupalgap.blocks) {
          if (!drupalgap.blocks.hasOwnProperty(index)) { continue; }
          var object = drupalgap.blocks[index];
          if (object[delta]) {
            block = object[delta];
            break;
          }
      }
    }
    if (block == null) {
      var msg = 'drupalgap_block_load - ' + t('failed to load') + ' "' + delta +
        '" ' + t('block!');
      drupalgap_alert(msg);
    }
    return block;
  }
  catch (error) { console.log('drupalgap_block_load - ' + error); }
}

/**
 * Renders the html string for a block.
 * @param {Object} region
 * @param {String} current_path
 * @param {String} block_delta
 * @param {Object} block_settings
 * @param {Object} block_counts
 * @return {String}
 */
function drupalgap_block_render(region, current_path, block_delta,
  block_settings, block_counts) {
  try {
    var html = '';
    // Check the block's visibility settings. If an access_callback
    // function is specified on the block's settings, we'll call that
    // to determine the visibility, otherwise we'll fall back to the
    // default visibility determination mechanism.
    var render_block = false;
    if (
      block_settings.access_callback &&
      drupalgap_function_exists(block_settings.access_callback)
    ) {
      var fn = window[block_settings.access_callback];
      render_block = fn({
          path: current_path,
          delta: block_delta,
          region: region.name,
          theme: drupalgap.settings.theme,
          settings: block_settings
      });
    }
    else if (drupalgap_check_visibility('block', block_settings)) {
      render_block = true;
      // The 'offline' and 'error' pages only have the 'main' system
      // block visible.
      if (block_delta != 'main' && (
        current_path == 'offline' || current_path == 'error')
      ) { render_block = false; }
    }
    if (render_block) {
      var block = drupalgap_block_load(block_delta);
      if (block_counts) { block_counts.block_count++; }
      if (menu_load(block_delta) && block_counts) {
        block_counts.block_menu_count++;
      }
      if (block) {
        html = module_invoke(
          block.module,
          'block_view',
          block_delta,
          region
        );
      }
    }
    return html;
  }
  catch (error) { console.log('drupalgap_block_render - ' + error); }
}


/**
 * Converts a JSON object to an XML/HTML tag attribute string and returns the
 * string.
 * @param {Object} attributes
 * @return {String{
 */
function drupalgap_attributes(attributes) {
  try {
    var attribute_string = '';
    if (attributes) {
      for (var name in attributes) {
          if (!attributes.hasOwnProperty(name)) { continue; }
          var value = attributes[name];
          if (value != '') {
            // @todo - if someone passes in a value with double quotes, this
            // will break. e.g.
            // 'onclick':'_drupalgap_form_submit("' + form.id + "');'
            // will break, but
            // 'onclick':'_drupalgap_form_submit(\'' + form.id + '\');'
            // will work.
            attribute_string += name + '="' + value + '" ';
          }
          else {
            // The value was empty, just place the attribute name on the
            // element.
            attribute_string += name + ' ';
          }
      }
    }
    return attribute_string;
  }
  catch (error) { console.log('drupalgap_attributes - ' + error); }
}

/**
 * Used by drupalgap_render_region to check the visibility settings on region
 * links and blocks. Just like Drupal Blocks, this function checks the
 * visibility rules specified by role or pages specified in data. Returns true
 * by default, otherwise it will return true or false depending on the first
 * visibility setting present in data.
 * @param {String} type
 * @param {Object} data
 * @return {Boolean}
 */
function drupalgap_check_visibility(type, data) {
  try {
    var visible = true;
    if (typeof data === 'undefined') {
      console.log(
        'drupalgap_check_visibility - WARNING - no data provided for type (' +
        type + ')'
      );
    }
    // Roles.
    else if (typeof data.roles !== 'undefined' &&
      data.roles && data.roles.value && data.roles.value.length != 0) {
      for (var role_index in data.roles.value) {
          if (!data.roles.value.hasOwnProperty(role_index)) { continue; }
          var role = data.roles.value[role_index];
          if (drupalgap_user_has_role(role)) {
            // User has role, show/hide the block accordingly.
            if (data.roles.mode == 'include') { visible = true; }
            if (data.roles.mode == 'exclude') { visible = false; }
          }
          else {
            // User does not have role, show/hide the block accordingly.
            if (data.roles.mode == 'include') { visible = false; }
            if (data.roles.mode == 'exclude') { visible = true; }
          }
          // Break out of the loop if already determined to be visible.
          if (visible) { break; }
      }
    }
    // Pages.
    else if (typeof data.pages !== 'undefined' && data.pages &&
      data.pages.value && data.pages.value.length != 0) {
      var current_path = drupalgap_path_get();
      var current_path_parts = current_path.split('/');
      for (var page_index in data.pages.value) {
          if (!data.pages.value.hasOwnProperty(page_index)) { continue; }
          var path = data.pages.value[page_index];
          if (path == '') { path = drupalgap.settings.front; }
          if (path == current_path) {
            if (data.pages.mode == 'include') { visible = true; }
            else if (data.pages.mode == 'exclude') { visible = false; }
            break;
          }
          else {
            // It wasn't a direct path match, is there a wildcard that matches
            // the router path?
            if (path.indexOf('*') != -1) {
              var router_path =
                drupalgap_get_menu_link_router_path(current_path);
              if (router_path.replace(/%/g, '*') == path) {
                if (data.pages.mode == 'include') { visible = true; }
                else if (data.pages.mode == 'exclude') { visible = false; }
                break;
              }
              else {
                var path_parts = path.split('/');
                var match = true;
                if (path_parts.length == 0) { match = false; }
                else if (path_parts.length == current_path_parts.length) {
                  for (var i = 0; i < path_parts.length; i++) {
                    if (path_parts[i] != current_path_parts[i]) {
                      match = false;
                      break;
                    }
                  }
                }
                if (match) {
                  if (data.pages.mode == 'include') { visible = false; }
                  else if (data.pages.mode == 'exclude') { visible = true; }
                }
              }
            }
            else {
              // There's no wildcard in the rule, and it wasn't a direct path
              // match.
              if (data.pages.mode == 'include') { visible = false; }
              else if (data.pages.mode == 'exclude') { visible = true; }
            }
          }
      }
    }
    return visible;
  }
  catch (error) { console.log('drupalgap_check_visibility - ' + error); }
}

/**
 * @deprecated
 * @ see entity_get_bundle()
 */
function drupalgap_get_bundle(entity_type, entity) {
  try {
    var msg = 'WARNING - drupalgap_get_bundle() is deprecated, use ' +
      'entity_get_bundle() instead!';
    console.log(msg);
    return entity_get_bundle(entity_type, entity);
  }
  catch (error) { console.log('drupalgap_get_bundle - ' + error); }
}

/**
 * Returns the path to a system item (module, theme, etc.), returns false if it
 * can't find it.
 * @param {String} type
 * @param {String} name
 * @return {*}
 */
function drupalgap_get_path(type, name) {
  try {
    var path = null;
    if (type == 'module') {
      var found_module = false;
      for (var bundle in Drupal.modules) {
          if (!Drupal.modules.hasOwnProperty(bundle)) { continue; }
          var modules = Drupal.modules[bundle];
          if (found_module) { break; }
          else {
            for (var index in modules) {
                if (!modules.hasOwnProperty(index)) { continue; }
                var module = modules[index];
                if (module.name == name) {
                  found_module = true;
                  path = '';
                  if (bundle == 'core') { path += 'modules'; }
                  else if (bundle == 'contrib') { path += 'app/modules'; }
                  else if (bundle == 'custom') { path += 'app/modules/custom'; }
                  else {
                    var msg = 'drupalgap_get_path - unknown module bundle (' +
                      bundle +
                    ')';
                    drupalgap_alert(msg);
                    break;
                  }
                  path += '/' + name;
                  break;
                }
            }
          }
      }
    }
    else if (type == 'theme') {
      if (name == 'easystreet3' || name == 'ava') { path = 'themes/' + name; }
      else { path = 'app/themes/' + name; }
    }
    else {
      console.log(
        'WARNING: drupalgap_get_path - unsupported type (' + type + ')'
      );
    }
    return path;
  }
  catch (error) { console.log('drupalgap_get_path - ' + error); }
}

/**
 * Given an error message, this will log the message to the console and goto
 * the error page, if it isn't there already. If Drupal.settings.debug is set
 * to true, this function will also alert the error. You may optionally send in
 * a second message that will be displayed to the user via an alert dialog box.
 * @param {String} message
 */
function drupalgap_error(message) {
  try {
    // Generate a developer error message, log it to the console, then alert
    // the message if debugging is enabled.
    var error_message = 'drupalgap_error() - ' +
                        arguments.callee.caller.name + ' - ' +
                        message;
    dpm(error_message);
    if (Drupal.settings.debug) { drupalgap_alert(error_message); }
    // If a message for the user was passed in, display it to the user.
    if (arguments[1]) { drupalgap_alert(arguments[1]); }
    // Goto the error page if we are not already there.
    if (drupalgap_path_get() != 'error') { drupalgap_goto('error'); }
  }
  catch (error) { console.log('drupalgap_error - ' + error); }
}

/**
 * Given a link JSON object, this will return its attribute class value, or null
 * if it isn't set.
 * @param {Object} link
 * @return {String}
 */
function drupalgap_link_get_class(link) {
  try {
    var css_class = null;
    if (
      link.options && link.options.attributes &&
      link.options.attributes['class'] &&
      !empty(link.options.attributes['class'])
    ) { css_class = link.options.attributes['class']; }
    return css_class;
  }
  catch (error) { console.log('drupalgap_link_get_class - ' + error); }
}

/**
 * Get the current DrupalGap path.
 * @return {String}
 */
function drupalgap_path_get() {
  try {
    var path = drupalgap.path;
    return path;
  }
  catch (error) { console.log('drupalgap_path_get - ' + error); }
}

/**
 * Set the current DrupalGap path.
 * @param {String} path
 */
function drupalgap_path_set(path) {
  try { drupalgap.path = path; }
  catch (error) { console.log('drupalgap_path_set - ' + error); }
}

/**
 * Get the current DrupalGap router_path.
 * @return {String}
 */
function drupalgap_router_path_get() {
  try {
    var router_path = drupalgap.router_path;
    return router_path;
  }
  catch (error) { console.log('drupalgap_router_path_get - ' + error); }
}

/**
 * Set the current DrupalGap router_path.
 * @param {String} router_path
 */
function drupalgap_router_path_set(router_path) {
  try { drupalgap.router_path = router_path; }
  catch (error) { console.log('drupalgap_router_path_set - ' + error); }
}

/**
 * Implementation of arg(index = null, path = null).
 * @return {*}
 */
function arg() {
  try {
    var result = null;
    // If there were zero or one arguments provided.
    if (arguments.length == 0 || arguments.length == 1) {
      // Split the path into parts.
      var drupalgap_path = drupalgap_path_get();
      var args = drupalgap_path.split('/');
      // If no arguments were provided just return the split array, otherwise
      // return whichever argument was requested.
      if (arguments.length == 0) { result = args; }
      else if (args[arguments[0]]) { result = args[arguments[0]]; }
    }
    else {
      // A path was provided, split it into parts, then return the split array
      // if they didn't request a specific index, otherwise return the value of
      // the specific index inside the split array.
      var path = arguments[1];
      var args = path.split('/');
      if (arguments[0] && args[arguments[0]]) { result = args[arguments[0]]; }
      else { result = args; }
    }
    return result;
  }
  catch (error) { console.log('arg - ' + error); }
}

/**
 * Returns a link.
 * @return {String}
 */
function l() {
  try {
    // Grab the text and the path from the arguments and then build a simple
    // link object.
    var text = arguments[0];
    var path = arguments[1];
    var link = {'text': text, 'path': path};
    // Determine if there are any incoming link options, if there are, attach
    // them to the link object. If there are any attributes, extract them from
    // the options and attach them directly to the link object.
    var options = null;
    if (arguments[2]) {
      options = arguments[2];
      if (options.attributes) { link.attributes = options.attributes; }
      link.options = options;
    }
    return theme('link', link);
  }
  catch (error) { console.log('l - ' + error); }
}

/**
 * Returns a button link.
 * @return {String}
 */
function bl() {
  try {
    // Grab the text and the path.
    var text = arguments[0];
    var path = arguments[1];
    // Build the default options and attributes, if necessary.
    var options = null;
    if (arguments[2]) {
      options = arguments[2];
    }
    else { options = {}; }
    if (!options.attributes) { options.attributes = { }; }
    options.attributes['data-role'] = 'button';
    return l(text, path, options);
  }
  catch (error) { console.log('bl - ' + error); }
}

/**
 * Returns translated text.
 * @param {String} str The string to translate
 * @return {String}
 */
function t(str) {
  var lang = arguments[3] ? arguments[3] : Drupal.settings.language_default;
  if (
    lang != 'und' &&
    typeof drupalgap.locale[lang] !== 'undefined' &&
    drupalgap.locale[lang][str]
  ) { return drupalgap.locale[lang][str]; }
  return str;
}


/**
 * Given a form element, this will return true if access to the element is
 * permitted, false otherwise.
 * @param {Object} element
 * @return {Boolean}
 */
function drupalgap_form_element_access(element) {
  try {
    var access = true;
    if (element.access == false) { access = false; }
    return access;
  }
  catch (error) { console.log('drupalgap_form_element_access - ' + error); }
}

/**
 * Given a form element type, this will return the name of the module that
 * implements the hook_field_widget_form() for the element. Keep in mind for now
 * some of the module names don't exist, and are actually implemented inside
 * the field module. If no module is found, it returns false.
 * @param {String} type
 * @return {String}
 */
function drupalgap_form_element_get_module_name(type) {
  try {
    var module = false;
    switch (type) {
      case 'checkbox':
      case 'radios':
      case 'select':
        module = 'options';
        break;
      case 'image':
        module = 'image';
        break;
    }
    return module;
  }
  catch (error) {
    console.log('drupalgap_form_element_get_module_name - ' + error);
  }
}

/**
 * Given a form element name and the form_id, this generates an html id
 * attribute value to be used in the DOM. An optional third argument is a
 * string language code to use. An optional fourth argument is an integer delta
 * value to use on field elements.
 * @param {String} name
 * @param {String} form_id
 * @return {String}
 */
function drupalgap_form_get_element_id(name, form_id) {
  try {
    if (name == null || name == '') { return ''; }
    var id =
      'edit-' +
      form_id.toLowerCase().replace(/_/g, '-') + '-' +
      name.toLowerCase().replace(/_/g, '-');
    // Any language code to append to the id?
    if (arguments[2]) { id += '-' + arguments[2]; }
    // Any delta value to append to the id?
    if (typeof arguments[3] !== 'undefined') {
      id += '-' + arguments[3] + '-value';
    }
    return id;
  }
  catch (error) { console.log('drupalgap_form_get_element_id - ' + error); }
}

/**
 * Given an element name, this will return the class name to use on the
 * element's container.
 * @param {String} name
 * @return {String}
 */
function drupalgap_form_get_element_container_class(name) {
  try {
    return 'form-item field-name-' + name.replace(/_/g, '-');
  }
  catch (error) {
    console.log('drupalgap_form_get_element_container_class - ' + error);
  }
}

/**
 * Renders all the input elements in a form.
 * @param {Object} form
 * @return {String}
 */
function _drupalgap_form_render_elements(form) {
  try {
    var content = '';
    var content_sorted = '';
    var content_weighted = [];
    // For each form element, if the element objects name property isn't set,
    // set it, then render the element if access is permitted. While rendering
    // the elements, set them aside according to their widget weight so they
    // can be appended to the content string in the correct order later.
    for (var name in form.elements) {
        if (!form.elements.hasOwnProperty(name)) { continue; }
        var element = form.elements[name];
        if (!element.name) { element.name = name; }
        if (drupalgap_form_element_access(element)) {
          if (
            element.is_field &&
            typeof element.field_info_instance.widget.weight !== 'undefined'
          ) {
            var weight = element.field_info_instance.widget.weight;
            while (typeof content_weighted[weight] !== 'undefined') { weight += .1; }
            content_weighted['' + weight] = _drupalgap_form_render_element(form, element);
          }
          else {
            // Extract the bundle. Note, on comments the bundle is prefixed with
            // 'comment_node_' so we need to remove that to correctly map to the
            // potential extra fields data.
            var bundle = null;
            if (form.bundle) {
              bundle = form.bundle;
              if (
                form.entity_type == 'comment' &&
                form.bundle.indexOf('comment_node_') != -1
              ) { bundle = form.bundle.replace('comment_node_', ''); }
            }

            // This is not a field, if it has it's own weight use it, or see if
            // there is a weight in field_info_extra_fields, otherwise just
            // append it to the element content.

            // Elements with a weight defined.
            if (typeof element.weight !== 'undefined') {
              if (content_weighted[element.weight]) {
                var msg = 'WARNING: _drupalgap_form_render_elements - the ' +
                'weight of ' + element.weight + ' for ' + element.name +
                ' is already in use by ' +
                content_weighted[element.weight].name;
                console.log(msg);
                // Just render it.
                var _content = _drupalgap_form_render_element(form, element);
                if (typeof _content !== 'undefined') { content += _content; }
              }
              else {
                content_weighted[element.weight] =
                  _drupalgap_form_render_element(form, element);
              }
            }

            // Extra fields.
            else if (
              form.entity_type && bundle &&
              typeof drupalgap.field_info_extra_fields[bundle][name] !==
                'undefined' &&
              typeof
                drupalgap.field_info_extra_fields[bundle][name].weight !==
                'undefined'
            ) {
              var weight =
                drupalgap.field_info_extra_fields[bundle][name].weight;
              if (content_weighted[weight]) {
                var msg = 'WARNING: _drupalgap_form_render_elements - the ' + 
                'weight of ' + weight + ' for ' + element.name + ' is ' +
                'already in use by ' + content_weighted[weight].name;
                console.log(msg);
                // Just render it.
                var _content = _drupalgap_form_render_element(form, element);
                if (typeof _content !== 'undefined') { content += _content; }
              }
              else {
                content_weighted[weight] =
                  _drupalgap_form_render_element(form, element);
              }
            }

            // No weight, just render it.
            else {
              var _content = _drupalgap_form_render_element(form, element);
              if (typeof _content !== 'undefined') { content += _content; }
            }

          }
        }
    }
    // Prepend the weighted elements to the content.
    if (!empty(content_weighted)) {
      for (var weight in content_weighted) {
        content_sorted += content_weighted[weight] + '\n';
      }
      // Attach sorted content.
      content = content_sorted + '\n' + content;
    }
    // Add any form buttons to the form elements html, if access to the button
    // is permitted.
    if (form.buttons && form.buttons.length != 0) {
      for (var name in form.buttons) {
          if (!form.buttons.hasOwnProperty(name)) { continue; }
          var button = form.buttons[name];
          if (drupalgap_form_element_access(button)) {
            var attributes = {
              type: 'button',
              id: drupalgap_form_get_element_id(name, form.id)
            };
            if (button.attributes) { $.extend(attributes, button.attributes); }
            content += '<button ' + drupalgap_attributes(attributes) + '">' +
              button.title +
            '</button>';
          }
      }
    }
    return content;
  }
  catch (error) { console.log('_drupalgap_form_render_elements - ' + error); }
}

/**
 * Renders an input element for a form.
 * @param {Object} form
 * @param {Object} element
 * @return {String}
 */
function _drupalgap_form_render_element(form, element) {
  try {
    var html = '';

    if (!element) { return html; }

    // Extract the element name.
    var name = element.name;

    // Grab the language.
    var language = language_default();

    // We'll assume the element has no items (e.g. title, nid, vid, etc), unless
    // we determine later that this element is a field, then it'll have items.
    var items = false;

    // If this element is a field, extract the items from the language code and
    // determine what module and hook will handle the items. If the element is
    // not a field, just flatten it into a single item collection and determine
    // which module handles this element type. Keep in mind not all the modules
    // actually exist, and we've placed implementations into the field module.
    var module = false;
    var field_widget_form_function_name = false;
    var field_widget_form_function = false;
    if (element.is_field) {
      items = element[language];
      module = element.field_info_instance.widget.module;
    }
    else {
      items = {0: element};
      module = drupalgap_form_element_get_module_name(element.type);
    }
    if (module) {
      field_widget_form_function_name = module + '_field_widget_form';

      if (drupalgap_function_exists(field_widget_form_function_name)) {
        field_widget_form_function = window[field_widget_form_function_name];
      }
      else {
        console.log(
          'WARNING: _drupalgap_form_render_element() - ' +
          field_widget_form_function_name +
          '() does not exist for the "' + element.type + '" form element!'
        );
      }
    }

    // If there were no items, just return.
    if (!items || items.length == 0) { return html; }

    // Generate default variables.
    var variables = {
      attributes: {}
    };

    // Grab the info instance and info field for the field, then attach them
    // both to the variables object so all theme functions will have access
    // to that data.
    variables.field_info_field = element.field_info_field;
    variables.field_info_instance = element.field_info_instance;

    // Render the element item(s). Remember the final delta value for later.
    var delta = 0;
    var item_html = '';
    var item_label = '';
    var render_item = null;
    for (var delta in items) {
        if (!items.hasOwnProperty(delta)) { continue; }
        var item = items[delta];

        // We'll render the item, unless we prove otherwise.
        render_item = true;

        // Overwrite the variable's attributes id with the item's id.
        variables.attributes.id = item.id;

        // Attach the item as the element onto variables.
        variables.element = item;

        // Create an array for the item's children if it doesn't exist already.
        // This is used by field widget forms to extend form elements.
        if (!items[delta].children) { items[delta].children = []; }

        // Generate the label for field items on delta zero only. Keep in mind
        // rendered labels, with an element title_placeholder set to true,
        // will not be appended to the result html later.
        if (element.is_field && delta == 0) {
          item.title = element.title;
          item_label = theme('form_element_label', {'element': item});
        }

        // If the element's title is set to be a placeholder, set the
        // placeholder attribute equal to the title on the current item, unless
        // someone already set it. If it is a required element, mark it as such.
        if (
          delta == 0 && typeof element.title_placeholder !== 'undefined' &&
          element.title_placeholder &&
          typeof variables.attributes['placeholder'] === 'undefined'
        ) {
          var placeholder = element.title;
          // @TODO show a better required marker for placeholders.
          /*if (element.required) {
            placeholder += ' ' + theme('form_required_marker', { });
          }*/
          variables.attributes['placeholder'] = placeholder;
        }

        // If there wasn't a default value provided, set one. Then set the default value into the variables' attributes,
        // if it wasn't already set, otherwise set it to the item's value.
        if (!item.default_value) { item.default_value = ''; }
        variables.attributes.value = item.default_value;
        if (
            typeof item.value !== 'undefined' &&
            (typeof variables.attributes.value === 'undefined' || empty(variables.attributes.value))
        ) { variables.attributes.value = item.value; }

        // Call the hook_field_widget_form() if necessary. Merge any changes
        // to the item back into this item.
        if (field_widget_form_function) {
          field_widget_form_function.apply(
            null, [
              form,
              null,
              element.field_info_field,
              element.field_info_instance,
              language,
              items,
              delta,
              element
          ]);
          item = $.extend(true, item, items[delta]);
          // If the item type got lost, replace it.
          if (!item.type && element.type) { item.type = element.type; }
        }

        // Merge element attributes into the variables object.
        if (item.options && item.options.attributes) {
          variables.attributes = $.extend(
            true,
            variables.attributes,
            item.options.attributes
          );
        }

        // Render the element item, unless it wasn't supported. Before rendering, clear out any default values so they
        // aren't stale for the next delta item.
        item_html += _drupalgap_form_render_element_item(
          form,
          element,
          variables,
          item
        );
        if (typeof variables.default_value !== 'undefined') { delete variables.default_value; }
        if (typeof variables.default_value_label !== 'undefined') { delete variables.default_value_label; }
        if (typeof variables.value !== 'undefined') { delete variables.value; }
        if (typeof item_html === 'undefined') {
          render_item = false;
          break;
        }
    }

    // Are we skipping the render of the item?
    if (!render_item) { return ''; }

    // Show the 'Add another item' button on unlimited value fields.
    /*if (element.field_info_field &&
      element.field_info_field.cardinality == -1) {
      var add_another_item_variables = {
        text: 'Add another item',
        attributes: {
          'class': 'drupalgap_form_add_another_item',
          onclick:
            "javascript:_drupalgap_form_add_another_item('" +
              form.id + "', '" +
              element.name + "', " +
              delta +
            ')'
        }
      };
      html += theme('button', add_another_item_variables);
    }*/

    // Is this element wrapped? We won't wrap hidden inputs by default, unless
    // someone is overriding it.
    var wrapped = true;
    if (typeof element.wrapped !== 'undefined' && !element.wrapped) {
      wrapped = false;
    }
    if (element.type == 'hidden') {
      wrapped = false;
      if (element.wrapped) { wrapped = true; }
    }

    // If there is an element prefix, place it in the html.
    if (element.prefix) { html += element.prefix; }

    // Open the element container.
    var container_attributes = {
      'class': drupalgap_form_get_element_container_class(name)
    };
    if (wrapped) {
      html += '<div ' + drupalgap_attributes(container_attributes) + '>';
    }

    // Add a label to the element, except submit and hidden elements. Any field
    // labels have already been rendered, other element labels must be manually
    // rendered here. Don't attach the label if the element's title_placeholder
    // is set to true.
    if (element.type != 'submit' && element.type != 'hidden') {
      if (
        typeof element.title_placeholder !== 'undefined' &&
        element.title_placeholder
      ) { /* Skip label for placeholders. */ }
      else {
        if (element.is_field) { html += item_label; }
        else {
          html += theme('form_element_label', {'element': element});
        }
      }
    }

    // Add the item html if it isn't empty.
    if (item_html != '') { html += item_html; }

    // Add element description.
    if (element.description && element.type != 'hidden') {
      html += '<div class="description">' + t(element.description) + '</div>';
    }

    // Close the element container.
    if (wrapped) { html += '</div>'; }

    // If there is an element suffix, place it in the html.
    if (element.suffix) { html += element.suffix; }

    // Return the element html.
    return html;

  }
  catch (error) { console.log('_drupalgap_form_render_element - ' + error); }
}

/**
 * Given a form, an element, the variables for a theme function, and the element
 * item, this will return the html rendering of the element item.
 * @param {Object} form
 * @param {Object} element
 * @param {Object} variables
 * @param {Object} item
 * @return {*}
 */
function _drupalgap_form_render_element_item(form, element, variables, item) {
  try {
    var html = '';
    // Depending on the element type, if necessary, adjust the variables and/or
    // theme function to be used, then render the element by calling its theme
    // function.
    // @TODO - this block of code should be moved into their respective
    // implementations of hook_field_widget_form().
    switch (item.type) {
      case 'text':
        item.type = 'textfield';
        break;
      case 'list_text':
      case 'list_float':
      case 'list_integer':
        item.type = 'select';
        break;
    }

    // Set the theme function.
    var theme_function = item.type;

    // If the element is disabled, add the 'disabled' attribute.
    if (element.disabled) { variables.attributes.disabled = ''; }

    // Make any preprocess modifications to the elements so they will map
    // cleanly to their theme function.
    // @todo A hook_field_widget_form() should be used instead here.
    if (item.type == 'submit') {
      variables.attributes.onclick =
        '_drupalgap_form_submit(\'' + form.id + '\');';
      if (!variables.attributes['data-theme']) {
        variables.attributes['data-theme'] = 'b';
      }
      if (typeof variables.attributes.type === 'undefined') {
        variables.attributes.type = 'button';
      }
      if (typeof variables.attributes['class'] === 'undefined') {
        variables.attributes['class'] = '';
      }
      variables.attributes['class'] += ' dg_form_submit_button ';
    }

    // Merge the item into variables.
    $.extend(true, variables, item);

    // If a value isn't set on variables, try to set it with the default value
    // on the item.
    if (typeof variables.value === 'undefined' || variables.value == null) {
      if (typeof item.default_value !== 'undefined') {
        variables.value = item.default_value;
      }
    }

    // Run the item through the theme system if a theme function exists, or try
    // to use the item markup, or let the user know the field isn't supported.
    if (drupalgap_function_exists('theme_' + theme_function)) {
      html += theme(theme_function, variables);
    }
    else {
      if (item.markup || item.markup == '') { html += item.markup; }
      else {
        // @todo - the reason for this warning sometimes happens because the
        // item.type is lost with $.extend in _drupalgap_form_render_element().
        // @update - if an item doesn't have a type, it gets set by the parent
        // element, so we should now always have a type available here.
        var msg = 'Field ' + item.type + ' not supported.';
        console.log('WARNING: _drupalgap_form_render_element_item() - ' + msg);
        dpm(item);
        return null;
      }
    }

    // Render any item children. If the child has markup, just use the html,
    // otherwise run the child through theme().
    if (item.children && item.children.length > 0) {
      for (var i = 0; i < item.children.length; i++) {
        if (item.children[i].markup) { html += item.children[i].markup; }
        else if (item.children[i].type || item.children[i].theme) {
          var theme_type = item.children[i].type;
          if (!theme_type) { theme_type = item.children[i].theme; }
          // Is there a title for a label?
          if (item.children[i].title) {
            html += theme('form_element_label', {
                element: item.children[i]
            });
          }
          // Render the child with the theme system.
          if (item.children[i].prefix) { html += item.children[i].prefix; }
          html += theme(theme_type, item.children[i]);
          if (item.children[i].suffix) { html += item.children[i].suffix; }
        }
        else {
          console.log(
            'WARNING: _drupalgap_form_render_element_item - ' +
            'failed to render child ' + i + ' for ' + element.name
          );
        }
      }
    }

    return html;
  }
  catch (error) {
    console.log('_drupalgap_form_render_element_item - ' + error);
  }
}

/**
 * Given an element name, the form, a language code and a delta value, this
 * will return default values that can be used to place an item element into a
 * Forms API object.
 * @param {String} name
 * @param {Object} form
 * @param {String} language
 * @param {Number} delta
 * @return {Object}
 */
function drupalgap_form_element_item_create(name, form, language, delta) {
  try {
    // Generate the id for this element field item and set it and
    // some default options onto the element item.
    var id = drupalgap_form_get_element_id(name, form.id, language, delta);
    return {
      id: id,
      options: {
        attributes: {
          id: id
        }
      },
      required: form.elements[name].required
    };
  }
  catch (error) {
    console.log('drupalgap_form_element_item_create - ' + error);
  }
}

/**
 *
 * @param {Object} form
 * @param {Object} form_state
 * @param {Object} element
 * @param {String} language
 * @param {Number} delta
 * @return {Array}
 */
function _drupalgap_form_element_items_widget_arguments(form, form_state,
  element, language, delta) {
  try {
    var widget_arguments = [];
    widget_arguments.push(form); // form
    widget_arguments.push(form_state); // form state
    widget_arguments.push(element.field_info_field); // field
    widget_arguments.push(element.field_info_instance); // instance
    widget_arguments.push(language); // language
    widget_arguments.push(form.elements[element.name][language]); // items
    widget_arguments.push(delta); // delta
    widget_arguments.push(element); // element
    return widget_arguments;
  }
  catch (error) {
    console.log('_drupalgap_form_element_items_widget_arguments - ' + error);
  }
}


/**
 * Internal function used to dynamically add another element item to a form for
 * unlimited value fields.
 * @param {String} form_id
 * @param {String} name
 * @param {Number} delta
 */
function _drupalgap_form_add_another_item(form_id, name, delta) {
  try {
    // Locate the last item, load the form, extract the element from
    // the form, generate default variables for the new item, determine the next
    // delta value.
    var selector = '.' + drupalgap_form_get_element_container_class(name) +
      ' .drupalgap_form_add_another_item';
    var add_another_item_button = $(selector);
    var form = drupalgap_form_local_storage_load(form_id);
    var language = language_default();
    var item = drupalgap_form_element_item_create(
      name,
      form,
      language,
      delta + 1
    );
    form.elements[name][language][delta + 1] = item;
    var element = form.elements[name];
    var variables = {
      attributes: {},
      field_info_field: element.field_info_field,
      field_info_instance: element.field_info_instance
    };
    var field_widget_form_function =
      element.field_info_instance.widget.module + '_field_widget_form';
    window[field_widget_form_function].apply(
      null,
      _drupalgap_form_element_items_widget_arguments(
        form,
        null,
        element,
        language,
        delta + 1
      )
    );
    drupalgap_form_local_storage_save(form);
    $(add_another_item_button).before(
      _drupalgap_form_render_element_item(form, element, variables, item)
    );
  }
  catch (error) { console.log('_drupalgap_form_add_another_item - ' + error); }
}

/**
 * Returns a 'Cancel' button object that can be used on most forms.
 * @return {Object}
 */
function drupalgap_form_cancel_button() {
    return {
      'title': t('Cancel'),
      attributes: {
        onclick: 'javascript:drupalgap_back();'
      }
    };
}

/**
 * Given a jQuery selector to a form, this will clear all the elements on
 * the UI.
 * @see http://stackoverflow.com/a/6364313/763010
 */
function drupalgap_form_clear(form_selector) {
  try {
    $(':input', form_selector)
     .not(':button, :submit, :reset, :hidden')
     .val('')
     .removeAttr('checked')
     .removeAttr('selected');
  }
  catch (error) { console.log('drupalgap_form_clear - ' + error); }
}

/**
 * Given a form id, this will assemble and return the default form JSON object.
 * @param {String} form_id
 * @return {Object}
 */
function drupalgap_form_defaults(form_id) {
  try {
    var form = {};
    // Set the form id, elements, buttons, options and attributes.
    form.id = form_id;
    form.elements = {};
    form.buttons = {};
    form.options = {
      attributes: {
        'class': ''
      }
    };
    // Create a prefix and suffix.
    form.prefix = '';
    form.suffix = '';
    // Create empty arrays for the form's validation and submission handlers,
    // then add the default call back functions to their respective array, if
    // they exist.
    form.validate = [];
    form.submit = [];
    var validate_function_name = form.id + '_validate';
    if (drupalgap_function_exists(validate_function_name)) {
      form.validate.push(validate_function_name);
    }
    var submit_function_name = form.id + '_submit';
    if (drupalgap_function_exists(submit_function_name)) {
      form.submit.push(submit_function_name);
    }
    // Finally, return the form.
    return form;
  }
  catch (error) { console.log('drupalgap_form_defaults - ' + error); }
}

/**
 * Given a drupalgap form, this renders the form html and returns it.
 * @param {Object} form
 * @return {String}
 */
function drupalgap_form_render(form) {
  try {
    // @TODO - we may possibly colliding html element ids!!! For example, I
    // think the node edit page gets an id of "node_edit" and possibly so does
    // the node edit form, which also may get an id of "node_edit". We may want
    // to prefix both the template page and form ids with prefixes, e.g.
    // drupalgap_page_* and drupalgap_form_*, but adding these prefixes could
    // get annoying for css selectors used in jQuery and CSS. What to do?

    // If no form id is provided, warn the user.
    if (!form.id) {
      return '<p>drupalgap_form_render() - missing form id!</p>' +
        JSON.stringify(form);
    }
    // If the form already exists in the DOM, remove it.
    if ($('form#' + form.id).length) { $('form#' + form.id).remove(); }
    // Render the prefix and suffix and wrap them in their own div.
    var prefix = form.prefix;
    if (!empty(prefix)) {
      prefix = '<div class="form_prefix">' + prefix + '</div>';
    }
    var suffix = form.suffix;
    if (!empty(suffix)) {
      suffix = '<div class="form_suffix">' + suffix + '</div>';
    }
    // Render the form's input elements.
    var form_elements = _drupalgap_form_render_elements(form);
    var form_attributes = drupalgap_attributes(form.options.attributes);
    // Return the form html.
    var form_html = '<form id="' + form.id + '" ' + form_attributes + '>' +
      prefix +
      '<div id="drupalgap_form_errors"></div>' +
      form_elements +
      suffix +
    '</form>';
    return form_html;
  }
  catch (error) { console.log('drupalgap_form_render - ' + error); }
}

/**
 * Given a form element name and an error message, this attaches the error
 * message to the drupalgap.form_errors array, keyed by the form element name.
 * @param {String} name
 * @param {String} message
 */
function drupalgap_form_set_error(name, message) {
  try {
    drupalgap.form_errors[name] = message;
  }
  catch (error) { console.log('drupalgap_form_set_error - ' + error); }
}

/**
 * Given a form id, this will render the form and return the html for the form.
 * Any additional arguments will be sent along to the form.
 * @param {String} form_id
 * @return {String}
 */
function drupalgap_get_form(form_id) {
  try {
    var html = '';
    var form = drupalgap_form_load.apply(
      null,
      Array.prototype.slice.call(arguments)
    );
    if (form) {
      // Render the form.
      html = drupalgap_form_render(form);
    }
    else {
      var msg = 'drupalgap_get_form - ' + t('failed to get form') +
        ' (' + form_id + ')';
      drupalgap_alert(msg);
    }
    return html;
  }
  catch (error) { console.log('drupalgap_get_form - ' + error); }
}

/**
 * Given a form id, this will delete the form from local storage.
 * If the form isn't in local storage, this returns false.
 * @param {String} form_id
 * @return {Object}
 */
function drupalgap_form_local_storage_delete(form_id) {
  try {
    var result = window.localStorage.removeItem(
      drupalgap_form_id_local_storage_key(form_id)
    );
    return result;
  }
  catch (error) {
    console.log('drupalgap_form_local_storage_delete - ' + error);
  }
}

/**
 * Given a form id, this will load the form from local storage and return it.
 * If the form isn't in local storage, this returns false.
 * @param {String} form_id
 * @return {Object}
 */
function drupalgap_form_local_storage_load(form_id) {
  try {
    var form = false;
    form = window.localStorage.getItem(
      drupalgap_form_id_local_storage_key(form_id)
    );
    if (!form) { form = false; }
    else { form = JSON.parse(form); }
    return form;
  }
  catch (error) { console.log('drupalgap_form_local_storage_load - ' + error); }
}

/**
 * Given a form, this will save the form to local storage, overwriting any
 * previously saved forms.
 * @param {Object} form
 */
function drupalgap_form_local_storage_save(form) {
  try {
    window.localStorage.setItem(
      drupalgap_form_id_local_storage_key(form.id),
      JSON.stringify(form)
    );
  }
  catch (error) { console.log('drupalgap_form_local_storage_save - ' + error); }
}

/**
 * Given a form id, this will return the local storage key used by DrupalGap
 * to save the assembled form to the device's local storage.
 * @param {String} form_id
 * @return {String}
 */
function drupalgap_form_id_local_storage_key(form_id) {
    return 'drupalgap_form_' + form_id;
}


/**
 * Given a form id, this will return the form JSON object assembled by the
 * form's call back function. If the form fails to load, this returns false.
 * @param {String} form_id
 * @return {Object}
 */
function drupalgap_form_load(form_id) {
  try {
    var form = drupalgap_form_defaults(form_id);

    // The form's call back function will be equal to the form id.
    var function_name = form_id;
    if (drupalgap_function_exists(function_name)) {

      // Grab the form's function.
      var fn = window[function_name];

      // Determine the language code.
      var language = language_default();

      // Build the form arguments by iterating over each argument then adding
      // each to to the form arguments, afterwards remove the argument at index
      // zero because that is the form id.
      var form_arguments = [];
      for (var index in arguments) {
        if (!arguments.hasOwnProperty(index)) { continue; }
        var argument = arguments[index];
        form_arguments.push(argument);
      }
      form_arguments.splice(0, 1);

      // Attach the form arguments to the form object.
      form.arguments = form_arguments;

      // If there were no arguments to pass along, call the function directly to
      // retrieve the form, otherwise call the function and pass along any
      // arguments to retrieve the form.
      if (form_arguments.length == 0) { form = fn(form, null); }
      else {
        // We must consolidate the form, form_state and arguments into one array
        // and then pass it along to the form builder.
        var consolidated_arguments = [];
        var form_state = null;
        consolidated_arguments.push(form);
        consolidated_arguments.push(form_state);
        for (var index in form_arguments) {
          if (!form_arguments.hasOwnProperty(index)) { continue; }
          var argument = form_arguments[index];
          consolidated_arguments.push(argument);
        }
        form = fn.apply(
            null,
            Array.prototype.slice.call(consolidated_arguments)
        );
      }

      // Set default values across elements in preparation for alteration.
      _drupalgap_form_load_set_element_defaults(form, language);

      // Give modules an opportunity to alter the form.
      module_invoke_all('form_alter', form, null, form_id);

      // Set default values across elements again in case any elements were added in preparation for alteration.
      _drupalgap_form_load_set_element_defaults(form, language);

      // Place the assembled form into local storage so _drupalgap_form_submit
      // will have access to the assembled form.
      drupalgap_form_local_storage_save(form);
      Drupal.cache_expiration.forms[form_id] = 1;
      window.localStorage.setItem('cache_expiration', JSON.stringify(Drupal.cache_expiration));
    }
    else {
      var error_msg = 'drupalgap_form_load - ' + t('no callback function') +
          ' (' + function_name + ') ' + t('available for form') +
          ' (' + form_id + ')';
      drupalgap_alert(error_msg);
    }
    return form;
  }
  catch (error) { console.log('drupalgap_form_load - ' + error); }
}

/**
 * An internal function used to set default values onto all form elements' properties, such as an id and name.
 * @param form
 * @param language
 * @private
 */
function _drupalgap_form_load_set_element_defaults(form, language) {
  try {

    // Set empty options and attributes properties on each form element if the
    // element does not yet have any. This allows others to more easily modify
    // options and attributes on an element without having to worry about
    // testing for nulls and creating empty properties first.
    for (var name in form.elements) {

      if (!form.elements.hasOwnProperty(name)) { continue; }
      var element = form.elements[name];
      //console.log(name);
      //console.log(element);

      // If we haven't already, check to see f this element is a field, load its field_info_field and
      // field_info_instance onto the element.
      if (typeof element.is_field === 'undefined') {
        var element_is_field = false;
        var field_info_field = drupalgap_field_info_field(name);
        if (field_info_field) {
          element_is_field = true;
          form.elements[name].field_info_field = field_info_field;
          form.elements[name].field_info_instance =
              drupalgap_field_info_instance(
                  form.entity_type,
                  name,
                  form.bundle
              );
        }
        form.elements[name].is_field = element_is_field;
      }

      // Set the name property on the element if it isn't already set.
      if (!form.elements[name].name) { form.elements[name].name = name; }

      // If the element is a field, we'll append a language code and delta
      // value to the element id, along with the field items appended
      // onto the element using the language code and delta values.
      var id = null;
      if (element_is_field) {
        // What's the number of allowed values (cardinality) on this field?
        // A cardinality of -1 means the field has unlimited values.
        var cardinality = parseInt(element.field_info_field.cardinality);
        if (cardinality == -1) {
          cardinality = 1; // we'll just add one element for now, until we
                           // figure out how to handle the 'add another
                           // item' feature.
        }
        // Initialize the item collections language code if it hasn't been.
        if (!form.elements[name][language]) {
          form.elements[name][language] = {};
        }
        // Prepare the item(s) for this element.
        for (var delta = 0; delta < cardinality; delta++) {
          // Prepare some item defaults.
          var item = drupalgap_form_element_item_create(
              name,
              form,
              language,
              delta
          );
          // If the delta for this item hasn't been created on the element,
          // create it using the default item values. Otherwise, merge the
          // default values into the pre existing item on the element.
          if (!form.elements[name][language][delta]) {
            form.elements[name][language][delta] = item;
          }
          else {
            $.extend(true, form.elements[name][language][delta], item);
          }
        }
      }
      else {
        if (element.name == 'user_roles') { console.log('defaults', element); }
        // This element is not a field, setup default options if none
        // have been provided. Then set the element id.
        if (!element.options) {
          form.elements[name].options = {attributes: {}};
        }
        else if (!element.options.attributes) {
          form.elements[name].options.attributes = {};
        }
        id = drupalgap_form_get_element_id(name, form.id);
        form.elements[name].id = id;
        form.elements[name].options.attributes.id = id;
      }
    }
  }
  catch (error) { console.log('_drupalgap_form_elements_set_defaults - ' + error); }
}

/**
 * Optionally use this function as an HTML DOM onkeypress handler, and it will
 * attempt to listen for the enter key being pressed and submit the form at that
 * time.
 * @param {String} form_id
 * @return {Boolean}
 */
function drupalgap_form_onkeypress(form_id) {
  try {
    var event = window.event;
    var charCode = event.which || event.keyCode;
    if (charCode != '13') { return; }
    $('#' + form_id + ' button.dg_form_submit_button').click();
    event.preventDefault();
    return false;
  }
  catch (error) { console.log('drupalgap_form_onkeypress - ' + error); }
}

/**
 * Handles a drupalgap form's submit button click.
 * @param {String} form_id
 * @return {*}
 */
function _drupalgap_form_submit(form_id) {
  try {
    // Load the form from local storage.
    var form = drupalgap_form_local_storage_load(form_id);
    if (!form) {
      var msg = '_drupalgap_form_submit - ' + t('failed to load form') + ': ' +
        form_id;
      drupalgap_alert(msg);
      return false;
    }

    // Assemble the form state values.
    var form_state = drupalgap_form_state_values_assemble(form);

    // Clear out previous form errors.
    drupalgap.form_errors = {};

    // Build the form validation wrapper function.
    var form_validation = function() {
      try {

        // Call the form's validate function(s), if any.
        for (var index in form.validate) {
            if (!form.validate.hasOwnProperty(index)) { continue; }
            var function_name = form.validate[index];
            var fn = window[function_name];
            fn.apply(null, Array.prototype.slice.call([form, form_state]));
        }

        // Call drupalgap form's api validate.
        _drupalgap_form_validate(form, form_state);

        // If there were validation errors, show the form errors and stop the
        // form submission. Otherwise submit the form.
        if (!jQuery.isEmptyObject(drupalgap.form_errors)) {
          var html = '';
          for (var name in drupalgap.form_errors) {
              if (!drupalgap.form_errors.hasOwnProperty(name)) { continue; }
              var message = drupalgap.form_errors[name];
              html += message + '\n\n';
          }
          drupalgap_alert(html);
        }
        else { form_submission(); }
      }
      catch (error) {
        console.log('_drupalgap_form_submit - form_validation - ' + error);
      }
    };

    // Build the form submission wrapper function.
    var form_submission = function() {
      try {
        // Call the form's submit function(s), if any.
        for (var index in form.submit) {
            if (!form.submit.hasOwnProperty(index)) { continue; }
            var function_name = form.submit[index];
            var fn = window[function_name];
            fn.apply(null, Array.prototype.slice.call([form, form_state]));
        }
        // Remove the form from local storage.
        // @todo - we can't do this here because often times a form's submit
        // handler makes asynchronous calls (i.e. user login) and although the
        // form validated, server side may say the input was invalid, so the
        // user will still be on the form, except we already removed the form.
        //drupalgap_form_local_storage_delete(form_id);
      }
      catch (error) {
        console.log('_drupalgap_form_submit - form_submission - ' + error);
      }
    };

    // Get ready to validate and submit the form, but first...

    // If this is an entity form, and there is an image field on the form, we
    // need to asynchronously process the image field, then continue onward
    // with normal form validation and submission.
    if (form.entity_type &&
      image_fields_present_on_entity_type(form.entity_type, form.bundle)
    ) {
      _image_field_form_process(form, form_state, {
          success: form_validation
      });
    }
    else {
      // There were no image fields on the form, proceed normally with form
      // validation, which will in turn process the submission if there are no
      // validation errors.
      form_validation();
    }
  }
  catch (error) { console.log('_drupalgap_form_submit - ' + error); }
}

/**
 * An internal function used by the DrupalGap forms api to validate all the
 * elements on a form.
 * @param {Object} form
 * @param {Object} form_state
 */
function _drupalgap_form_validate(form, form_state) {
  try {
    for (var name in form.elements) {
        if (!form.elements.hasOwnProperty(name)) { continue; }
        var element = form.elements[name];
        if (name == 'submit') { continue; }
        if (element.required) {
          var valid = true;
          var value = null;
          if (element.is_field) {
            value = form_state.values[name][language_default()][0];
          }
          else { value = form_state.values[name]; }
          // Check for empty values.
          if (empty(value)) { valid = false; }
          // Validate a required select list.
          else if (
            element.type == 'select' && element.required && value == ''
          ) { valid = false; }
          else if (element.type == 'checkboxes' && element.required) {
            var has_value = false;
            for (var key in form_state.values[name]) {
              if (!form_state.values[name].hasOwnProperty(key)) { continue; }
              var value = form_state.values[name][key];
              if (value) { has_value = true; break; }
            }
            if (!has_value) { valid = false; }
          }
          if (!valid) {
            var field_title = name;
            if (element.title) { field_title = element.title; }
            drupalgap_form_set_error(
              name,
              t('The') + ' ' + field_title + ' ' + t('field is required') + '.'
            );
          }
        }
    }
  }
  catch (error) { console.log('_drupalgap_form_validate - ' + error); }
}

/**
 * Given a form, this function iterates over the form's elements and assembles
 * each element and value and places them into the form state's values. This
 * is similar to $form_state['values'] in Drupal.
 * @param {Object} form
 * @return {Object}
 */
function drupalgap_form_state_values_assemble(form) {
  try {
    var lng = language_default();
    var form_state = { values: {} };
    for (var name in form.elements) {
      if (!form.elements.hasOwnProperty(name)) { continue; }
      var element = form.elements[name];
      if (name == 'submit') { continue; } // Always skip the form 'submit'.
      var id = null;
      if (element.is_field) {
        form_state.values[name] = {};
        form_state.values[name][lng] = {};
        var allowed_values = element.field_info_field.cardinality;
        if (allowed_values == -1) {
          allowed_values = 1; // Convert unlimited value field to one for now...
        }
        for (var delta = 0; delta < allowed_values; delta++) {
          id = drupalgap_form_get_element_id(name, form.id, lng, delta);
          form_state.values[name][lng][delta] =
            _drupalgap_form_state_values_assemble_get_element_value(
              id,
              element
            );
        }
      }
      else {
        id = drupalgap_form_get_element_id(name, form.id);
        form_state.values[name] =
          _drupalgap_form_state_values_assemble_get_element_value(
            id,
            element
          );
      }
    }
    // Attach the form state to drupalgap.form_states keyed by the form id.
    drupalgap.form_states[form.id] = form_state;
    return form_state;
  }
  catch (error) {
    console.log('drupalgap_form_state_values_assemble - ' + error);
  }
}

/**
 *
 * @param {String} id
 * @param {Object} element
 * @return {String|Number}
 */
function _drupalgap_form_state_values_assemble_get_element_value(id, element) {
  try {
    // If a value_callback is specified on the element, call that function to
    // retrieve the element's value. Ohterwise, we'll use the default techniques
    // implemented to extract a value for the form state.
    if (element.value_callback && function_exists(element.value_callback)) {
      var fn = window[element.value_callback];
      return fn(id, element);
    }
    // Figure out the value, depending on the element type.
    var value = null;
    var selector = '';
    if (element.type == 'radios') {
      selector = 'input:radio[name="' + id + '"]:checked';
    }
    else { selector = '#' + id; }
    switch (element.type) {
      case 'checkbox':
        var _checkbox = $(selector);
        if ($(_checkbox).is(':checked')) { value = 1; }
        else { value = 0; }
        break;
      case 'checkboxes':
        // Iterate over each option, and see if it is checked, then pop it onto
        // the value array. It's possible for values to be updated dynamically
        // by a developer (i.e. through a pageshow handler), so it isn't safe to
        // look at the options in local storage. Instead we'll directly iterate
        // over the element option(s) in the DOM.
        value = {};
        var options = $('label[for="' + id + '"]').siblings('.ui-checkbox');
        $.each(options, function(index, option) {
          var checkbox = $(option).children('input');
          var _value = $(checkbox).attr('value');
          if ($(checkbox).is(':checked')) { value[_value] = _value; }
          else { value[_value] = 0; }
        });
        break;
      case 'list_boolean':
        var _checkbox = $(selector);
        if ($(_checkbox).is(':checked')) { value = $(_checkbox).attr('on'); }
        else { value = $(_checkbox).attr('off'); }
        break;
      case 'list_text':
        // Radio buttons.
        if (
          element.field_info_instance &&
          element.field_info_instance.widget.type == 'options_buttons'
        ) {
          selector = 'input:radio[name="' + id + '"]:checked';
        }
        break;
    }
    if (value === null) { value = $(selector).val(); }
    if (typeof value === 'undefined') { value = null; }
    return value;
  }
  catch (error) {
    console.log(
      '_drupalgap_form_state_values_assemble_get_element_value - ' +
      error
    );
  }
}

/**
 * When a service call results in an error, this function is used to extract the
 * request's response form errors into a human readble string and returns it. If
 * there are no form errors, it will return false.
 * @param {Object} form
 * @param {Object} form_state
 * @param {Object} xhr
 * @param {String} status
 * @param {String} message
 * @return {String|Boolean}
 */
function _drupalgap_form_submit_response_errors(form, form_state, xhr, status,
  message) {
  try {
    var responseText = JSON.parse(xhr.responseText);
    if (typeof responseText === 'object' && responseText.form_errors) {
      var msg = '';
      for (var element_name in responseText.form_errors) {
          if (!responseText.form_errors.hasOwnProperty(element_name)) { continue; }
          var error_msg = responseText.form_errors[element_name];
          if (error_msg != '') {
            // The element name tends to come back weird, e.g.
            // "field_art_type][und", so let's trim anything at and after
            // the first "]".
            var pos = element_name.indexOf(']');
            if (pos != -1) {
              element_name = element_name.substr(0, pos);
            }
            var label = element_name;
            if (
              form.elements[element_name] &&
              form.elements[element_name].title
            ) { label = form.elements[element_name].title; }
            msg += $('<div>(' + label + ') - ' +
              error_msg + '</div>').text() + '\n';
          }
      }
      if (msg != '') { return msg; }
    }
    return false;
  }
  catch (error) {
    console.log('_drupalgap_form_submit_response_errors - ' + error);
  }
}


/**
 * Themes a checkbox input.
 * @param {Object} variables
 * @return {String}
 */
function theme_checkbox(variables) {
  try {
    variables.attributes.type = 'checkbox';
    // Check the box?
    if (variables.checked) {
      variables.attributes.checked = 'checked';
    }
    var output = '<input ' + drupalgap_attributes(variables.attributes) + ' />';
    return output;
  }
  catch (error) { console.log('theme_checkbox - ' + error); }
}

/**
 * Themes checkboxes input.
 * @param {Object} variables
 * @return {String}
 */
function theme_checkboxes(variables) {
  try {
    var html = '';
    variables.attributes.type = 'checkboxes';
    for (var value in variables.options) {
        if (!variables.options.hasOwnProperty(value)) { continue; }
        var label = variables.options[value];
        if (value == 'attributes') { continue; } // Skip attributes.
        var _label = value;
        if (!empty(label)) { _label = label; }
        var checkbox = {
          value: value,
          attributes: {
            name: variables.name + '[' + value + ']',
            'class': variables.name,
            value: value
          }
        };
        if (variables.value && variables.value[value]) { checkbox.checked = true; }
        html += '<label>' + theme('checkbox', checkbox) + '&nbsp;' + label + '</label>';
    }
    // Check the box?
    /*if (variables.checked) {
      variables.attributes.checked = 'checked';
    }*/
    return html;
  }
  catch (error) { console.log('theme_checkbox - ' + error); }
}

/**
 * Themes a email input.
 * @param {Object} variables
 * @return {String}
 */
function theme_email(variables) {
  try {
    variables.attributes.type = 'email';
    var output = '<input ' + drupalgap_attributes(variables.attributes) + ' />';
    return output;
  }
  catch (error) { console.log('theme_email - ' + error); }
}

/**
 * Themes a file input.
 * @param {Object} variables
 * @return {String}
 */
function theme_file(variables) {
  try {
    variables.attributes.type = 'file';
    var output = '<input ' + drupalgap_attributes(variables.attributes) + ' />';
    return output;
  }
  catch (error) { console.log('theme_file - ' + error); }
}

/**
 * Themes a form element label.
 * @param {Object} variables
 * @return {String}
 */
function theme_form_element_label(variables) {
  try {
    var element = variables.element;
    if (empty(element.title)) { return ''; }
    // Any elements with a title_placeholder set to true
    // By default, use the element id as the label for, unless the element is
    // a radio, then use the name.
    var label_for = '';
    if (element.id) { label_for = element.id; }
    else if (element.attributes && element.attributes['for']) {
      label_for = element.attributes['for'];
    }
    if (element.type == 'radios') { label_for = element.name; }
    // Render the label.
    var html =
      '<label for="' + label_for + '"><strong>' + element.title + '</strong>';
    if (element.required) { html += theme('form_required_marker', { }); }
    html += '</label>';
    return html;
  }
  catch (error) { console.log('theme_form_element_label - ' + error); }
}

/**
 * Themes a marker for a required form element label.
 * @param {Object} variables
 * @return {String}
 */
function theme_form_required_marker(variables) {
    return '*';
}

/**
 * Themes a number input.
 * @param {Object} variables
 * @return {String}
 */
function theme_number(variables) {
  try {
    variables.attributes.type = 'number';
    return '<input ' + drupalgap_attributes(variables.attributes) + ' />';
  }
  catch (error) { console.log('theme_number - ' + error); }
}

/**
 * Themes a hidden input.
 * @param {Object} variables
 * @return {String}
 */
function theme_hidden(variables) {
  try {
    variables.attributes.type = 'hidden';
    if (!variables.attributes.value && variables.value != null) {
      variables.attributes.value = variables.value;
    }
    return '<input ' + drupalgap_attributes(variables.attributes) + ' />';
  }
  catch (error) { console.log('theme_hidden - ' + error); }
}

/**
 * Themes a password input.
 * @param {Object} variables
 * @return {String}
 */
function theme_password(variables) {
  try {
    variables.attributes.type = 'password';
    return '<input ' + drupalgap_attributes(variables.attributes) + ' />';
  }
  catch (error) { console.log('theme_password - ' + error); }
}

/**
 * Themes radio buttons.
 * @param {Object} variables
 * @return {String}
 */
function theme_radios(variables) {
  try {
    var radios = '';
    if (variables.options) {
      variables.attributes.type = 'radio';
      // Determine an id prefix to use.
      var id = 'radio';
      if (variables.attributes.id) {
        id = variables.attributes.id;
        delete variables.attributes.id;
      }
      // Set the radio name equal to the id if one doesn't exist.
      if (!variables.attributes.name) {
        variables.attributes.name = id;
      }
      // Init a delta value so each radio button can have a unique id.
      var delta = 0;
      for (var value in variables.options) {
          if (!variables.options.hasOwnProperty(value)) { continue; }
          var label = variables.options[value];
          if (value == 'attributes') { continue; } // Skip the attributes.
          var checked = '';
          if (variables.value && variables.value == value) {
            checked = ' checked="checked" ';
          }
          var input_id = id + '_' + delta.toString();
          var input_label =
            '<label for="' + input_id + '">' + label + '</label>';
          radios += '<input id="' + input_id + '" value="' + value + '" ' +
                                 drupalgap_attributes(variables.attributes) +
                                 checked + ' />' + input_label;
          delta++;
      }
    }
    return radios;
  }
  catch (error) { console.log('theme_radios - ' + error); }
}

/**
 * Themes a range input.
 * @param {Object} variables
 * @return {String}
 */
function theme_range(variables) {
  try {
    variables.attributes.type = 'range';
    if (typeof variables.attributes.value === 'undefined') {
      variables.attributes.value = variables.value;
    }
    var output = '<input ' + drupalgap_attributes(variables.attributes) + ' />';
    return output;
  }
  catch (error) { console.log('theme_range - ' + error); }
}

/**
 * Themes a search input.
 * @param {Object} variables
 * @return {String}
 */
function theme_search(variables) {
  try {
    variables.attributes.type = 'search';
    var output = '<input ' + drupalgap_attributes(variables.attributes) + ' />';
    return output;
  }
  catch (error) { console.log('theme_search - ' + error); }
}

/**
 * Themes a select list input.
 * @param {Object} variables
 * @return {String}
 */
function theme_select(variables) {
  try {
    var options = '';
    if (variables.options) {
      for (var value in variables.options) {
          if (!variables.options.hasOwnProperty(value)) { continue; }
          var label = variables.options[value];
          if (value == 'attributes') { continue; } // Skip the attributes.
          // Is the option selected?
          var selected = '';
          if (typeof variables.value !== 'undefined') {
            if (
              ($.isArray(variables.value) && in_array(value, variables.value)) ||
              variables.value == value
            ) { selected = ' selected '; }
          }
          // Render the option.
          options += '<option value="' + value + '" ' + selected + '>' +
            label +
          '</option>';
      }
    }
    return '<select ' + drupalgap_attributes(variables.attributes) + '>' +
      options +
    '</select>';
  }
  catch (error) { console.log('theme_select - ' + error); }
}

/**
 * Themes a telephone input.
 * @param {Object} variables
 * @return {String}
 */
function theme_tel(variables) {
  try {
    variables.attributes['type'] = 'tel';
    return '<input ' + drupalgap_attributes(variables.attributes) + ' />';
  }
  catch (error) { console.log('theme_tel - ' + error); }
}

/**
 * Themes a text input.
 * @param {Object} variables
 * @return {String}
 */
function theme_textfield(variables) {
  try {
    variables.attributes.type = 'text';
    return '<input ' + drupalgap_attributes(variables.attributes) + ' />';
  }
  catch (error) { console.log('theme_textfield - ' + error); }
}

/**
 * Themes a textarea input.
 * @param {Object} variables
 * @return {String}
 */
function theme_textarea(variables) {
  try {
    var output =
      '<div><textarea ' + drupalgap_attributes(variables.attributes) + '>' +
        variables.value +
      '</textarea></div>';
    return output;
  }
  catch (error) { console.log('theme_textarea - ' + error); }
}


// Holds onto any query string during the page go process.
var _drupalgap_goto_query_string = null;

/**
 * Given a path, this will change the current page in the app.
 * @param {String} path
 * @return {*}
 */
function drupalgap_goto(path) {
  try {

    // Extract any incoming options, set any defaults that weren't provided,
    // then populate the global page options variable.
    var options = {};
    if (arguments[1]) {
      options = arguments[1];
      if (typeof options.form_submission === 'undefined') {
        options.form_submission = false;
      }
    }
    drupalgap.page.options = options;

    // Prepare the path.
    path = _drupalgap_goto_prepare_path(path, true);
    if (!path) { return false; }

    // Invoke all implementations of hook_drupalgap_goto_preprocess().
    module_invoke_all('drupalgap_goto_preprocess', path);

    // Determine the router path.
    var router_path = drupalgap_get_menu_link_router_path(path);

    // Make sure we have a menu link item that can handle this router path,
    // otherwise we'll goto the 404 page.
    if (!drupalgap.menu_links[router_path]) {
      // Is anyone trying to handle this 404?
      var new_path = false;
      var invocation_results = module_invoke_all('404', router_path);
      if (invocation_results) {
        for (var index in invocation_results) {
            if (!invocation_results.hasOwnProperty(index)) { continue; }
            var result = invocation_results[index];
            if (result !== false) {
              new_path = result;
              break;
            }
        }
      }
      // If a 404 handler provided a new path use it, otherwise just use the
      // system 404 page. Either way, update the router path before continuing
      // with a normal page build.
      if (new_path) { path = new_path; }
      else { path = '404'; }
      router_path = drupalgap_get_menu_link_router_path(path);
    }

    // Make sure the user has access to this router path, if they don't send
    // them to the 401 page.
    // @TODO - for now we're going to skip access checks on local tasks, since
    // they are covered by menu_block_view(), but if someone were to navigate
    // directly to e.g. a node's edit page, they would be able to see the page.
    // Of course Drupal would actually prevent them from updating the node on
    // the live site, but nonetheless this needs to be fixed. It's a tough issue
    // though and related to https://github.com/signalpoint/DrupalGap/issues/257
    if (
      drupalgap.menu_links[router_path].type != 'MENU_DEFAULT_LOCAL_TASK' &&
      drupalgap.menu_links[router_path].type != 'MENU_LOCAL_TASK' &&
      !drupalgap_menu_access(router_path)
    ) {
      path = '401';
      router_path = drupalgap_get_menu_link_router_path(path);
    }

    // If the new router path is the same as the current router path and the new
    // path is the same as the current path, we may need to cancel the
    // navigation attempt (i.e. don't go anywhere), unless it is a
    // form submission, then continue.
    if (
      router_path == drupalgap_router_path_get() &&
      path == drupalgap_path_get()
    ) {
      // If it's a form submission, we'll continue onward...
      if (options.form_submission) { }

      // If we're reloading the current page, we need to set aside this path
      // and navigate to the system's _reload page, which will then handle the
      // actual reloading of the page.
      // @see system_drupalgap_goto_post_process()
      else if (options.reloadPage) {
        _system_reload_page = path;
        path = '_reload';
        router_path = drupalgap_get_menu_link_router_path(path);
      }

      // Otherwise, just stop the navigation attempt.
      else { return false; }

    }

    // Grab the page id.
    var page_id = drupalgap_get_page_id(path);

    // Return if we are trying to go to the path we are already on, unless this
    // was a form submission, then we'll let the page rebuild itself. For
    // accuracy we compare the jQM active page url with the destination page
    // id.
    // @todo - this boolean doesn't match the comment description of the code
    // block, i.e. the form_submission check is opposite of what it says
    if (drupalgap_jqm_active_page_url() == page_id && options.form_submission) {
      // Clear any messages from the page before returning.
      drupalgap_clear_messages();
      return false;
    }

    // @TODO when drupalgap_goto() is called during the bootstrap, we shouldn't
    // put the path onto the back_path array.

    // Save the back path.
    var back_paths_to_ignore = ['user/logout', '_reload'];
    if (!in_array(drupalgap_path_get())) {
      drupalgap.back_path.push(drupalgap_path_get());
    }

    // Set the current menu path to the path input.
    drupalgap_path_set(path);

    // Set the drupalgap router path.
    drupalgap_router_path_set(router_path);

    // If the page is already in the DOM and we're asked to reload it, then
    // remove the page and let it rebuild itself. If we're not reloading the
    // page and we're not in the middle of a form submission, prevent the page
    // from processing then change to it.
    if (drupalgap_page_in_dom(page_id)) {

      // If there are any hook_menu() item options for this router path, bring
      // them into the current options without overwriting any existing values.
      if (drupalgap.menu_links[router_path].options) {
        options = $.extend(
          {},
          drupalgap.menu_links[router_path].options,
          options
        );
      }

      // Reload the page? If so, remove the page from the DOM, delete the
      // reloadPage option, then set the reloadingPage option to true so others
      // down the line will know the page is reloading. We can't pass along the
      // actual reloadPage option since it may collide with jQM later on. We
      // have to use 'force' when removing the page from the DOM since DG won't
      // remove it since it thinks we are already on the page, so it won't
      // remove it.
      if (typeof options.reloadPage !== 'undefined' && options.reloadPage) {
        // @TODO we may need to leave the query ONLY if the above call to
        // _drupalgap_goto_prepare_path() yielded a query string, otherwise we
        // may be leaving unwanted queries in _GET()!
        var leaveQuery = _drupalgap_goto_query_string ? true : false;
        drupalgap_remove_page_from_dom(page_id, {
          force: true,
          leaveQuery: leaveQuery
        });
        delete options.reloadPage;
        options.reloadingPage = true;
      }
      else if (!options.form_submission) {
        drupalgap_clear_messages();
        _drupalgap_goto_query_string = null;
        drupalgap.page.process = false;
        // @TODO changePage has been deprecated as of jQM 1.4.0 and will be
        // removed in 1.50.
        // @see https://api.jquerymobile.com/jQuery.mobile.changePage/
        $.mobile.changePage('#' + page_id, options);
        module_invoke_all('drupalgap_goto_post_process', path);
        return;
      }
    }
    else if (typeof options.reloadPage !== 'undefined' && options.reloadPage) {
      // The page is not in the DOM, and we're being asked to reload it, so
      // we'll just delete the reloadPage option.
      delete options.reloadPage;
    }

    // Generate the page.
    drupalgap_goto_generate_page_and_go(path, page_id, options, drupalgap.menu_links[router_path]);
  }
  catch (error) { console.log('drupalgap_goto - ' + error); }
}

/**
 * Generate a JQM page by running it through the theme then attach the
 * page to the <body> of the document, then change to the page. Remember,
 * the rendering of the page does not take place here, that is covered by
 * the pagebeforechange event in theme.inc.js which happens after we change
 * the page here.
 * @param {String} path
 * @param {String} page_id
 * @param {Object} options
 * @param {Object} menu_link The menu link object from drupalgap.menu_links.
 */
function drupalgap_goto_generate_page_and_go(
  path, page_id, options, menu_link) {
  try {

    // @TODO using a page.tpl.html is pretty dumb, this makes a disc read on each page change, use render arrays only
    // be deprecating the page.tpl.html file, converting it to a render array and warning developers to upgrade their
    // themes.
    var page_template_path = path_to_theme() + '/page.tpl.html';
    if (!drupalgap_file_exists(page_template_path)) {
      console.log(
        'drupalgap_goto_generate_page_and_go - ' +
        'page template does not exist! (' + page_template_path + ')'
      );
    }
    else {

      // Reset the internal query string.
      _drupalgap_goto_query_string = null;

      // If options wasn't set, set it as an empty JSON object.
      if (typeof options === 'undefined') { options = {}; }

      // Load the page template html file. Determine if we are going to cache
      // the template file or not.
      // @TODO another disc read here, dumb, use render arrays and deprecate.
      var file_options = {};
      if (drupalgap.settings.cache &&
          drupalgap.settings.cache.theme_registry !== 'undefined' &&
          !drupalgap.settings.cache.theme_registry) {
          file_options.cache = false;
       }
      var html = drupalgap_file_get_contents(page_template_path, file_options);

      if (html) {

        // Add page to DOM.
        drupalgap_add_page_to_dom({
            page_id: page_id,
            html: html,
            menu_link: menu_link
        });

        // Setup change page options if necessary.
        if (drupalgap_path_get() == path && options.form_submission) {
          options.allowSamePageTransition = true;
        }

        // Let's change to the page. Web apps and the ripple emulator do not
        // seem to like the 'index.html' prefix, so we'll remove that.
        var destination = 'index.html#' + page_id;
        if (
          drupalgap.settings.mode != 'phonegap' ||
          typeof parent.window.ripple === 'function'
        ) { destination = '#' + page_id; }
        $.mobile.changePage(destination, options); // @see the pagebeforechange handler in page.inc.js

        // Invoke all implementations of hook_drupalgap_goto_post_process().
        module_invoke_all('drupalgap_goto_post_process', path);
      }
      else {
        drupalgap_alert(
          'drupalgap_goto_generate_page_and_go - ' +
          t('failed to load theme\'s page.tpl.html file')
        );
      }
    }
  }
  catch (error) {
    console.log('drupalgap_goto_generate_page_and_go - ' + error);
  }
}

/**
 * Prepares a drupalgap page path.
 * @deprecated Use _drupalgap_goto_prepare_path() instead.
 * @param {String} path
 * @return {String}
 */
function drupalgap_goto_prepare_path(path) {
  try {
    console.log('WARNING - drupalgap_goto_prepare_path() is deprecated, ' +
      'use _drupalgap_goto_prepare_path() instead!');
    return _drupalgap_goto_prepare_path(path);
  }
  catch (error) { console.log('drupalgap_goto_prepare_path - ' + error); }
}

/**
 * An internal function used to prepare the path for menu routing. An optional
 * second parameter (boolean) may be passed in, and if it is set to true it will
 * process any _GET query string parameters.
 * @param {String} path
 * @return {String}
 */
function _drupalgap_goto_prepare_path(path) {
  try {

    // Pull out any query string parameters and populate them into _GET, if we
    // were instructed to do so. Hold onto a global copy of the query string
    // for page go processing.
    if (typeof arguments[1] !== 'undefined' && arguments[1]) {
      var pos = path.indexOf('?');
      if (pos != -1 && pos != path.length - 1) {
        var query = path.substr(pos + 1, path.length - pos);
        _drupalgap_goto_query_string = query;
        path = path.substr(0, pos);
        var parts = query.split('&');
        for (var i = 0; i < parts.length; i++) {
          pos = parts[i].indexOf('=');
          if (pos == -1) { continue; }
          query = parts[i].split('=');
          if (query.length != 2) { continue; }
          _GET(
            decodeURIComponent(query[0]),
            decodeURIComponent(query[1]),
            path
          );
        }
      }
    }

    // If the path is an empty string, change it to the front page path.
    if (path == '') {
      if (!drupalgap.settings.front) {
        drupalgap_alert(
          'drupalgap_goto_prepare_path - ' +
          t('no front page specified in settings.js!')
        );
        return false;
      }
      else { path = drupalgap.settings.front; }
    }

    // Change 'user' to 'user/login' for anonymous users, or change it to e.g.
    // 'user/123' for authenticated users.
    else if (path == 'user') {
      if (Drupal.user.uid != 0) { path = 'user/' + Drupal.user.uid; }
      else { path = 'user/login'; }
    }

    // Finally return the path.
    return path;

  }
  catch (error) { console.log('drupalgap_goto_prepare_path - ' + error); }
}

/**
 * Change the page to the previous page.
 */
function drupalgap_back() {
  try {
    var active_page_id = $('.ui-page-active').attr('id');
    if (active_page_id == drupalgap.settings.front) {
      var msg = t('Exit') + ' ' + drupalgap.settings.title + '?';
      if (drupalgap.settings.exit_message) {
        msg = drupalgap.settings.exit_message;
      }
      drupalgap_confirm(msg, {
          confirmCallback: _drupalgap_back_exit
      });
    }
    else if (active_page_id == '_drupalgap_splash') { return; }
    else { _drupalgap_back(); }
  }
  catch (error) { console.log('drupalgap_back - ' + error); }
}

/**
 * An internal function used to change the page to the previous page.
 */
function _drupalgap_back() {
  try {
    // @WARNING - any changes here (except the history.back() call) need to be
    // reflected into the window "navigate" handler below
    drupalgap.back = true;

    // Properly handle iOS9 back button clicks, and default back button clicks.
    if (typeof device !== 'undefined' && device.platform === "iOS" && parseInt(device.version) === 9) {
      $.mobile.back();
    }
    else { history.back(); }

    // Update the path and router path.
    var from = drupalgap_path_get();
    drupalgap_path_set(drupalgap.back_path.pop());
    var to = drupalgap_path_get();
    drupalgap_router_path_set(drupalgap_get_menu_link_router_path(to));
    module_invoke_all('drupalgap_back', from, to);

  }
  catch (error) { console.log('_drupalgap_back - ' + error); }
}

/**
 * An internal function used to exit the app when the back button is clicked.
 * @param {Number} button Which button was pressed.
 */
function _drupalgap_back_exit(button) {
  try {
    button === 1 ? navigator.app.exitApp() : '';
  }
  catch (error) { console.log('_drupalgap_back_exit - ' + error); }
}

$(window).on('navigate', function(event, data) {

    // If we're already moving backwards (aka from a "soft" back button click),
    // then don't do anything.
    if (drupalgap.back) { return; }

    // In web-app mode, clicking the back button on your browser (or Android
    // device browser), does not actually fire drupalgap_back(), so we mimic
    // it here, but skip the history.back() call because that's already
    // happened via the hardware button.
    if (drupalgap.settings.mode == 'web-app') {
      // Grab the direction we're travelling.
      // back, forward (or undefined, aka moving from splash to front page)
      var direction = data.state.direction;
      if (direction == 'back' && drupalgap.back_path.length > 0) {
        // @WARNING - any changes here should be reflected into _drupalgap_back().
        drupalgap.back = true;
        // Update the path and router path.
        var from = drupalgap_path_get();
        drupalgap_path_set(drupalgap.back_path.pop());
        var to = drupalgap_path_get();
        drupalgap_router_path_set(drupalgap_get_menu_link_router_path(to));
        module_invoke_all('drupalgap_back', from, to);
      }
    }

});


/**
 * Execute the page callback associated with the current path and return its
 * content.
 * @return {Object}
 */
function menu_execute_active_handler() {
  try {

    // Determine the path and then grab the page id.
    var path = null;
    if (arguments[0]) { path = arguments[0]; }
    if (!path) { path = drupalgap_path_get(); }
    var page_id = drupalgap_get_page_id(path);

    // @todo - Make sure the user has access to this DrupalGap menu path!

    // Get the router path.
    var router_path = drupalgap_router_path_get();

    if (!router_path) { return; }

    // Call the page call back for this router path and send along any
    // arguments.
    var function_name = drupalgap.menu_links[router_path].page_callback;
    var page_arguments = [];
    if (drupalgap_function_exists(function_name)) {

      // Grab the page callback function and get ready to build the html.
      var fn = window[function_name];
      var content = '';

      // Are there any arguments to send to the page callback?
      if (drupalgap.menu_links[router_path].page_arguments) {
        // For each page argument, if the argument is an integer, grab the
        // corresponding arg(#), otherwise just push the arg onto the page
        // arguments. Then try to prepare any entity that may be present in
        // the url so the entity is sent via the page arguments to the page
        // callback, instead of just sending the integer.
        var args = arg(null, path);
        for (var index in drupalgap.menu_links[router_path].page_arguments) {
            if (!drupalgap.menu_links[router_path].page_arguments.hasOwnProperty(index)) { continue; }
            var object = drupalgap.menu_links[router_path].page_arguments[index];
            if (is_int(object) && args[object]) {
              page_arguments.push(args[object]);
            }
            else { page_arguments.push(object); }
        }

        // Call the page callback function with the page arguments.
        content = fn.apply(null, Array.prototype.slice.call(page_arguments));
      }
      else {
        // There are no arguments, just return the page callback result.
        content = fn();
      }

      // If the content came back as a string, convert it to a render object
      // so any jQM page events can be attached to the content if necessary.
      if (typeof content === 'string') {
        content = {
          content: {
            markup: content
          }
        };
      }

      // Clear out any previous jQM page events, then if there are any jQM
      // event callback functions attached to the menu link for this page, set
      // each event up to be fired with inline JS on the page. We pass along
      // any page arguments to the jQM event handler function.
      drupalgap.page.jqm_events = [];
      var jqm_page_events = drupalgap_jqm_page_events();
      var jqm_page_event_args = null;
      if (page_arguments.length > 0) {
        jqm_page_event_args = JSON.stringify(page_arguments);
      }
      for (var i = 0; i < jqm_page_events.length; i++) {
        if (drupalgap.menu_links[router_path][jqm_page_events[i]]) {
          var jqm_page_event = jqm_page_events[i];
          var jqm_page_event_callback =
            drupalgap.menu_links[router_path][jqm_page_event];
          if (drupalgap_function_exists(jqm_page_event_callback)) {
            var options = {
              'page_id': page_id,
              'jqm_page_event': jqm_page_event,
              'jqm_page_event_callback': jqm_page_event_callback,
              'jqm_page_event_args': jqm_page_event_args
            };
            content[jqm_page_event] = {
              markup: drupalgap_jqm_page_event_script_code(options)
            };
          }
          else {
            console.log(
              'menu_execute_active_handler (' + path + ') - the jQM ' +
              jqm_page_event + ' call back function ' +
              jqm_page_event_callback + ' does not exist!'
            );
          }
        }
      }

      // Add a pageshow handler for the page title.
      if (typeof content === 'object') {
        var options = {
          'page_id': page_id,
          'jqm_page_event': 'pageshow',
          'jqm_page_event_callback': '_drupalgap_page_title_pageshow',
          'jqm_page_event_args': jqm_page_event_args
        };
        content['drupalgap_page_title_pageshow'] = {
          markup: drupalgap_jqm_page_event_script_code(options)
        };
      }

      // And finally return the content.
      return content;
    }
    else {
      // No page call back specified.
      console.log(
        'menu_execute_active_handler - no page callback (' + router_path + ')'
      );
      console.log(JSON.stringify(drupalgap.menu_links[router_path]));
    }
  }
  catch (error) {
    console.log('menu_execute_active_handler(' + path + ') - ' + error);
  }
}

/**
 * Gets a router item.
 * @return {Object}
 */
function menu_get_item() {
  try {
    var path = null;
    var router_item = null;
    if (arguments[0]) { path = arguments[0]; }
    if (arguments[1]) { router_item = arguments[1]; }
    if (path && drupalgap.menu_links[path]) {
      return drupalgap.menu_links[path];
    }
    else { return null; }
  }
  catch (error) { console.log('menu_get_item - ' + error); }
}

/**
 * Returns default menu item options.
 * @return {Object}
 */
function menu_item_default_options() {
    return { attributes: menu_item_default_attributes() };
}

/**
 * Returns default menu item attributes.
 * @return {Object}
 */
function menu_item_default_attributes() {
    return { 'class': '' };
}

/**
 * Returns an array containing the names of system-defined (default) menus.
 * @return {Object}
 */
function menu_list_system_menus() {
  try {
    var system_menus = {
      'user_menu_anonymous': {
        'title': t('User menu authenticated')
      },
      'user_menu_authenticated': {
        'title': t('User menu authenticated')
      },
      'main_menu': {
        'title': t('Main menu')
      },
      'primary_local_tasks': {
        'title': t('Primary Local Tasks')
      }
    };
    // Add the menu_name to each menu as a property.
    for (var menu_name in system_menus) {
        if (!system_menus.hasOwnProperty(menu_name)) { continue; }
        var menu = system_menus[menu_name];
        menu.menu_name = menu_name;
    }
    return system_menus;
  }
  catch (error) { console.log('menu_list_system_menus - ' + error); }
}

/**
 * Collects and alters the menu definitions.
 */
function menu_router_build() {
  //  Calls all hook_menu implementations and builds a collection of menu links.
  try {
    // For each module that implements hook_menu, iterate over each of the
    // menu links defined by the hook, then add each menu item to
    // drupalgap.menu_links keyed by the path.
    var modules = module_implements('menu');
    var function_name;
    var fn;
    var menu_links;
    for (var index in modules) {
        if (!modules.hasOwnProperty(index)) { continue; }
        var module = modules[index];
        // Determine the hook function name, grab the function, and call it
        // to retrieve the hook's menu links.
        function_name = module + '_menu';
        fn = window[function_name];
        menu_links = fn();
        // Iterate over each item.
        for (var path in menu_links) {
            if (!menu_links.hasOwnProperty(path)) { continue; }
            var menu_item = menu_links[path];
            // Attach module name to item.
            menu_item.module = module;
            // Set a default type for the item if one isn't provided.
            if (typeof menu_item.type === 'undefined') {
              menu_item.type = 'MENU_NORMAL_ITEM';
            }
            // Set default options and attributes if none have been provided.
            // @TODO - Now that we are doing this here, there may be a few
            // places throughout the code that were checking for these
            // undefined objects that will no longer need to be checked.
            if (typeof menu_item.options === 'undefined') {
              menu_item.options = menu_item_default_options();
            }
            else if (typeof menu_item.options.attributes === 'undefined') {
              menu_item.options.attributes = menu_item_default_attributes();
            }
            // Make the path available as a property in the menu link.
            menu_item.path = path;
            // Determine any parent, sibling, and child paths for the item.
            drupalgap_menu_router_build_menu_item_relationships(
              path,
              menu_item
            );
            // Attach item to menu links.
            drupalgap.menu_links[path] = menu_item;
        }
    }
  }
  catch (error) { console.log('menu_router_build - ' + error); }
}

/**
 * Given a menu link path, this determines and returns the router path as a
 * string.
 * @param {String} path
 * @return {String}
 */
function drupalgap_get_menu_link_router_path(path) {
  try {

    // @TODO - Why is this function called twice sometimes? E.G. via an MVC item
    // view item/local_users/user/0, this function gets called twice in one page
    // load, that can't be good.

    // @TODO - this function has a limitation in the types of menu paths it can
    // handle, for example a menu path of 'collection/%/%/list' with a path of
    // 'collection/local_users/user/list' can't find eachother. So we had to
    // change the mvc_menu() item path to be collection/list/%/%.

    // @TODO - each time this function is called, we should create a static
    // record of the result router path, keyed by the incoming path, that way
    // this heavy function can be called more often with less resource.

    // Is this path defined in drupalgap.menu_links? If it is, use it's router
    // path if it is defined, otherwise just set its router path to its own
    // path.
    if (drupalgap.menu_links[path]) {
      if (typeof drupalgap.menu_links[path].router_path === 'undefined') {
        return path;
      }
      else {
        return drupalgap.menu_links[path].router_path;
      }
    }

    // Let's figure out where to route this menu item, and attach the
    // router to the item.
    var router_path = null;
    var args = arg(null, path);

    // If there is an integer in the path, replace it with the wildcard
    // defined via hook_menu implementation.
    if (args) {
      var args_size = args.length;
      switch (args[0]) {
        case 'comment':
        case 'user':
        case 'node':
          if (args_size > 1 && is_int(parseInt(args[1]))) {
            args[1] = '%';
            router_path = args.join('/');
          }
          break;
        case 'taxonomy':
          if (args_size > 2 && (args[1] == 'vocabulary' || args[1] == 'term') &&
              is_int(parseInt(args[2]))
          ) {
            args[2] = '%';
            router_path = args.join('/');
          }
          break;
        default:
          if (args_size > 1 && is_int(parseInt(args[1]))) {
            args[1] = '%';
            router_path = args.join('/');
          }
          break;
      }
    }

    // If we haven't found a router path yet, try some other techniques to find
    // it. If all else fails, just set the router path to the path itself.
    if (!router_path) {
      // Are there any paths in drupalgap.menu_links that would be good
      // candidates as the router for this path? Let's start at the back of the
      // argument list and start replacing each with a wildcard (%) and then
      // see if there is a path in drupalgap.menu_links that can handle it.
      if (args && args.length > 1) {
        var temp_router_path;
        for (var i = args.length - 1; i != -1; i--) {
          temp_router_path = '';
          for (var j = 0; j < args.length; j++) {
            if (j < i) {
              temp_router_path += args[j];
            }
            else {
              temp_router_path += '%';
            }
            if (j != args.length - 1) {
              temp_router_path += '/';
            }
          }
          // If we found a router path, let's use it.
          if (drupalgap.menu_links[temp_router_path]) {
            router_path = temp_router_path;
            break;
          }
        }
      }
    }

    // If the router path is a default menu local task, inherit its parent
    // router path.
    if (drupalgap.menu_links[router_path] &&
        drupalgap.menu_links[router_path].type == 'MENU_DEFAULT_LOCAL_TASK' &&
        drupalgap.menu_links[router_path].parent) {
      router_path = drupalgap.menu_links[router_path].parent;
    }

    // If there isn't a router and we couldn't find one, we'll just route
    // to the path itself.
    if (!router_path) { router_path = path; }

    // Finally, return the router path.
    return router_path;
  }
  catch (error) {
    console.log('drupalgap_get_menu_link_router_path - ' + error);
  }
}


/**
 * Loads all of the menus specified in drupalgap.settings.menus into
 * drupalgap.menus. This is called after menu_router_build(), so any system
 * defined menus will already be present and should be overwritten with any
 * customizations present in the settings. It then iterates over the menu links
 * specified in drupalgap.menu_links and attaches any of them that have a
 * menu_name to their corresponding menu in drupalgap.menus. Any menu link items
 * that have a 'region' property specified will be added to
 * drupalgap.theme.regions[region].
 */
function drupalgap_menus_load() {
  try {
    if (drupalgap.settings.menus) {
      // Process each menu defined in the settings.
      for (var menu_name in drupalgap.settings.menus) {
          if (!drupalgap.settings.menus.hasOwnProperty(menu_name)) { continue; }
          var menu = drupalgap.settings.menus[menu_name];
          // If the menu does not already exist, it is a custom menu, so create
          // the menu and its corresponding block.
          if (!drupalgap.menus[menu_name]) {
            // If the custom menu doesn't have its machine name set, set it.
            if (!menu.menu_name) { menu.menu_name = menu_name; }
            // Save the custom menu, as long is its name isn't 'regions',
            // because that is a special "system" menu that allows menu links to
            // be placed directly in regions via settings.js. Keep in mind the
            // 'regions' menu is in fact NOT a system menu.
            if (menu_name != 'regions') {
              menu_save(menu);
              // Make a block for this custom menu.
              var block_delta = menu.menu_name;
              drupalgap.blocks[0][block_delta] = {
                name: block_delta,
                delta: block_delta,
                module: 'menu'
              };
            }
          }
          else {
            // The menu is a system defined menu, merge it together with any
            // custom settings.
            $.extend(true, drupalgap.menus[menu_name], menu);
          }
      }
      // Now that we have all of the menus loaded up, and the menu router is
      // built, let's iterate over all the menu links and perform various
      // operations on them.
      for (var path in drupalgap.menu_links) {
          if (!drupalgap.menu_links.hasOwnProperty(path)) { continue; }
          var menu_link = drupalgap.menu_links[path];
          // Let's grab any links from the router that have a menu specified,
          // and add the link to the router.
          if (menu_link.menu_name) {
            if (drupalgap.menus[menu_link.menu_name]) {
              // Create a links array for the menu if one doesn't exist already.
              if (!drupalgap.menus[menu_link.menu_name].links) {
                drupalgap.menus[menu_link.menu_name].links = [];
              }
              // Add the path to the menu link inside the menu.
              menu_link.path = path;
              // Now push the link onto the menu. We only care about the title,
              // path and options, as this is just a link. The rest of the
              // menu link data can be retrieved from drupalgap.menu_links.
              var link =
                drupalgap_menus_load_convert_menu_link_to_link_json(menu_link);
              drupalgap.menus[menu_link.menu_name].links.push(link);
            }
            else {
              console.log(
                'drupalgap_menus_load - menu does not exist (' +
                menu_link.menu_name + '), cannot attach link to it (' +
                path + ')'
              );
            }
          }
          // If the menu link is set to a specific region, create a links array
          // for the region if one doesn't exist already, then add the menu item
          // to the links array as a link.
          if (menu_link.region) {
            if (!drupalgap.theme.regions[menu_link.region.name].links) {
              drupalgap.theme.regions[menu_link.region.name].links = [];
            }
            drupalgap.theme.regions[menu_link.region.name].links.push(
              menu_link
            );
          }
      }
      // If there are any region menu links defined in settings.js, create a
      // links array for the region if one doesn't exist already, then add the
      // menu item to the links array as a link.
      if (typeof drupalgap.settings.menus.regions !== 'undefined') {
        for (var region in drupalgap.settings.menus.regions) {
            if (!drupalgap.settings.menus.regions.hasOwnProperty(region)) { continue; }
            var menu = drupalgap.settings.menus.regions[region];
            if (
              typeof menu.links !== 'undefined' &&
              $.isArray(menu.links) &&
              menu.links.length > 0
            ) {
              if (!drupalgap.theme.regions[region].links) {
                drupalgap.theme.regions[region].links = [];
              }
              for (var index in menu.links) {
                  if (!menu.links.hasOwnProperty(index)) { continue; }
                  var link = menu.links[index];
                  drupalgap.theme.regions[region].links.push(link);
              }
            }
        }
      }
    }
  }
  catch (error) { console.log('drupalgap_menus_load - ' + error); }
}

/**
 * Given a menu link item from drupalgap_menus_load(), this will return a JSON
 * object representing a link object compatable with theme_link(). It contains
 * the link title, path and options.
 * @param {Object} menu_link
 * @return {Object}
 */
function drupalgap_menus_load_convert_menu_link_to_link_json(menu_link) {
  try {
    var link = {};
    if (menu_link.title) {
      // TODO - this is strange, we have to fill the 'text' value so theme_link
      // will play nice. These two properties, and their usage, need a thorough
      // review, only one should probably be used.
      // UPDATE - the link.text property is probably no longer used, it should
      // be safe to get rid of. In fact, this whole function is dumb and should
      // go away.
      link.title = menu_link.title;
      link.text = menu_link.title;
    }
    if (menu_link.path) { link.path = menu_link.path; }
    if (menu_link.options) { link.options = menu_link.options; }
    // If it is a menu link on a region, and it has options, set the link
    // options to the ones provided in the menu link region settings.
    if (menu_link.region && menu_link.region.options) {
      link.options = menu_link.options = menu_link.region.options;
    }
    return link;
  }
  catch (error) {
    console.log(
      'drupalgap_menus_load_convert_menu_link_to_link_json - ' + error
    );
  }
}


/**
 * Given a path, and its corresponding menu item, this will determine any
 * parent, sibling, and/or child menu item paths and set the references on each
 * so they are all aware of eachother's paths.
 * @param {String} path
 * @param {Object} menu_item
 */
function drupalgap_menu_router_build_menu_item_relationships(path, menu_item) {
  try {
    // Split up the path arguments.
    var args = arg(null, path);
    // Any parent?
    if (args.length > 1) {
      // Set the parent path.
      var parent = args.splice(0, args.length - 1).join('/');
      menu_item.parent = parent;
      // Make sure the parent exists.
      if (drupalgap.menu_links[parent]) {
        // Now tell the parent about this child. If the parent doesn't yet have
        // any children, setup the children array on the parent.
        if (typeof drupalgap.menu_links[parent].children === 'undefined') {
          drupalgap.menu_links[parent].children = [];
        }
        drupalgap.menu_links[parent].children.push(path);
        // Now tell any siblings about this item, and tell this item about any
        // siblings.
        if (typeof menu_item.siblings === 'undefined') {
          menu_item.siblings = [];
        }
        for (var index in drupalgap.menu_links[parent].children) {
            if (!drupalgap.menu_links[parent].children.hasOwnProperty(index)) { continue; }
            var sibling = drupalgap.menu_links[parent].children[index];
            if (sibling != path && drupalgap.menu_links[sibling]) {
              if (
                typeof drupalgap.menu_links[sibling].siblings === 'undefined'
              ) {
                drupalgap.menu_links[sibling].siblings = [];
              }
              drupalgap.menu_links[sibling].siblings.push(path);
              menu_item.siblings.push(sibling);
            }
        }
      }
    }
  }
  catch (error) {
    console.log('drupalgap_menu_router_build_relationships - ' + error);
  }
}


/**
 * Show the jQueryMobile loading message.
 * @see http://stackoverflow.com/a/16277865/763010
 */
function drupalgap_loading_message_show() {
  try {
    // Backwards compatibility for versions prior to 7.x-1.6-alpha
    if (drupalgap.loading === 'undefined') { drupalgap.loading = false; }
    // Return if the loading message is already shown.
    if (drupalgap.loading || drupalgap_toast_is_shown()) { return; }
    var options = drupalgap_loader_options();
    if (arguments[0]) { options = arguments[0]; }
    // Show the loading message.
    //$.mobile.loading('show', options);
    //drupalgap.loading = true;
    setTimeout(function() {
      $.mobile.loading('show', options);
      drupalgap.loading = true;
    }, 1);
  }
  catch (error) { console.log('drupalgap_loading_message_show - ' + error); }
}

/**
 * Hide the jQueryMobile loading message.
 */
function drupalgap_loading_message_hide() {
  try {
    if (drupalgap_toast_is_shown()) { return; }
    setTimeout(function() {
      $.mobile.loading('hide');
      drupalgap.loading = false;
      drupalgap.loader = 'loading';
    }, 100);
  }
  catch (error) { console.log('drupalgap_loading_message_hide - ' + error); }
}

/**
 * Returns the jQM loader options based on the current mode and settings.js.
 * @return {Object}
 */
function drupalgap_loader_options() {
  try {
    var mode = drupalgap.loader;
    var text = t('Loading') + '...';
    var textVisible = true;
    if (mode == 'saving') { var text = t('Saving') + '...'; }
    var options = {
      text: text,
      textVisible: textVisible
    };
    if (drupalgap.settings.loader && drupalgap.settings.loader[mode]) {
      options = $.extend(true, options, drupalgap.settings.loader[mode]);
      if (options.text) { options.text = t(options.text); }
    }
    return options;
  }
  catch (error) { console.log('drupalgap_loader_options - ' + error); }
}

/**
 * Sets a message to display to the user. Optionally pass in a second argument
 * to specify the message type: status, warning, error
 * @param {String} message
 */
function drupalgap_set_message(message) {
  try {
    if (empty(message)) { return; }
    var type = 'status';
    if (arguments[1]) { type = arguments[1]; }
    var msg = {
      message: message,
      type: type
    };
    drupalgap.messages.push(msg);
  }
  catch (error) { console.log('drupalgap_set_message - ' + error); }
}

/**
 * Sets the current messages.
 * @param {Array} messages
 */
function drupalgap_set_messages(messages) {
  try {
    drupalgap.messages = messages;
  }
  catch (error) { console.log('drupalgap_set_messages - ' + error); }
}

/**
 * Returns the current messages.
 * @return {Array}
 */
function drupalgap_get_messages() {
  try {
    return drupalgap.messages;
  }
  catch (error) { console.log('drupalgap_get_messages - ' + error); }
}

/**
 * Clears the messages from the current page. Optionally pass in a page id to
 * clear messages from a particular page.
 */
function drupalgap_clear_messages() {
  try {
    var page_id = arguments[0];
    if (empty(page_id)) { page_id = drupalgap_get_page_id(); }
    $('#' + page_id + ' div.messages').remove();
  }
  catch (error) { console.log('drupalgap_clear_messages - ' + error); }
}

/**
 * Alerts a message to the user using PhoneGap's alert. It is important to
 * understand this is an async function, so code will continue to execute while
 * the alert is displayed to the user.
 * You may optionally pass in a second argument as a JSON object with the
 * following properties:
 *   alertCallback - the function to call after the user presses OK
 *   title - the title to use on the alert box, defaults to 'Alert'
 *   buttonName - the text to place on the button, default to 'OK'
 * @param {String} message
 */
function drupalgap_alert(message) {
  try {
    var options = null;
    if (arguments[1]) { options = arguments[1]; }
    var alertCallback = function() { };
    var title = t('Alert');
    var buttonName = t('OK');
    if (options) {
      if (options.alertCallback) { alertCallback = options.alertCallback; }
      if (options.title) { title = options.title; }
      if (options.buttonName) { buttonName = options.buttonName; }
    }
    if (
      drupalgap.settings.mode != 'phonegap' ||
      typeof navigator.notification === 'undefined'
    ) {
      alert(message);
      alertCallback();
    }
    else {
      navigator.notification.alert(message, alertCallback, title, buttonName);
    }
  }
  catch (error) { console.log('drupalgap_alert - ' + error); }
}

/**
 * Displays a confirmation message to the user using PhoneGap's confirm. It is
 * important to understand this is an async function, so code will continue to
 * execute while the confirmation is displayed to the user.
 * You may optionally pass in a second argument as a JSON object with the
 * following properties:
 *   confirmCallback - the function to call after the user presses a button, the
 *               button's label is passed to this function.
 *   title - the title to use on the alert box, defaults to 'Confirm'
 *   buttonLabels - the text to place on the OK, and Cancel buttons, separated
 *                  by comma.
 * @param {String} message
 * @return {Boolean}
 */
function drupalgap_confirm(message) {
  try {
    var options = null;
    if (arguments[1]) { options = arguments[1]; }
    var confirmCallback = function(button) { };
    var title = t('Confirm');
    var buttonLabels = [t('OK'), t('Cancel')];
    if (options) {
      if (options.confirmCallback) {
        confirmCallback = options.confirmCallback;
      }
      if (options.title) { title = options.title; }
      if (options.buttonLabels) { buttonLabels = options.buttonLabels; }
    }
    // The phonegap confirm dialog doesn't seem to work in Ripple, so just use
    // the default one, and it definitely doesn't work in a web app, so
    // otherwise just use the default confirm.
    if (
      typeof parent.window.ripple === 'function' ||
      drupalgap.settings.mode == 'web-app'
    ) {
      var r = confirm(message);
      if (r == true) { confirmCallback(1); } // OK button.
      else { confirmCallback(2); } // Cancel button.
    }
    else {
      navigator.notification.confirm(
        message,
        confirmCallback,
        title,
        buttonLabels
      );
    }
    return false;
  }
  catch (error) { console.log('drupalgap_confirm - ' + error); }
}

/**
 * Show a non intrusive alert message. You may optionally pass in an
 * integer value as the second argument to specify how many milliseconds
 * to wait before closing the message. Likewise, you can pass in a
 * third argument to specify how long to wait before opening the
 * message.
 * @param {string} html - The html to display.
 */
function drupalgap_toast(html) {
  try {
    var open = arguments[2] ? arguments[2] : 750;
    var close = arguments[1] ? arguments[1] : 1500;
    setTimeout(function() {
      drupalgap.toast.shown = true;
      $.mobile.loading('show', {
        textVisible: true,
        html: html
      });
      var interval = setInterval(function () {
        $.mobile.loading('hide');
        drupalgap.toast.shown = false;
        clearInterval(interval);
      }, close);
    }, open);
  }
  catch (error) {
    console.log('drupalgap_toast - ' + error);
  }
}

/**
 * Returns true if the toast is currently shown, false otherwise.
 * @returns {Boolean}
 */
function drupalgap_toast_is_shown() {
  return drupalgap.toast.shown;
}

/**
 * This will return the query string arguments for the page. You may optionally
 * pass in a key to get its value, pass in a key then a value to set the key
 * equal to the value, and you may optionally pass in a third argument to use
 * a specific page id, otherwise DrupalGap will automatically use the
 * appropriate page id.
 * @return {String|NULL}
 */
function _GET() {
  try {

    // Set up defaults.
    var get = false;
    var set = false;
    var key = null;
    var value = null;

    // Are we setting? If so, grab the value and key to set.
    if (typeof arguments[1] !== 'undefined') {
      set = true;
      value = arguments[1];
      if (typeof arguments[0] !== 'undefined') { key = arguments[0]; }
      else {
        console.log('WARNING: _GET - missing key for value (' + value + ')');
        return null;
      }
    }

    // Are we getting a certain value? If so, grab the key to get.
    else if (typeof arguments[0] !== 'undefined') {
      get = true;
      key = arguments[0];
    }

    // Otherwise we are getting the whole page.
    else { get = true; }

    // Now perform the get or set...

    // Get.
    if (get) {

      // If a page id was provided use it, otherwise use the current page's id.
      var id = null;
      if (typeof arguments[2] !== 'undefined') { id = arguments[2]; }
      else { id = drupalgap_get_page_id(); }

      // Now that we know the page id, lets return the value if a key was
      // provided, otherwise return the whole query string object for the page.
      if (typeof _dg_GET[id] !== 'undefined') {
        if (!key) { return _dg_GET[id]; }
        else if (typeof _dg_GET[id][key] !== 'undefined') {
          return _dg_GET[id][key];
        }
        return null;
      }

    }

    // Set.
    else if (set) {

      // If we were given a path, use its page id as the property index, other
      // wise we'll use the current page (which is different than the
      // destination page!).
      var id = null;
      if (typeof arguments[2] !== 'undefined') {
        id = drupalgap_get_page_id(arguments[2]);
      }
      else { id = drupalgap_get_page_id(); }

      // If the id hasn't been instantiated, do so. Then set the key and value
      // onto it.
      if (typeof _dg_GET[id] === 'undefined') { _dg_GET[id] = {}; }
      if (value) { _dg_GET[id][key] = value; }

    }
    return null;
  }
  catch (error) { console.log('_GET - ' + error); }
}

/**
 * Each time we use drupalgap_goto to change a page, this function is called on
 * the pagebeforehange event. If we're not moving backwards, or navigating to
 * the same page, this will preproccesses the page, then processes it.
 */
$(document).on('pagebeforechange', function(e, data) {
    try {
      // If we're moving backwards, reset drupalgap.back and return.
      if (drupalgap && drupalgap.back) {
        drupalgap.back = false;
        return;
      }
      // If the jqm active page url is the same as the page id of the current
      // path, return.
      if (
        drupalgap_jqm_active_page_url() ==
        drupalgap_get_page_id(drupalgap_path_get())
      ) { return; }
      // We only want to process the page we are going to, not the page we are
      // coming from. When data.toPage is a string that is our destination page.
      if (typeof data.toPage === 'string') {

        // If drupalgap_goto() determined that it is necessary to prevent the
        // default page from reloading, then we'll skip the page
        // processing and reset the prevention boolean.
        if (drupalgap && !drupalgap.page.process) {
          drupalgap.page.process = true;
        }
        else if (drupalgap) {
          // Pre process, then process the page.
          template_preprocess_page(drupalgap.page.variables);
          template_process_page(drupalgap.page.variables);
        }

      }
    }
    catch (error) { console.log('pagebeforechange - ' + error); }
});

/**
 * Implementation of template_preprocess_page().
 * @param {Object} variables
 */
function template_preprocess_page(variables) {
  try {
    // Set up default attributes for the page's div container.
    if (typeof variables.attributes === 'undefined') {
      variables.attributes = {};
    }

    // @todo - is this needed?
    // @UPDATE - this should be used, but these page attributes are ignored
    // by drupalgap_add_page_to_dom()!
    variables.attributes['data-role'] = 'page';

    module_invoke_all('preprocess_page', variables);

    // Place the variables into drupalgap.page
    drupalgap.page.variables = variables;
  }
  catch (error) { console.log('template_preprocess_page - ' + error); }
}

/**
 * Implementation of template_process_page().
 * @param {Object} variables
 */
function template_process_page(variables) {
  try {
    var drupalgap_path = drupalgap_path_get();
    // Execute the active menu handler to assemble the page output. We need to
    // do this before we render the regions below.
    drupalgap.output = menu_execute_active_handler();
    // For each region, render it, then replace the placeholder in the page's
    // html with the rendered region.
    var page_id = drupalgap_get_page_id(drupalgap_path);
    var page = $('#' + page_id);
    var page_html = $(page).html();
    if (!page_html) { return; }
    for (var index in drupalgap.theme.regions) {
        if (!drupalgap.theme.regions.hasOwnProperty(index)) { continue; }
        var region = drupalgap.theme.regions[index];
        var _region = {};
        $.extend(true, _region, region);
        page_html = page_html.replace(
          '{:' + region.name + ':}',
          drupalgap_render_region(_region)
        );
    }
    $(page).html(page_html);
    module_invoke_all('post_process_page', variables);
  }
  catch (error) { console.log('template_process_page - ' + error); }
}

/**
 * Given a path, this will return the id for the page's div element.
 * For example, a string path of 'foo/bar' would result in an id of 'foo_bar'.
 * If no path is provided, it will return the current page's id.
 * @param {String} path
 * @return {String}
 */
function drupalgap_get_page_id(path) {
  try {
    if (!path) { path = drupalgap_path_get(); }
    var id = path.toLowerCase().replace(/\//g, '_').replace(/-/g, '_');
    return id;
  }
  catch (error) { console.log('drupalgap_get_page_id - ' + error); }
}

/**
 * Given a page id, the theme's page.tpl.html string, and the menu link object
 * (all bundled in options) this takes the page template html and adds it to the
 * DOM. It doesn't actually render the page, that is taken care of by the
 * pagebeforechange handler.
 * @param {Object} options
 */
function drupalgap_add_page_to_dom(options) {
  try {
    // Prepare the default page attributes, then merge in any customizations
    // from the hook_menu() item, then inject the attributes into the
    // placeholder. We have to manually add our default class name after the
    // extend until this issue is resolved:
    // https://github.com/signalpoint/DrupalGap/issues/321
    var attributes = {
      id: options.page_id,
      'data-role': 'page'
    };
    attributes =
      $.extend(true, attributes, options.menu_link.options.attributes);
    attributes['class'] +=
      ' ' + drupalgap_page_class_get(drupalgap.router_path);
    options.html = options.html.replace(
      /{:drupalgap_page_attributes:}/g,
      drupalgap_attributes(attributes)
    );
    // Add the html to the page and the page id to drupalgap.pages.
    $('body').append(options.html);
    drupalgap.pages.push(options.page_id);
  }
  catch (error) { console.log('drupalgap_add_page_to_dom - ' + error); }
}

/**
 * Attempts to remove given page from the DOM, will not remove the current page.
 * You may force the removal by passing in a second argument as a JSON object
 * with a 'force' property set to true. You may pass in a third argument to
 * specify the current page, otherwise it will default to what DrupalGap thinks
 * is the current page. No matter what, the current page (specified or not)
 * can't be removed from the DOM, because jQM always needs one page in the DOM.
 * @param {String} page_id
 */
function drupalgap_remove_page_from_dom(page_id) {
  try {
    var current_page_id = null;
    if (typeof arguments[2] !== 'undefined') { current_page_id = arguments[2]; }
    else { current_page_id = drupalgap_get_page_id(drupalgap_path_get()); }
    var options = {};
    if (typeof arguments[1] !== 'undefined') { options = arguments[1]; }
    if (current_page_id != page_id || options.force) {
      var currentPage = $('#' + current_page_id);
      // Preserve and re-apply style to current page, @see https://github.com/signalpoint/DrupalGap/issues/837
      var style = $(currentPage).attr('style');
      $('#' + page_id).empty().remove();
      if (style) { $(currentPage).attr('style', style); }
      var page_index = drupalgap.pages.indexOf(page_id);
      if (page_index > -1) { drupalgap.pages.splice(page_index, 1); }
      // We'll remove the query string, unless we were instructed to leave it.
      if (
        typeof _dg_GET[page_id] !== 'undefined' &&
        (typeof options.leaveQuery === 'undefined' || !options.leaveQuery)
      ) { delete _dg_GET[page_id]; }
      // Remove any embedded view for the page.
      views_embedded_view_delete(page_id);
    }
    else {
      console.log('WARNING: drupalgap_remove_page_from_dom() - not removing ' +
        'the current page (' + page_id + ') from the DOM!');
    }
  }
  catch (error) { console.log('drupalgap_remove_page_from_dom - ' + error); }
}

/**
 * Removes all pages from the DOM except the current one.
 */
function drupalgap_remove_pages_from_dom() {
  try {
    var current_page_id = drupalgap_get_page_id(drupalgap_path_get());
    var pages = drupalgap.pages.slice(0);
    for (var index in pages) {
        if (!pages.hasOwnProperty(index)) { continue; }
        var page_id = pages[index];
        if (current_page_id != page_id) {
          drupalgap_remove_page_from_dom(page_id, null, current_page_id);
        }
    }
    // Reset drupalgap.pages to only contain the current page id.
    drupalgap.pages = [current_page_id];
    // Reset the drupalgap.views.ids array.
    drupalgap.views.ids = [];
    // Reset the jQM page events.
    drupalgap.page.jqm_events = [];
    // Reset the back path.
    drupalgap.back_path = [];
  }
  catch (error) { console.log('drupalgap_remove_pages_from_dom - ' + error); }
}

/**
 * Given a router path, this will return the CSS class name that can be used for
 * the page container.
 * @param {String} router_path The page router path.
 * @return {String} A css class name.
 */
function drupalgap_page_class_get(router_path) {
  try {
    // Replace '/' and '%' with underscores, then trim any trailing underscores.
    var class_name = router_path.replace(/[\/%]/g, '_');
    while (class_name.lastIndexOf('_') == class_name.length - 1) {
      class_name = class_name.substr(0, class_name.length - 1);
    }
    return class_name;
  }
  catch (error) { console.log('drupalgap_page_class_get - ' + error); }
}

/**
 * Returns true if the given page id's page div already exists in the DOM.
 * @param {String} page_id
 * @return {Boolean}
 */
function drupalgap_page_in_dom(page_id) {
  try {
    var pages = $("body div[data-role$='page']");
    var page_in_dom = false;
    if (pages && pages.length > 0) {
      for (var index in pages) {
          if (!pages.hasOwnProperty(index)) { continue; }
          var page = pages[index];
          if (($(page).attr('id')) == page_id) {
            page_in_dom = true;
            break;
          }
      }
    }
    return page_in_dom;
  }
  catch (error) { console.log('drupalgap_page_in_dom - ' + error); }
}

/**
 * Returns true if the current page is the front page, false otherwise.
 * @return {Boolean}
 */
function drupalgap_is_front_page() {
  try {
    return drupalgap_path_get() == drupalgap.settings.front;
  }
  catch (error) { console.log('drupalgap_is_front_page - ' + error); }
}

/**
 * Returns the URL of the active jQuery Mobile page.
 * @return {String}
 */
function drupalgap_jqm_active_page_url() {
  try {
    // WARNING: when the app first loads, this value may be much different than
    // you expect. It certainly is not the front page path, because on Android
    // for example it returns '/android_asset/www/index.html'. Also, when the
    // app first loads, activePage is null, so just return an empty string.
    if (!$.mobile.activePage) { return ''; }
    return $.mobile.activePage.data('url');
  }
  catch (error) { console.log('drupalgap_jqm_active_page_url - ' + error); }
}

/**
 * Renders the html string of the page content that is stored in
 * drupalgap.output.
 * @return {String}
 */
function drupalgap_render_page() {
  try {
    module_invoke_all('page_build', drupalgap.output);
    return drupalgap_render(drupalgap.output);
  }
  catch (error) { console.log('drupalgap_render_page - ' + error); }
}

/**
 * Given a region, this renders it and all the blocks in it. The blocks are
 * specified in the settings.js file, they are bundled under a region, which in
 * turn is bundled under a theme name. Returns an empty string if it fails.
 * @param {Object} region
 * @return {String}
 */
function drupalgap_render_region(region) {
  try {
    // @TODO - this function is getting huge. Break it up into many more
    // manageable functions.

    // Make sure there are blocks specified for this theme in settings.js.
    if (!drupalgap.settings.blocks[drupalgap.settings.theme]) {
      var msg = 'WARNING: drupalgap_render_region() - there are no blocks ' +
        'for the "' + drupalgap.settings.theme + '" theme in the settings.js ' +
        'file!';
      console.log(msg);
      return '';
    }

    // Grab the current path.
    var current_path = drupalgap_path_get();

    // Let's render the region...
    var region_html = '';

    region_html +=
      _drupalgap_region_render_zone('_prefix', region, current_path);

    // If the region has blocks specified for it in the theme in settings.js...
    if (drupalgap.settings.blocks[drupalgap.settings.theme][region.name]) {

      // If a class attribute hasn't yet been provided, set a default, then
      // append a system class name for the region onto its attributes array.
      if (!region.attributes['class']) { region.attributes['class'] = ''; }
      region.attributes['class'] += ' region_' + region.name + ' ';

      // Open the region container.
      region_html += '<div ' + drupalgap_attributes(region.attributes) + '>';

      // If there are any links attached to this region, render them first.
      var region_link_count = 0;
      var region_link_popup_count = 0;
      if (region.links && region.links.length > 0) {

        // Let's first iterate over all of the region links and keep counts of
        // any links that use the ui-btn-left and ui-btn-right class attribute.
        // This will allow us to properly wrap region links in a control group.
        var ui_btn_left_count = 0;
        var ui_btn_right_count = 0;
        for (var index in region.links) {
            if (!region.links.hasOwnProperty(index)) { continue; }
            var link = region.links[index];
            var data = menu_region_link_get_data(link);
            if (!drupalgap_check_visibility('region', data)) { continue; }
            region_link_count++;
            var css_class = drupalgap_link_get_class(link);
            if (css_class) {
              var side = menu_region_link_get_side(css_class);
              if (side == 'left') { ui_btn_left_count++; }
              else if (side == 'right') { ui_btn_right_count++; }
            }
        }

        // We need to separately render each side of the header (left, right).
        // That allows us to properly wrap the links with a control group if
        // it is needed.
        var region_link_html = '';
        var ui_btn_left_html = '';
        var ui_btn_right_html = '';
        for (var i = 0; i < region.links.length; i++) {

          // Grab the link and its data.
          var region_link = region.links[i];
          var data = menu_region_link_get_data(region_link);

          // Check link's region visiblity settings. Links will not be rendered
          // on certain system pages.
          // @TODO - this additional call to drupalgap_check_visibility() here
          // may be expensive, consider setting aside the results from the call
          // above, and using them here.
          if (drupalgap_check_visibility('region', data)) {

            // Don't render the link on certain system pages.
            if (in_array(current_path, ['offline', 'error', 'user/logout'])) {
              continue;
            }

            // If this is a popup region link, set the jQM attributes to make
            // this link function as a popup (dropdown) menu. Set the default
            // link icon, if it isn't set.
            var link_text = region_link.title;
            var link_path = region_link.path;
            if (data.options.popup) {

              region_link_popup_count++;

              // If the link text isn't set, and the data icon pos isn't set,
              // set it the data icon pos so the button and icon are rendered
              // properly.
              if (
                (!link_text || empty(link_text)) &&
                typeof data.options.attributes['data-iconpos'] === 'undefined'
              ) { data.options.attributes['data-iconpos'] = 'notext'; }

              // If data-rel, data-icon, data-role aren't set, set them.
              if (
                typeof data.options.attributes['data-rel'] === 'undefined'
              ) { data.options.attributes['data-rel'] = 'popup'; }
              if (
                typeof data.options.attributes['data-icon'] === 'undefined'
              ) { data.options.attributes['data-icon'] = 'bars'; }
              if (
                typeof data.options.attributes['data-role'] === 'undefined'
              ) { data.options.attributes['data-role'] = 'button'; }

              // Popup menus need a dynamic href value on the link, so we
              // always overwrite it.
              link_path = null;
              data.options.attributes['href'] =
                '#' + menu_container_id(data.options.popup_delta);
            }
            else {

              // Set the data-role to a button, if one isn't already set.
              if (typeof data.options.attributes['data-role'] === 'undefined') {
                data.options.attributes['data-role'] = 'button';
              }

            }

            // If it has notext for the icon position, force the text to be
            // an nbsp.
            if (data.options.attributes['data-iconpos'] == 'notext') {
              link_text = '&nbsp;';
            }

            // Render the link on the proper side.
            var css_class = drupalgap_link_get_class(region_link);
            var side = menu_region_link_get_side(css_class);
            var link_html = l(link_text, link_path, data.options);
            if (side == 'left') { ui_btn_left_html += link_html; }
            else if (side == 'right') { ui_btn_right_html += link_html; }

          }

        }

        // If there was more than one link on a side, wrap it in a control
        // group, and remove the ui-btn class from the links.
        if (ui_btn_left_count > 1) {
          var attrs = {
            'data-type': 'horizontal',
            'data-role': 'controlgroup',
            'class': 'ui-btn-left'
          };
          ui_btn_left_html = '<div ' + drupalgap_attributes(attrs) + '>' +
            ui_btn_left_html.replace(/ui-btn-left/g, '') +
          '</div>';
        }
        if (ui_btn_right_count > 1) {
          var attrs = {
            'data-type': 'horizontal',
            'data-role': 'controlgroup',
            'class': 'ui-btn-right'
          };
          ui_btn_right_html = '<div ' + drupalgap_attributes(attrs) + '>' +
            ui_btn_right_html.replace(/ui-btn-right/g, '') +
          '</div>';
        }

        // Finally render the ui sides on the region.
        region_html += ui_btn_left_html + ui_btn_right_html;
      }

      // Render each block in the region. Determine how many visible blocks are
      // in the region.
      var block_counts = {
        block_count: 0,
        block_menu_count: 0
      };
      var blocks = drupalgap.settings.blocks[drupalgap.settings.theme][region.name];
      for (var block_delta in blocks) {
          if (!blocks.hasOwnProperty(block_delta)) { continue; }
          var block_settings = blocks[block_delta];
          // Ignore region _prefix and _suffix.
          if (block_delta == '_prefix' || block_delta == '_suffix') { continue; }
          // Render the block.
          region_html += drupalgap_block_render(
            region,
            current_path,
            block_delta,
            block_settings,
            block_counts
          );
      }

      // If this was a header or footer, and there were only region links
      // rendered, place an empty header in the region.
      if (
        in_array(region.attributes['data-role'], ['header', 'footer']) &&
        (
          block_counts.block_count == 0 && region_link_count > 0 ||
          block_counts.block_count - block_counts.block_menu_count == 0
        ) ||
        (
          region_link_count > 0 &&
          region_link_popup_count >= block_counts.block_menu_count &&
          block_counts.block_count == 0
        )
      ) {
        // Show an empty header if we're not collapsing on an empty region.
        if (
          typeof region.collapse_on_empty === 'undefined' ||
          region.collapse_on_empty === false
        ) { region_html += '<h2>&nbsp;</h2>'; }
      }

      // Close the region container.
      region_html += '</div><!-- ' + region.name + ' -->';

    }

    region_html +=
      _drupalgap_region_render_zone('_suffix', region, current_path);

    return region_html;
  }
  catch (error) { console.log('drupalgap_render_region - ' + error); }
}

/**
 * Renders the given zone (_prefix, _suffix) if any for a region.
 * @param {String} zone
 * @param {Object} region
 * @param {String} current_path
 * @return {String}
 */
function _drupalgap_region_render_zone(zone, region, current_path) {
  try {
    var html = '';
    var theme_name = drupalgap.settings.theme;
    if (typeof drupalgap.settings.blocks[theme_name][region.name] ===
      'undefined'
    ) { return html; }
    var region_settings =
      drupalgap.settings.blocks[theme_name][region.name];
    if (typeof region_settings[zone] === 'undefined') { return html; }
    var blocks = region_settings[zone];
    for (var block_delta in blocks) {
        if (!blocks.hasOwnProperty(block_delta)) { continue; }
        var block_settings = blocks[block_delta];
        html += drupalgap_block_render(
          region,
          current_path,
          block_delta,
          block_settings
        );
    }
    return html;
  }
  catch (error) { console.log('_drupalgap_region_render_zone - ' + error); }
}

/**
 * Given a key (typically a block delta), this will generate a unique ID that
 * can be used for the panel. It will be fused with the current page id.
 * @param {String} key
 * @return {String}
 */
function drupalgap_panel_id(key) {
  try {
    return key + '_' + drupalgap_get_page_id();
  }
  catch (error) { console.log('drupalgap_panel_id - ' + error); }
}


function drupalgap_render(content) {

  var output_type = $.type(content);
  var html = '';

  // If the output came back as a string, we can render it as is. If the
  // output came back as on object, render each element in it through the
  // theme system.
  if (output_type === 'string') {
    // The page came back as an html string.
    html = content;
  }
  else if (output_type === 'object') {
    // The page came back as a render object. Let's define the names of
    // variables that are reserved for theme processing.
    var render_variables = ['theme', 'view_mode', 'language'];

    if (content.markup) { return content.markup; }

    // Is there a theme value specified in the content and the registry?
    if (content.theme && drupalgap.theme_registry[content.theme]) {

      // Extract the theme object template and determine the template file
      // name and path.
      var template = drupalgap.theme_registry[content.theme];
      var template_file_name = content.theme.replace(/_/g, '-') + '.tpl.html';
      var template_file_path = template.path + '/' + template_file_name;

      // Make sure the template file exists.
      // @TODO disc read here, replace with render array!
      if (drupalgap_file_exists(template_file_path)) {

        // Loads the template file's content into a string.
        // @TODO there is a disc read here, it is slow for UX! Deprecate via a render array.
        var template_file_html = drupalgap_file_get_contents(template_file_path);
        if (template_file_html) {

          // What variable placeholders are present in the template file?
          var placeholders = drupalgap_get_placeholders_from_html(template_file_html);
          if (placeholders) {

            // Replace each placeholder with html.
            for (var index in placeholders) {
              if (!placeholders.hasOwnProperty(index)) { continue; }
              var placeholder = placeholders[index];
              var _html = '';
              if (content[placeholder]) {
                // Grab the element variable from the content.
                var element = content[placeholder];
                // If it is markup, render it as is, if it is themeable,
                // then theme it.
                if (content[placeholder].markup) {
                  _html = content[placeholder].markup;
                }
                else if (content[placeholder].theme) {
                  _html = theme(content[placeholder].theme, element);
                }
                // Now remove the variable from the content.
                delete content[placeholder];
              }
              // Now replace the placeholder with the html, even if it was
              // empty.
              template_file_html = template_file_html.replace('{:' + placeholder + ':}', _html);
            }
          }
          else {
            // There were no place holders found, do nothing, ok.
          }

          // Finally add the rendered template file to the html.
          html += template_file_html;
        }
        else {
          console.log('drupalgap_render - failed to get file contents (' + template_file_path + ')');
        }
      }
      else {
        console.log('drupalgap_render - template file does not exist (' + template_file_path + ')');
      }
    }

    // Iterate over any remaining variables and theme them.
    for (var element in content) {
      if (!content.hasOwnProperty(element)) { continue; }
      var variables = content[element];
      if ($.inArray(element, render_variables) == -1) {
        html += theme(typeof variables.theme === 'undefined' ? null : variables.theme, variables);
      }
    }
  }

  // Now that we are done assembling the content into an html string, we can
  // return it.
  return html;
}

/**
 * Returns the path to the current DrupalGap theme, false otherwise.
 * @return {String|Boolean}
 */
function path_to_theme() {
  try {
    if (drupalgap.theme_path) {
      return drupalgap.theme_path;
    }
    else {
      console.log('path_to_theme - drupalgap.theme_path is not set!');
      return false;
    }
  }
  catch (error) { console.log('path_to_theme - ' + error); }
}

/**
 * Implementation of theme().
 * @param {String} hook
 * @param {Object} variables
 * @return {String}
 */
function theme(hook, variables) {
  try {

    // If there is HTML markup present, just return it as is. Otherwise, run
    // the theme hook and send along the variables.
    if (!variables) { variables = {}; }
    if (typeof variables.access !== 'undefined' && !variables.access) { return ''; }
    if (variables.markup) { return variables.markup; }
    var content = '';
    if (!hook) { return content; }

    // First see if the current theme implements the hook, if it does use it, if
    // it doesn't fallback to the core theme implementation of the hook.
    var theme_function = drupalgap.settings.theme + '_' + hook;
    if (!function_exists(theme_function)) {
      theme_function = 'theme_' + hook;

      // Fail safely an informative message if a bogus hook was passed in.
      if (!function_exists(theme_function)) {
        var caller = null;
        if (arguments.callee.caller) { caller = arguments.callee.caller.name; }
        var msg = 'WARNING: ' + theme_function + '() does not exist.';
        if (caller) { msg += ' Called by: ' + caller + '().' }
        console.log(msg);
        return content;
      }

    }

    // If no attributes are coming in, look to variables.options.attributes
    // as a secondary option, otherwise setup an empty JSON object for them.
    if (
      typeof variables.attributes === 'undefined' ||
      !variables.attributes
    ) {
      if (variables.options && variables.options.attributes) {
        variables.attributes = variables.options.attributes;
      }
      else {
        variables.attributes = {};
      }
    }
    // If there is no class name, set an empty one.
    if (!variables.attributes['class']) {
      variables.attributes['class'] = '';
    }
    var fn = window[theme_function];
    content = fn.call(null, variables);
    return content;
  }
  catch (error) { console.log('theme - ' + error); }
}

/**
 * Themes a button.
 * @param {Object} variables
 * @return {String}
 */
function theme_button(variables) {
  try {
    variables.attributes['data-role'] = 'button';
    var html = '<a ' + drupalgap_attributes(variables.attributes) + '>' +
      variables.text +
    '</a>';
    return html;
  }
  catch (error) { console.log('theme_button_link - ' + error); }
}

/**
 * Themes a button link.
 * @param {Object} variables
 * @return {String}
 */
function theme_button_link(variables) {
  try {
    variables.attributes['data-role'] = 'button';
    return theme_link(variables);
  }
  catch (error) { console.log('theme_button_link - ' + error); }
}

/**
 * Themes a collapsible widget.
 * @param {Object} variables
 * @return {String}
 */
function theme_collapsible(variables) {
  try {
    variables.attributes['data-role'] = 'collapsible';
    var header_type = 'h2';
    if (variables.header_type) { header_type = variables.header_type; }
    var header_attributes = {};
    if (variables.header_attributes) {
      header_attributes = variables.header_attributes;
    }
    var html = '<div ' + drupalgap_attributes(variables.attributes) + '>' +
      '<' + header_type + ' ' + drupalgap_attributes(header_attributes) + '>' +
        variables.header +
      '</' + header_type + '>' + variables.content + '</div>';
    return html;
  }
  catch (error) { console.log('theme_collapsible - ' + error); }
}

/**
 * Themes a collapsibleset widget.
 * @param {Object} variables
 * @return {String}
 */
function theme_collapsibleset(variables) {
  try {
    variables.attributes['data-role'] = 'collapsible-set';
    var html = '<div ' + drupalgap_attributes(variables.attributes) + '>';
    for (var index in variables.items) {
        if (!variables.items.hasOwnProperty(index)) { continue; }
        var item = variables.items[index];
        html += theme('collapsible', item);
    }
    html += '</div>';
    return html;
  }
  catch (error) { console.log('theme_collapsibleset - ' + error); }
}

/**
 * Themes a controlgroup widget.
 * @param {Object} variables
 * @return {String}
 */
function theme_controlgroup(variables) {
  try {
    variables.attributes['data-role'] = 'controlgroup';
    var html = '<div ' + drupalgap_attributes(variables.attributes) + '>';
    for (var index in variables.items) {
      if (!variables.items.hasOwnProperty(index)) { continue; }
      var item = variables.items[index];
      html += item;
    }
    html += '</div>';
    return html;
  }
  catch (error) { console.log('theme_controlgroup - ' + error); }
}

/**
 * Themes a header widget.
 * @param {Object} variables
 * @return {String}
 */
function theme_header(variables) {
  try {
    variables.attributes['data-role'] = 'header';
    if (typeof variables.type === 'undefined') { type = 'h2'; }
    var html = '<div ' + drupalgap_attributes(variables.attributes) + '>' +
      '<' + type + '>' + variables.text + '</' + type + '></div>';
    return html;
  }
  catch (error) { console.log('theme_header - ' + error); }
}

/**
 * Implementation of theme_image().
 * @param {Object} variables
 * @return {String}
 */
function theme_image(variables) {
  try {
    // Turn the path, alt and title into attributes if they are present.
    if (variables.path) { variables.attributes.src = variables.path; }
    if (variables.alt) { variables.attributes.alt = variables.alt; }
    if (variables.title) { variables.attributes.title = variables.title; }
    // Render the image.
    return '<img ' + drupalgap_attributes(variables.attributes) + ' />';
  }
  catch (error) { console.log('theme_image - ' + error); }
}


/**
 * Implementation of theme_audio().
 * @param {Object} variables
 * @return {String}
 */
function theme_audio(variables) {
  try {
    // Turn the path, alt and title into attributes if they are present.
    if (variables.path) { variables.attributes.src = variables.path; }
    if (variables.alt) { variables.attributes.alt = variables.alt; }
    if (variables.title) { variables.attributes.title = variables.title; }
    // Render the audio player.
    return '<audio controls ' + drupalgap_attributes(variables.attributes) +
    '></audio>';
  }
  catch (error) { console.log('theme_audio - ' + error); }
}

/**
 * Implementation of theme_video().
 * @param {Object} variables
 * @return {String}
 */
function theme_video(variables) {
  try {
    // Turn the path, alt and title into attributes if they are present.
    if (variables.path) { variables.attributes.src = variables.path; }
    if (variables.alt) { variables.attributes.alt = variables.alt; }
    if (variables.title) { variables.attributes.title = variables.title; }
    // Add the 'webkit-playsinline' attribute on iOS devices if no one made a
    // decision about it being there or not.
    if (
      typeof device !== 'undefined' &&
      device.platform == 'iOS' &&
      typeof variables.attributes['webkit-playsinline'] === 'undefined'
    ) { variables.attributes['webkit-playsinline'] = ''; }
    // Render the video player.
    return '<video ' + drupalgap_attributes(variables.attributes) +
    '></video>';
  }
  catch (error) { console.log('theme_video - ' + error); }
}

/**
 * Implementation of theme_image_style().
 * @param {Object} variables
 * @return {String}
 */
function theme_image_style(variables) {
  try {
    variables.path = image_style_url(variables.style_name, variables.path);
    return theme_image(variables);
  }
  catch (error) { console.log('theme_image - ' + error); }
}

/**
 * Theme's an item from an MVC collection.
 * @param {Object} variables
 * @return {String}
 */
function theme_item(variables) {
  try {
    var html = '';
    for (var field in variables.item) {
        if (!variables.item.hasOwnProperty(field)) { continue; }
        var value = variables.item[field];
        html +=
          '<h2>' + variables.model.fields[field].title + '</h2>' +
          '<p>' + value + '</p>';
    }
    return html;
  }
  catch (error) { console.log('theme_item - ' + error); }
}

/**
 * Implementation of theme_item_list().
 * @param {Object} variables
 * @return {String}
 */
function theme_item_list(variables) {
  try {
    // We'll theme an empty list unordered list by default, if there is a type
    // of list specified we'll use that, and if there are some items we'll
    // theme them too.
    var type = 'ul';
    if (variables.type) { type = variables.type; }
    var html = '';
    if (variables.title) { html += '<h2>' + variables.title + '</h2>'; }
    html += '<' + type + ' ' +
      drupalgap_attributes(variables.attributes) + '>';
    if (variables.items && variables.items.length > 0) {
      var listview = typeof variables.attributes['data-role'] !== 'undefined' &&
        variables.attributes['data-role'] == 'listview';
      for (var index = 0; index < variables.items.length; index++) {
        var item = variables.items[index];
        if (typeof item === 'string') {
          var icon = null;
          html += '<li';
          if (listview && (icon = $(item).attr('data-icon'))) {
            // If we're in a listview and the item specifies an icon,
            // add the icon attribute to the list item element.
            html += ' data-icon="' + icon + '"';
          }
          html += '>' + item + '</li>';
        }
        else if (typeof item === 'object') {
          var attributes = item.attributes ? item.attributes : {};
          var content = item.content ? item.content : '';
          html += '<li ' + drupalgap_attributes(attributes) + '>' + drupalgap_render(content) + '</li>';
        }
      }
    }
    html += '</' + type + '>';
    return html;
  }
  catch (error) { console.log('theme_item_list - ' + error); }
}

/**
 * Identical to theme_item_list, except this turns the list into a jQM listview.
 * @param {Object} variables
 * @return {String}
 */
function theme_jqm_item_list(variables) {
  try {
    if (variables.attributes) {
      if (
        variables.attributes['data-role'] &&
        variables.attributes['data-role'] != 'listview'
      ) { }
      else {
        variables.attributes['data-role'] = 'listview';
      }
    }
    else {
      variables.attributes['data-role'] = 'listview';
    }
    return theme_item_list(variables);
  }
  catch (error) { console.log('theme_jqm_item_list - ' + error); }
}

/**
 * Implementation of theme_link().
 * @param {Object} variables
 * @return {String}
 */
function theme_link(variables) {
  try {
    var text = '';
    if (variables.text) { text = variables.text; }
    if (typeof variables.path !== 'undefined' && variables.path != null) {

      // If the path begins with a hashtag, just render the link as is with the
      // hashtag for the href.
      if (variables.path.indexOf('#') == 0) {
        variables.attributes['href'] = variables.path;
        return '<a ' + drupalgap_attributes(variables.attributes) + '>' +
          text +
        '</a>';
      }

      // By default our onclick will use a drupalgap_goto(). If we have any
      // incoming link options, then modify the link accordingly.
      var onclick = 'drupalgap_goto(\'' + variables.path + '\');';
      if (variables.options) {

        // Use an InAppBrowser?
        if (variables.options.InAppBrowser) {
          onclick =
            "window.open('" + variables.path + "', '_blank', 'location=yes');";
        }

        else {

          // Prepare the path.
          variables.path = _drupalgap_goto_prepare_path(variables.path);

          // All other options need to be extracted into a JSON string for the
          // onclick handler.

          var goto_options = '';
          for (var option in variables.options) {
              if (!variables.options.hasOwnProperty(option)) { continue; }
              var value = variables.options[option];
              if (option == 'attributes') { continue; }
              if (typeof value === 'string') { value = "'" + value + "'"; }
              goto_options += option + ':' + value + ',';
          }
          onclick =
            'drupalgap_goto(\'' +
              variables.path + '\', ' +
              '{' + goto_options + '});';

        }
      }

      // Is this link active?
      if (variables.path == drupalgap_path_get()) {
        if (variables.attributes['class'].indexOf('ui-btn-active') == -1) {
          variables.attributes['class'] += ' ui-btn-active ';
        }
        if (variables.attributes['class'].indexOf('ui-state-persist') == -1) {
          variables.attributes['class'] += ' ui-state-persist ';
        }
      }

      // Finally, return the link.
      return '<a href="#" onclick="javascript:' + onclick + '"' +
        drupalgap_attributes(variables.attributes) + '>' + text + '</a>';

    }
    else {

      // The link has no path, so just render the text and attributes.
      if (typeof variables.attributes.href === 'undefined') {
        variables.attributes.href = '#';
      }
      return '<a ' + drupalgap_attributes(variables.attributes) + '>' +
        text +
      '</a>';

    }
  }
  catch (error) { console.log('theme_link - ' + error); }
}

/**
 * Themes the logout button.
 * @param {Object} variables
 * @return {String}
 */
function theme_logout(variables) {
  try {
    return bl(
      t('Logout'),
      'user/logout',
      {
        attributes: {
          'data-icon': 'action',
          'data-iconpos': 'right'
        }
      }
    );
  }
  catch (error) { console.log('theme_logout - ' + error); }
}

/**
 * Themes a popup widget.
 * @param {Object} variables
 * @return {String}
 */
function theme_popup(variables) {
  try {
    variables.attributes['data-role'] = 'popup';
    var button_attributes = {};
    if (variables.button_attributes) {
      button_attributes = variables.button_attributes;
    }
    button_attributes.href = '#' + variables.attributes.id;
    button_attributes['data-rel'] = 'popup';
    var html = bl(variables.button_text, null, {
        attributes: button_attributes
    }) +
    '<div ' + drupalgap_attributes(variables.attributes) + '>' +
      variables.content +
    '</div>';
    return html;
  }
  catch (error) { console.log('theme_popup - ' + error); }
}

/**
 * Implementation of theme_submit().
 * @param {Object} variables
 * @return {String}
 */
function theme_submit(variables) {
  try {
    return '<button ' + drupalgap_attributes(variables.attributes) + '>' +
      variables.element.value +
    '</button>';
  }
  catch (error) { console.log('theme_submit - ' + error); }
}

/**
 * Implementation of theme_table().
 * @param {Object} variables
 * @return {String}
 */
function theme_table(variables) {
  try {
    var html = '<table ' + drupalgap_attributes(variables.attributes) + '>';
    if (variables.header) {
      html += '<thead><tr>';
      for (var index in variables.header) {
          if (!variables.header.hasOwnProperty(index)) { continue; }
          var column = variables.header[index];
          if (column.data) {
            html += '<td>' + column.data + '</td>';
          }
      }
      html += '</tr></thead>';
    }
    html += '<tbody>';
    if (variables.rows) {
      for (var row_index in variables.rows) {
          if (!variables.rows.hasOwnProperty(row_index)) { continue; }
          var row = variables.rows[row_index];
          html += '<tr>';
          if (row) {
            for (var column_index in row) {
              if (!row.hasOwnProperty(column_index)) { continue; }
              var column = row[column_index];
              html += '<td>' + column + '</td>';
            }
          }
          html += '</tr>';
      }
    }
    return html + '</tbody></table>';
  }
  catch (error) { console.log('theme_table - ' + error); }
}

/**
 * Theme a jQueryMobile table.
 * @param {Object} variables
 * @return {String}
 */
function theme_jqm_table(variables) {
  try {
    variables.attributes['data-role'] = 'table';
    variables.attributes['data-mode'] = 'reflow';
    return theme_table(variables);
  }
  catch (error) { console.log('theme_jqm_table - ' + error); }
}


/**
 * An interal callback used to handle the setting of the page title during the
 * pageshow event.
 * @param {*} page_arguments
 */
function _drupalgap_page_title_pageshow(page_arguments) {
  try {
    var router_path = drupalgap_router_path_get();
    // Set the page title. First we'll see if the hook_menu() item has a
    // title variable set, then check for a title_callback function.
    var title_arguments = [];
    if (typeof drupalgap.menu_links[router_path].title !== 'undefined') {
      drupalgap_set_title(drupalgap.menu_links[router_path].title);
    }
    if (
      typeof drupalgap.menu_links[router_path].title_callback !== 'undefined'
    ) {
      var function_name = drupalgap.menu_links[router_path].title_callback;
      if (drupalgap_function_exists(function_name)) {
        // Grab the title callback function.
        var fn = window[function_name];
        // Place the internal success callback handler on the front of the
        // title arguments.
        title_arguments.unshift(_drupalgap_page_title_pageshow_success);
        // Are there any additional arguments to send to the title callback?
        if (drupalgap.menu_links[router_path].title_arguments) {
          // For each title argument, if the argument is an integer, grab the
          // corresponding arg(#), otherwise just push the arg onto the title
          // arguments.
          var args = arg(null, drupalgap_path_get());
          var _title_arguments = drupalgap.menu_links[router_path].title_arguments;
          for (var index in _title_arguments) {
              if (!_title_arguments.hasOwnProperty(index)) { continue; }
              var object = _title_arguments[index];
              if (is_int(object) && args[object]) {
                title_arguments.push(args[object]);
              }
              else { title_arguments.push(object); }
          }
        }
        // Call the title callback function with the title arguments.
        drupalgap_set_title(
          fn.apply(
            null,
            Array.prototype.slice.call(title_arguments)
          )
        );
      }
    }
    else {
      _drupalgap_page_title_pageshow_success(drupalgap_get_title());
    }
  }
  catch (error) { console.log('_drupalgap_page_title_pageshow - ' + error); }
}

/**
 * An internal function used to set the page title when the page title callback
 * function is successful.
 * @param {String} title
 */
function _drupalgap_page_title_pageshow_success(title) {
  try {
    var id = system_title_block_id(drupalgap_path_get());
    $('h1#' + id).html(title);
  }
  catch (error) {
    console.log('_drupalgap_page_title_pageshow_success - ' + error);
  }
}


/**
 * When a form submission for an entity is assembling the entity json object to
 * send to the server, some form element fields need to be assembled in unique
 * ways to match the entity's structure in Drupal. Modules that implement fields
 * can use this hook to properly assemble the item value (by delta) and return
 * it.
 * @param {Object} entity_type
 * @param {String} bundle
 * @param {String} form_state_value
 * @param {Object} field
 * @param {Object} instance
 * @param {String} langcode
 * @param {Number} delta
 * @param {Object} field_key Set the 'value' string property on this object to
 *                           use a custom property name on the field value.
 *                           Defaults to 'value'.
 *                           Set the 'use_key' boolean property on this object
 *                           to false to not use a key when assembling the
 *                           result into the field. Defaults to true.
 *                           Set the 'use_wrapper' boolean property on this
 *                           object to false to not use the default wrapper
 *                           placed around the result object. Defaults to true.
 *                           Set the 'use_delta' boolean property to false when
 *                           a delta value is not needed. Defaults to true.
 * @param {Object} form
 *
 * @return {*}
 */
function hook_assemble_form_state_into_field(entity_type, bundle,
  form_state_value, field, instance, langcode, delta, field_key, form) {
  try {
    // Listed below are example use cases. Each show how to assemble the result,
    // and what the resulting field object will look like when assembled by the
    // DrupalGap Forms API. We'll use a field machine name of 'field_my_field'
    // in all of the examples.
    
    // Optional helpful values.
    // Grab the form element's name, the form id, and the element's id.
    // var element_name = field_key.name;
    // var form_id = field_key.form_id;
    // var element_id = field_key.element_id;

    // Example 1 - Here's a simple example using all the defaults.
    var result = {
      foo: 'bar'
    };
    return result;
    // This in turn gets assembled by the DG FAPI.
    // {
    //   /* ... other entity values and fields ... */
    //   field_my_field: {
    //     und: [
    //       { value: { foo: "bar" } }
    //     ]
    //   }
    // }
    
    // Example 2 - Setting the 'value' property.
    field_key.value = "foo";
    var result = "bar";
    return result;
    // {
    //   /* ... other entity values and fields ... */
    //   field_my_field: {
    //     und: [
    //       { foo: "bar" }
    //     ]
    //   }
    // }
    
    // Example 3 - Setting the 'use_key' to false.
    field_key.use_key = false;
    var result = "bar";
    return result;
    // {
    //   /* ... other entity values and fields ... */
    //   field_my_field: {
    //     und: ["bar"]
    //   }
    // }
    
    // Example 4 - Setting the 'use_delta' to false.
    field_key.use_delta = false;
    var result = "bar";
    return result;
    // {
    //   /* ... other entity values and fields ... */
    //   field_my_field: {
    //     und: ["bar"]
    //   }
    // }
    
    // Example 5 - Setting the 'use_delta' and 'use_wrapper' to false.
    field_key.use_delta = false;
    field_key.use_wrapper = false;
    var result = "bar";
    return result;
    // {
    //   /* ... other entity values and fields ... */
    //   field_my_field: {
    //     und: "bar"
    //   }
    // }
  }
  catch (error) {
    console.log('hook_assemble_form_state_into_field - ' + error);
  }
}

/**
 * When the app is first loading up, DrupalGap checks to see if the device has a connection, if it does then this hook
 * is called. If DrupalGap doesn't have a connection, then hook_device_offline() is called. Implementations of
 * hook_deviceready() need to return true if they'd like DrupalGap to continue, or return false if you'd like DrupalGap
 * to NOT continue. If DrupalGap continues, it will perform a System Connect resource call then go to the App's front
 * page. This is called during DrupalGap's "deviceready" implementation for PhoneGap. Note, the Drupal.user object is
 * not initialized at this point, and will always be an anonymous user.
 */
function hook_deviceready() {}

/**
 * When someone calls drupalgap_has_connection(), this hook has an opportunity to set drupalgap.online to true or false.
 * The value of drupalgap.online is returned to anyone who calls drupalgap_has_connection(), including DrupalGap core.
 */
function hook_device_connection() {

  // If it is Saturday, take the app offline and force the user to go outside and play.
  var d = new Date();
  if (d.getDay() == 6) { drupalgap.online = false; }

}

/**
 * Called during app startup if the device does not have a connection. Note, the Drupal.user object is ot initialized at
 * this point, and will always be an anonymous user.
 */
function hook_device_offline() {

  // Even though we're offline, let's just go to the front page.
  drupalgap_goto('');

}

/**
 * Take action when the user presses the "back" button. This includes the soft,
 * hardware and browser back buttons. The browser back button is only available
 * in web app mode, the hardware back button is typically only on compiled
 * Android devices, whereas the soft back button actually appears within the UX
 * of the app.
 * @param {String} from
 * @param {String} to
 * @see http://docs.drupalgap.org/7/Widgets/Buttons/Back_Button
 */
function hook_drupalgap_back(from, to) {

  // When the user navigates from the front page to the login page, show them
  // a message (a toast).
  if (from == drupalgap.settings.front && to == 'user/login') {
    drupalgap_toast('Please login to continue');
  }

}

/**
 * Each time a page is navigated to within the app, drupalgap_goto() is called.
 * Use this hook to do some preprocessing before drupalgap_goto() gets started.
 * @param {String} path The current page path.
 */
function hook_drupalgap_goto_preprocess(path) {
  try {
    // Pre process the front page.
    if (path == drupalgap.settings.front) {
      drupalgap_alert(t('Preprocessing the front page!'));
    }
  }
  catch (error) {
    console.log('hook_drupalgap_goto_preprocess - ' + error);
  }
}

/**
 * Each time a page is navigated to within the app, drupalgap_goto() is called.
 * Use this hook to do some post processing after drupalgap_goto() has finished.
 * @param {String} path The current page path.
 */
function hook_drupalgap_goto_post_process(path) {
  try {
    // Post process the front page.
    if (path == drupalgap.settings.front) {
      drupalgap_alert(t('Post processing the front page!'));
    }
  }
  catch (error) {
    console.log('hook_drupalgap_goto_post_process - ' + error);
  }
}

/**
 * Called after a successful services API call to a Drupal site. Do not call
 * any services from within your implementation, you may run into an infinite
 * loop in your code. See http://drupalgap.org/project/force_authentication for
 * example usage.
 * @deprecated - use hook_services_postprocess() instead.
 */
function hook_services_success(url, data) { }

/**
 * A hook used to declare custom block information.
 */
function hook_block_info() {}

/**
 * A hook used to render custom blocks.
 */
function hook_block_view(delta, region) {
}

/**
 * A hook used to handle a 404 in the app.
 */
function hook_404(router_path) {}

/**
 * Implements hook_entity_pre_build_content().
 */
function hook_entity_pre_build_content(entity, entity_type, bundle) {

  // Change some weights on nodes with a date field.
  if (entity_type == 'node' && typeof entity.field_date !== 'undefined') {
    entity.body.weight = 0;
    entity.field_date.weight = 1;
  }
}

/**
 * Implements hook_entity_post_build_content().
 */
function hook_entity_post_build_content(entity, entity_type, bundle) {

}

/**
 * Implements hook_entity_pre_render_content().
 * Called before drupalgap_entity_render_content() assembles the entity.content
 * string. Use this to make modifications to an entity before its' content is rendered.
 */
function hook_entity_pre_render_content(entity, entity_type, bundle) {
  try {

    // Remove access to the date field on all nodes.
    if (entity_type == 'node' && typeof entity.field_date !== 'undefined') {
      entity.field_date.access = false;
    }

  }
  catch (error) {
    console.log('hook_entity_pre_render_content - ' + error);
  }
}

/**
 * Called after drupalgap_entity_render_content() assembles the entity.content
 * string. Use this to make modifications to the HTML output of the entity's
 * content before it is displayed.
 */
function hook_entity_post_render_content(entity, entity_type, bundle) {
  try {
    if (entity.type == 'article') {
      entity.content += '<p>' + t('Example text on every article!') + '</p>';
    }
  }
  catch (error) {
    console.log('hook_entity_post_render_content - ' + error);
  }
}

/**
 * Implements hook_entity_view_alter().
 * Called immediately before a page is rendered and injected into its waiting
 * container. Use this hook to modifications to the build object by adding or
 * editing render arrays (widgets) on the build object.
 */
function hook_entity_view_alter(entity_type, entity_id, mode, build) {
  try {
    if (entity_type == 'user' && mode == 'view') {
      if (entity_id == Drupal.user.uid) {
        build['foo'] = { markup: '<p>Extra stuff when viewing own user profile...</p>' };
        build['volume'] = {
          theme: 'range',
          attributes: {
            min: '0',
            max: '11',
            value: '11',
            'data-theme': 'b'
          }
        };
      }
      else {
        build['bar'] = { markup: '<p>Viewing some other profile...</p>' };
      }
    }
  }
  catch (error) { console.log('hook_entity_view_alter - ' + error); }
}

/**
 * Implements hook_field_info_instance_add_to_form().
 * Used by modules that provide custom fields to operate on a form or its
 * elements before the form gets saved to local storage. This allows extra
 * data be attached to the form that way things like hook_field_widget_form(),
 * which takes place at render time, can have access to any extra data it may
 * need.
 * @param {String} entity_type
 * @param {String} bundle
 * @param {Object} form
 * @param {Object} entity
 * @param {Object} element
 */
function hook_field_info_instance_add_to_form(entity_type, bundle, form, entity, element) {
  try {
    // Attach a value_callback to the element so we can manually build its form
    // state value.
    element.value_callback = 'example_field_value_callback';
  }
  catch (error) { console.log('hook_field_info_instance_add_to_form - ' + error); }
}

/**
 * Implements hook_field_formatter_view().
 * @param {String} entity_type
 * @param {Object} entity
 * @param {Object} field
 * @param {Object} instance
 * @param {String} langcode
 * @param {Object} items
 * @param {Object} display
 */
function hook_field_formatter_view(entity_type, entity, field, instance, langcode, items, display) {
  try {
    
    // Use this hook to render a field's content on an entity. Use dpm() to
    // inspect the incoming arguments. The arguments contain field display
    // settings from Drupal.
    
    //console.log(entity_type);
    //console.log(entity);
    //console.log(field);
    //console.log(instance);
    //console.log(langcode);
    //console.log(items);
    //console.log(display);
    
    // Iterate over each item, and place a widget onto the render array.
    var content = {};
    for (var delta in items) {
        if (!items.hasOwnProperty(delta)) { continue; }
        var item = items[delta];
        content[delta] = {
          markup: '<p>' + t('Hello!') + '</p>'
        };
    }
    return content;
  }
  catch (error) { console.log('hook_field_formatter_view - ' + error); }
}

/**
 * Implements hook_field_widget_form().
 * @param {Object} form
 * @param {Object} form_state
 * @param {Object} field
 * @param {Object} instance
 * @param {String} langcode
 * @param {Object} items
 * @param {Number} delta
 * @param {Object} element
 */
function hook_field_widget_form(form, form_state, field, instance, langcode, items, delta, element) {
  try {
    // Use this hook to provide field widgets for form element items. This hook
    // is called for each delta value on the field. Make modifications to the
    // items collection using the provided delta value. The object contained
    // within is a standard DrupalGap Forms API object, so you may assemble the
    // field (and any children widgets) as needed.
    
    // Very simple example, make the widget for the field a text field.
    items[delta].type = 'textfield';
  }
  catch (error) { console.log('hook_field_widget_form - ' + error); }
}

/**
 * Called after a form element is assembled. Use it to alter a form element.
 */
//function hook_form_element_alter(form, element, variables) { }

/**
 * Implements hook_entity_post_render_field().
 * Called after drupalgap_entity_render_field() assembles the field content
 * string. Use this to make modifications to the HTML output of the entity's
 * field before it is displayed. The field content will be inside of
 * reference.content, so to make modifications, change reference.content. For
 * more info: http://stackoverflow.com/questions/518000/is-javascript-a-pass-by-reference-or-pass-by-value-language
 */
function hook_entity_post_render_field(entity, field_name, field, reference) {
  if (field_name == 'field_my_image') {
    reference.content += '<h2>' + entity.title + '</h2>';
  }
}

/**
 * Implements hook_form_alter().
 * This hook is used to make alterations to existing forms.
 */
function hook_form_alter(form, form_state, form_id) {
  // Change the description of the name element on the user login form
  if (form_id == 'user_login_form') {
    form.elements['name'].description = t('Enter your login name');
  }
}

/**
 * Implements hook_image_path_alter().
 * Called after drupalgap_image_path() assembles the image path. Use this hook
 * to make modifications to the image path. Return the modified path, or false
 * to allow the default path to be generated.
 */
function hook_image_path_alter(src) { }

/**
 * Implements hook_install().
 * This hook is used by modules that need to execute custom code when the module
 * is loaded. Note, the Drupal.user object is not initialized at this point, and
 * always appears to be an anonymous user.
 */
function hook_install() { }

/**
 * Implements hook_locale().
 * Used to declare language code .json files that should be loaded by DrupalGap.
 * @see http://drupalgap.org/translate
 */
function hook_locale() {
  // Tell DrupalGap to load our custom Spanish and Italian language files
  // located here:
  //   app/modules/custom/my_module/locale/es.json
  //   app/modules/custom/my_module/locale/it.json
  return ['es', 'it'];
}

/**
 * Implements hook_menu()
 * This hook is used to declare menu paths for custom pages.
 */
function hook_menu() {
  try {
    var items = {};
    items['hello_world'] = {
      title: t('Hello World'),
      page_callback: 'my_module_hello_world_page'
    };
    return items;
  }
  catch (error) { console.log('hook_menu - ' + error); }
}

function hook_mvc_model() {
  var models = {};
  return models;
}
function hook_mvc_view() {}
function hook_mvc_controller() {}

/**
 * Implements hook_node_page_view_alter_TYPE().
 * @param {Object} node The fully loaded node object.
 * @param {Object} options The options object, containing the success callback.
 */
function hook_node_page_view_alter_TYPE(node, options) {
  try {

    // Use this hook to completely take over the content that is shown when a
    // user views a certain content type's page within the app. Pass your
    // content (render array or html string) to the sucess callback provided in
    // options to have it automatically injected in the page.

    var content = {};
    content['my_markup'] = {
      markup: '<p>'+t('Click below to see the node!')+'</p>'
    };
    content['my_collapsible'] = {
      theme: 'collapsible',
      header: node.title,
      content: node.content
    };
    options.success(content);

  }
  catch (error) { console.log('hook_node_page_view_alter_TYPE() - ' + error); }
}

/**
 * Implements hook_page_build().
 * @param {Object} output The page build output object.
 */
function hook_page_build(output) {
  try {
    // Remove all titles from article node pages.
    if (output.node && output.node.type == 'article') {
      delete output.title;
    }
  }
  catch (error) { console.log('hook_page_build - ' + error); }
}

/**
 * Implements hook_preprocess_page().
 * Take action before the page is processed and shown to the user.
 * @param {Object} variables The page variables.
 */
function hook_preprocess_page(variables) {
  try {

  }
  catch (error) {
    console.log('hook_preprocess_page - ' + error);
  }
}

/**
 * Implements hook_post_process_page().
 * Take action after the page is processed and shown to the user.
 * @param {Object} variables The page variables.
 */
function hook_post_process_page(variables) {
  try {

  }
  catch (error) {
    console.log('hook_post_process_page - ' + error);
  }
}

/**
 * Implements hook_views_exposed_filter().
 * @param {Object} form
 * @param {Object} form_state
 * @param {Object} element
 * @param {Object} filter
 * @param {Object} field
 */
function hook_views_exposed_filter(form, form_state, element, filter, field) {
  try {
    
    // This hook is used to assemble the form element for an exposed filter.
    // Make modifications to the element JSON object to control how it is used
    // on the form. The element comes prepopulated with some basic values. The
    // majority of the data you need to assemble the field should be contained
    // within the filter and field JSON objects, so use dpm() to insepct them
    // and assemble your filter's element.
    
    //dpm(filter);
    //dpm(field);
    
  }
  catch (error) { console.log('hook_views_exposed_filter - ' + error); }
}

/**
 * Implements hook_menu().
 * @return {Object}
 */
function comment_menu() {
    var items = {
      'comment/%': {
        title: t('Comment'),
        page_callback: 'comment_page_view',
        page_arguments: [1],
        pageshow: 'comment_page_view_pageshow',
        title_callback: 'comment_page_title',
        title_arguments: [1]
      },
      'comment/%/view': {
        title: t('View'),
        type: 'MENU_DEFAULT_LOCAL_TASK',
        weight: -10
      },
      'comment/%/edit': {
        title: t('Edit'),
        page_callback: 'entity_page_edit',
        pageshow: 'entity_page_edit_pageshow',
        page_arguments: ['comment_edit', 'comment', 1],
        weight: 0,
        type: 'MENU_LOCAL_TASK',
        access_callback: 'comment_access',
        access_arguments: [1],
        options: { reloadPage: true }
      }
    };
    return items;
}

/**
 * Given a comment, this determines if the current user has access to it.
 * Returns true if so, false otherwise.
 * @param {Object} comment
 * @return {Boolean}
 */
function comment_access(comment) {
  try {
    if (comment.uid == Drupal.user.uid && user_access('edit own comments') ||
      user_access('administer comments')) {
      return true;
    }
    else { return false; }
  }
  catch (error) { console.log('comment_access - ' + error); }
}

/**
 * Given a node id, this will return the id to use on the comments wrapper.
 * @param {Number} nid
 * @return {String}
 */
function comments_container_id(nid) {
    return 'comments_container_' + nid;
}

/**
 * Given a comment id, this will return the id to use on the comment wrapper.
 * @param {Number} cid
 * @return {String}
 */
function comment_container_id(cid) {
    return 'comment_container_' + cid;
}

/**
 * Given a node id, this will return the id to use on the html list for the
 * comments.
 * @param {Number} nid
 * @return {String}
 * @deprecated Use comments_container_id() instead.
 */
function comment_list_id(nid) {
  try {
    return comments_container_id(nid);
  }
  catch (error) { console.log('comment_list_id - ' + error); }
}

/**
 * Page callback for comment/%.
 * @param {Number} cid
 * @return {Object}
 */
function comment_page_view(cid) {
  try {
    if (cid) {
      var content = {
        container: _drupalgap_entity_page_container('comment', cid, 'view')
      };
      return content;
    }
    else { drupalgap_error(t('No comment id provided!')); }
  }
  catch (error) { console.log('comment_page_view - ' + error); }
}

/**
 * jQM pageshow handler for comment/% pages.
 * @param {Number} cid
 */
function comment_page_view_pageshow(cid) {
  try {
    comment_load(cid, {
        success: function(comment) {
          var item = theme('comment', { comment: comment });
          var content = theme('jqm_item_list', {items: [item]});
          _drupalgap_entity_page_container_inject(
            'comment',
            cid,
            'view',
            content
          );
        }
    });
  }
  catch (error) { console.log('comment_page_view_pageshow - ' + error); }
}

/**
 * The title call back function for the comment view page.
 * @param {Function} callback
 * @param {Number} cid
 */
function comment_page_title(callback, cid) {
  try {
    // Try to load the comment subject, then send it back to the given callback.
    var title = '';
    var comment = comment_load(cid, {
        success: function(comment) {
          if (comment && comment.subject) { title = comment.subject; }
          callback.call(null, title);
        }
    });
  }
  catch (error) { console.log('comment_page_title - ' + error); }
}

/**
 * The comment edit form.
 * @param {Object} form
 * @param {Object} form_state
 * @param {Object} comment
 * @param {Object} node
 * @return {Object}
 */
function comment_edit(form, form_state, comment, node) {
  try {

    // If there is no comment object coming in, make an empty one with a node
    // id. Note, once the form.js submit handler is aware of its own entity and
    // can pass it along to this function (and others) when loading a form
    // during the submission process, this little chunk of code will no longer
    // be needed.
    if (!comment) { comment = {'nid': arg(1)}; }

    // Determine the comment bundle from the node type.
    var node_type = null;
    if (node && node.type) { node_type = node.type; }
    else { node_type = comment.node_type.replace('comment_node_', ''); }
    var bundle = 'comment_node_' + node_type;

    // Setup form defaults.
    form.id += '_' + comment.nid;  // Append the node id the comment form id.
    form.entity_type = 'comment';
    form.bundle = bundle;
    form.action = 'node/' + comment.nid;

    // Add the entity's core fields to the form.
    drupalgap_entity_add_core_fields_to_form(
      'comment',
      bundle,
      form,
      comment
    );

    // Add the fields for this content type to the form.
    drupalgap_field_info_instances_add_to_form(
      'comment',
      bundle,
      form,
      comment
    );

    // Add submit to form.
    form.elements.submit = {
      'type': 'submit',
      'value': t('Save')
    };

    // Add cancel and delete button to form if we're editing a comment. Also
    // figure out a form title to use in the prefix.
    var form_title = t('Add comment');
    if (comment && comment.cid) {
      form_title = t('Edit comment');
      form.buttons['cancel'] = drupalgap_form_cancel_button();
      form.buttons['delete'] =
        drupalgap_entity_edit_form_delete_button('comment', comment.cid);
    }

    // Add a prefix.
    form.prefix += '<h2>' + form_title + '</h2>';

    return form;
  }
  catch (error) { console.log('comment_edit - ' + error); }
}

/**
 * The comment edit submit function.
 * @param {Object} form
 * @param {Object} form_state
 */
function comment_edit_submit(form, form_state) {
  try {
    var comment = drupalgap_entity_build_from_form_state(form, form_state);
    drupalgap_entity_form_submit(form, form_state, comment);
  }
  catch (error) { console.log('comment_edit_submit - ' + error); }
}

/**
 * Implements hook_services_postprocess().
 * @param {Object} options
 * @param {Object} result
 */
function comment_services_postprocess(options, result) {
  try {
    if (options.service == 'comment' && options.resource == 'create') {
      // If we're on the node view page, inject the comment into the comment
      // listing (if it is in the DOM), then scroll the page to the newly
      // inserted/rendered comment, then clear the form input.
      var path = drupalgap_path_get();
      var router_path = drupalgap_get_menu_link_router_path(path);
      if (router_path == 'node/%') {
        var nid = arg(1);
        var container_id = comments_container_id(nid);
        var container = $('#' + container_id);
        if (
          typeof container.length !== 'undefined' &&
          container.length == 0
        ) { return; }
        node_load(nid, {
            reset: true,
            success: function(node) {
              comment_load(result.cid, {
                  success: function(comment) {
                    $(container).append(
                      theme('comment', { comment: comment })
                    ).trigger('create');
                    scrollToElement('#' + container_id + ' #' + comment_container_id(comment.cid), 500);
                    var form_selector = '#' + drupalgap_get_page_id() +
                      ' #comment_edit' + '_' + comment.nid;
                    drupalgap_form_clear(form_selector);
                  }
              });
            }
        });
      }
    }
  }
  catch (error) { console.log('comment_services_postprocess - ' + error); }
}

/**
 * Theme's a comment container.
 * @param {Object} variables
 * @return {String}
 */
function theme_comments(variables) {
  try {
    // Set the container id and append default attributes.
    variables.attributes.id = comments_container_id(variables.node.nid);
    variables.attributes['class'] +=
      'comments comments-node-' + variables.node.type;
    variables.attributes['data-role'] = 'collapsible-set';
    // Open the container.
    var html = '<div ' + drupalgap_attributes(variables.attributes) + '>';
    // Show a comments title if there are any comments.
    if (variables.node.comment_count > 0) {
      html += '<h2 class="comments-title">Comments</h2>';
    }
    // If the comments are already rendered, show them.
    if (variables.comments) { html += variables.comments; }
    // Close the container and return the html.
    html += '</div>';
    return html;
  }
  catch (error) { console.log('theme_comments - ' + error); }
}

/**
 * Theme's a comment.
 * @param {Object} variables
 * @return {String}
 */
function theme_comment(variables) {
  try {
    var comment = variables.comment;
    // Set the container id and append default attributes.
    variables.attributes.id = comment_container_id(comment.cid);
    variables.attributes['class'] += 'comment ';
    variables.attributes['data-role'] = 'collapsible';
    variables.attributes['data-collapsed'] = 'false';
    var html = '<div ' + drupalgap_attributes(variables.attributes) + '>';
    var comment_content = '';
    // Any user picture?
    // @TODO - the user picture doesn't use an image style here, it uses the
    // original picture uploaded by the user, which can be varying sizes.
    var picture = '';
    if (comment.picture_uri) {
      picture += theme(
        'image', { path: drupalgap_image_path(comment.picture_uri) }
      );
    }
    // Comment date.
    var created = new Date(comment.created * 1000);
    created = created.toLocaleDateString() + ' at ' +
      created.toLocaleTimeString();
    // Append comment extra fields and content. The user info will be rendered
    // as a list item link.
    var author = picture +
        '<h3>' + comment.name + '</h3>' +
        '<p>' + created + '</p>';
    author = l(author, 'user/' + comment.uid);
    comment_content +=
      '<h2>' + comment.subject + '</h2>' +
      '<ul data-role="listview" data-inset="true">' +
        '<li>' + author + '</li>' +
      '</ul>' + comment.content;
    html += comment_content;
    // Add an edit link if necessary.
    if (
      user_access('administer comments') ||
      (user_access('edit own comments') && comment.uid == Drupal.user.uid)
    ) {
      html += theme('button_link', {
          text: t('Edit'),
          path: 'comment/' + comment.cid + '/edit',
          attributes: {
            'data-icon': 'gear'
          }
      });
    }
    // Close the container and return the html.
    html += '</div>';
    return html;
  }
  catch (error) { console.log('theme_comment - ' + error); }
}


/**
 * Implements hook_menu().
 * @return {Object}
 */
function contact_menu() {
  try {
    var items = {};
    items['contact'] = {
      title: t('Contact'),
      page_callback: 'drupalgap_get_form',
      page_arguments: ['contact_site_form'],
      pageshow: 'contact_site_form_pageshow',
      access_arguments: ['access site-wide contact form']
    };
    items['user/%/contact'] = {
      title: t('User contact'),
      page_callback: 'drupalgap_get_form',
      page_arguments: ['contact_personal_form', 1],
      pageshow: 'contact_personal_form_pageshow',
      access_arguments: ['access user contact forms'],
      weight: 10,
      type: 'MENU_LOCAL_TASK'
    };
    return items;
  }
  catch (error) { console.log('contact_menu - ' + error); }
}

/**
 * The contact index service resource.
 * @param {Object} options
 */
function contact_index(options) {
  try {
    options.method = 'GET';
    options.path = 'contact.json';
    options.service = 'contact';
    options.resource = 'index';
    Drupal.services.call(options);
  }
  catch (error) { console.log('contact_index - ' + error); }
}

/**
 * The contact site service resource.
 * @param {Object} options
 */
function contact_site(options) {
  try {
    options.method = 'POST';
    options.path = 'contact/site.json';
    options.service = 'contact';
    options.resource = 'site';
    Drupal.services.call(options);
  }
  catch (error) { console.log('contact_site - ' + error); }
}

/**
 * The contact personal service resource.
 * @param {Object} options
 */
function contact_personal(options) {
  try {
    options.method = 'POST';
    options.path = 'contact/personal.json';
    options.service = 'contact';
    options.resource = 'personal';
    Drupal.services.call(options);
  }
  catch (error) { console.log('contact_personal - ' + error); }
}

/**
 * The site contact form.
 * @param {Object} form
 * @param {Object} form_state
 * @return {Object}
 */
function contact_site_form(form, form_state) {
  try {
    form.elements.name = {
      title: t('Your name'),
      type: 'textfield',
      required: true
    };
    form.elements.mail = {
      title: t('Your e-mail address'),
      type: 'email',
      required: true
    };
    form.elements.subject = {
      title: t('Subject'),
      type: 'textfield',
      required: true
    };
    form.elements.cid = {
      title: t('Category'),
      type: 'select',
      required: true
    };
    form.elements.message = {
      title: t('Message'),
      type: 'textarea',
      required: true
    };
    form.elements.copy = {
      title: t('Send yourself a copy?'),
      type: 'checkbox',
      default_value: 0,
      access: false
    };
    form.elements.submit = {
      type: 'submit',
      value: t('Send message')
    };
    // If the user is logged in, set the default values.
    if (Drupal.user.uid != 0) {
      form.elements.name.default_value = Drupal.user.name;
      form.elements.name.disabled = true;
      form.elements.mail.default_value = Drupal.user.mail;
      form.elements.mail.disabled = true;
      form.elements.copy.access = true;
    }
    return form;
  }
  catch (error) { console.log('contact_site_form - ' + error); }
}

/**
 * The pageshow callback for the contact site form.
 */
function contact_site_form_pageshow() {
  try {
    contact_index({
        success: function(results) {
          var select = $('#edit-contact-site-form-cid');
          if (!results || !results.length) { return; }
          for (var index in results) {
              if (!results.hasOwnProperty(index)) { continue; }
              var result = results[index];
              var selected = result.selected == 1 ? 'selected' : '';
              var option = '<option value="' + result.cid + '" ' + selected + '>' + result.category + '</option>';
              $(select).append(option);
          }
          $(select).selectmenu('refresh');
          if (results.length == 1) { $(select).hide(); }
        }
    });
  }
  catch (error) { console.log('contact_site_form_pageshow - ' + error); }
}

/**
 * The site wide contact form submit handler.
 * @param {Ojbect} form
 * @param {Ojbect} form_state
 */
function contact_site_form_submit(form, form_state) {
  var data = {
    name: form_state.values['name'],
    mail: form_state.values['mail'],
    subject: form_state.values['subject'],
    cid: form_state.values['cid'],
    message: form_state.values['message'],
    copy: form_state.values['copy']
  };
  contact_site({
      data: JSON.stringify(data),
      success: function(result) {
        if (result[0]) {
          drupalgap_alert(t('Your message has been sent!'));
        }
        else {
          drupalgap_alert(
            t('There was a problem sending your message!'),
            { title: t('Error') }
          );
        }
        drupalgap_form_clear();
      },
      error: function(xhr, status, message) {
        if (message) {
          message = JSON.parse(message);
          if (message.form_errors) {
            var errors = '';
            for (var element in message.form_errors) {
                if (!message.form_errors.hasOwnProperty(element)) { continue; }
                var error = message.form_errors[element];
                errors += error + '\n';
            }
            if (errors != '') { drupalgap_alert(errors); }
          }
        }
      }
  });
}

/**
 * The personal contact form.
 * @param {Object} form
 * @param {Object} form_state
 * @param {Number} recipient
 * @return {Object}
 */
function contact_personal_form(form, form_state, recipient) {
  try {
    // @TODO - when providing a personal contact form, make sure the user has
    // their personal contact form enabled.
    form.elements.name = {
      title: t('Your name'),
      type: 'textfield',
      required: true
    };
    form.elements.mail = {
      title: t('Your e-mail address'),
      type: 'email',
      required: true
    };
    form.elements.to = {
      type: 'hidden',
      required: true
    };
    var container_id = contact_personal_form_to_container_id(recipient);
    form.elements.to_display = {
      title: 'To',
      markup: '<div id="' + container_id + '"></div>'
    };
    form.elements.subject = {
      title: t('Subject'),
      type: 'textfield',
      required: true
    };
    form.elements.message = {
      title: t('Message'),
      type: 'textarea',
      required: true
    };
    form.elements.copy = {
      title: t('Send yourself a copy?'),
      type: 'checkbox',
      default_value: 0,
      access: false
    };
    form.elements.submit = {
      type: 'submit',
      value: t('Send message')
    };
    // If the user is logged in, set the default values.
    if (Drupal.user.uid != 0) {
      form.elements.name.default_value = Drupal.user.name;
      form.elements.name.disabled = true;
      form.elements.mail.default_value = Drupal.user.mail;
      form.elements.mail.disabled = true;
      form.elements.copy.access = true;
    }
    return form;
  }
  catch (error) { console.log('contact_personal_form - ' + error); }
}

/**
 * The pageshow callback for the personal contact form.
 * @param {Object} form
 * @param {Number} recipient
 */
function contact_personal_form_pageshow(form, recipient) {
  try {
    user_load(recipient, {
        success: function(account) {
          // Make sure the user has their contact form enabled.
          if (!account.data.contact) {
            $('#' + drupalgap_get_page_id() + ' #drupalgap_form_errors').html(
              "<div class='messages warning'>" +
                t("Sorry, this user's contact form is disabled.") +
              '</div>'
            );
            return;
          }
          // Populate hidden value for the 'to' field.
          var container_id = contact_personal_form_to_container_id(recipient);
          $('#' + container_id).html(l(account.name, 'user/' + account.uid));
          var hidden_selector = '#' + drupalgap_get_page_id() +
            ' #edit-contact-personal-form-to';
          $(hidden_selector).val(account.name);
        }
    });
  }
  catch (error) { console.log('contact_personal_form_pageshow - ' + error); }
}

/**
 * The personal contact form submit handler.
 * @param {Ojbect} form
 * @param {Ojbect} form_state
 */
function contact_personal_form_submit(form, form_state) {
  var data = {
    name: form_state.values['name'],
    mail: form_state.values['mail'],
    to: form_state.values['to'],
    subject: form_state.values['subject'],
    message: form_state.values['message'],
    copy: form_state.values['copy']
  };
  contact_personal({
      data: JSON.stringify(data),
      success: function(result) {
        if (result[0]) { drupalgap_alert(t('Your message has been sent!')); }
        else {
          drupalgap_alert(
            t('There was a problem sending your message!'),
            { title: t('Error') }
          );
        }
        drupalgap_form_clear();
      },
      error: function(xhr, status, message) {
        if (message) {
          message = JSON.parse(message);
          if (message.form_errors) {
            var errors = '';
            for (var element in message.form_errors) {
                if (!message.form_errors.hasOwnProperty(element)) { continue; }
                var error = message.form_errors[element];
                errors += error + '\n';
            }
            if (errors != '') { drupalgap_alert(errors); }
          }
        }
      }
  });
}

/**
 * Returns the div container id to use on the "to" markup on the personal
 * contact form.
 * @param {Number} recipient
 * @return {String}
 */
function contact_personal_form_to_container_id(recipient) {
    return 'contact_personal_form_user_' + recipient;
}


function drupalgap_entity_view_mode(entity_type, bundle) {
  var view_mode = 'drupalgap';
  if (typeof drupalgap.settings.view_modes !== 'undefined') {
    if (entity_type && bundle) {
      if (
        drupalgap.settings.view_modes[entity_type] &&
        drupalgap.settings.view_modes[entity_type][bundle] &&
        drupalgap.settings.view_modes[entity_type][bundle].view_mode
      ) { view_mode = drupalgap.settings.view_modes[entity_type][bundle].view_mode; }
    }
    else if (entity_type) {
      if (
          drupalgap.settings.view_modes[entity_type] &&
          drupalgap.settings.view_modes[entity_type].view_mode
      ) { view_mode = drupalgap.settings.view_modes[entity_type].view_mode; }
    }
  }
  return view_mode;
}
/**
 * Implements hook_install().
 */
function entity_install() {
  entity_clean_local_storage();
}

/**
 * Given an entity type, bundle name, form and entity, this will add the
 * entity's core fields to the form via the DrupalGap forms api.
 * @param {String} entity_type
 * @param {String} bundle
 * @param {Object} form
 * @param {Object} entity
 */
function drupalgap_entity_add_core_fields_to_form(entity_type, bundle,
  form, entity) {
  try {
    // Grab the core fields for this entity type and bundle.
    var fields = drupalgap_entity_get_core_fields(entity_type, bundle);
    // Iterate over each core field in the entity and add it to the form. If
    // there is a value present in the entity, then set the field's form element
    // default value equal to the core field value.
    for (var name in fields) {
      if (!fields.hasOwnProperty(name)) { continue; }
      var field = fields[name];
      var default_value = field.default_value;
      if (entity && entity[name]) { default_value = entity[name]; }
      form.elements[name] = field;
      form.elements[name].default_value = default_value;
    }
  }
  catch (error) {
    console.log('drupalgap_entity_add_core_fields_to_form - ' + error);
  }
}

/**
 * Deprecated! Given an entity type, the bundle, the entity (assembled from form
 * state values) and any options, this assembles the ?data= string for the
 * entity service resource call URLs.
 * @param {String} entity_type
 * @param {String} bundle
 * @param {Object} entity
 * @param {Object} options
 */
function drupalgap_entity_assemble_data(entity_type, bundle, entity, options) {
  try {
    console.log('WARNING: drupalgap_entity_assemble_data() has been ' +
      'deprecated! Now just call e.g. node_save() for auto assembly.');
    return;
  }
  catch (error) { console.log('drupalgap_entity_assemble_data - ' + error); }
}

/**
 * Returns the 'Delete' button object that is used on entity edit forms.
 * @param {String} entity_type
 * @param {Number} entity_id
 * @return {Object}
 */
function drupalgap_entity_edit_form_delete_button(entity_type, entity_id) {
    return {
      title: t('Delete'),
      attributes: {
        onclick: "javascript:drupalgap_entity_edit_form_delete_confirmation('" +
          entity_type + "', " + entity_id +
        ');'
      }
    };
}

/**
 * Given an entity type and id, this will display a confirmation dialogue and
 * will subsequently delete the entity if the user confirms the dialogue box.
 * The Services module retains Drupal user permissions so users without proper
 * permissions will not be able to delete the entities from the server.
 * @param {String} entity_type
 * @param {Number} entity_id
 * @return {*}
 */
function drupalgap_entity_edit_form_delete_confirmation(entity_type,
  entity_id) {
  try {
    var confirm_msg =
      t('Delete this content, are you sure? This action cannot be undone...');
    drupalgap_confirm(confirm_msg, {
        confirmCallback: function(button) {
          if (button == 2) { return; }
          // Change the jQM loader mode to deleting.
          drupalgap.loader = 'deleting';
          // Set up the api call arguments and success callback.
          var call_arguments = {};
          call_arguments.success = function(result) {
            // Remove the entities page from the DOM, if it exists.
            var entity_page_path = entity_type + '/' + entity_id;
            var entity_page_id = drupalgap_get_page_id(entity_page_path);
            if (drupalgap_page_in_dom(entity_page_id)) {
              drupalgap_remove_page_from_dom(entity_page_id);
            }
            // Remove the entity from local storage.
            // @todo - this should be moved to jDrupal.
            window.localStorage.removeItem(
              entity_local_storage_key(entity_type, entity_id)
            );
            // Go to the front page, unless a form action path was specified.
            var form = drupalgap_form_local_storage_load('node_edit');
            var destination = form.action ? form.action : '';
            drupalgap_goto(destination, {
              reloadPage: true,
              form_submission: true
            });
          };
          // Call the delete function.
          var name = services_get_resource_function_for_entity(
            entity_type,
            'delete'
          );
          var fn = window[name];
          fn(entity_id, call_arguments);
        }
    });
  }
  catch (error) {
    console.log('drupalgap_entity_edit_form_delete_confirmation - ' + error);
  }
}

/**
 * Given an entity, this will render the content of the entity and place it in
 * the entity JSON object as the 'content' property.
 * @param {String} entity_type
 * @param {Object} entity
 */
function drupalgap_entity_render_content(entity_type, entity) {
  try {
    entity.content = '';

    // Figure out the bundle.
    var bundle = entity.type;
    if (entity_type == 'comment') { bundle = entity.bundle; }
    else if (entity_type == 'taxonomy_term') { bundle = entity.vocabulary_machine_name; }

    // Load the field info for this entity and bundle combo.
    var field_info = drupalgap_field_info_instances(entity_type, bundle);
    if (!field_info) { return; }

    // Give modules a chance to pre build the content.
    module_invoke_all('entity_pre_build_content', entity, entity_type, bundle);

    // Render each field on the entity, using the drupalgap or default display.
    var field_weights = {};
    var field_displays = {};
    for (var field_name in field_info) {
        if (!field_info.hasOwnProperty(field_name)) { continue; }
        var field = field_info[field_name];

        // Determine which display mode to use. The default mode will be used if the drupalgap display mode is not
        // present, unless a view mode has been specified in settings.js then we'll use that config for the current
        // entity/bundle combo. If a module isn't listed on a custom display, use the default display's module.
        if (!field.display) { break; }
        var display = field.display['default'];
        var view_mode = drupalgap_entity_view_mode(entity_type, bundle);
        if (field.display[view_mode]) {
          display = field.display[view_mode];
          if (typeof display.module === 'undefined' && typeof field.display['default'].module !== 'undefined'
          ) { display.module = field.display['default'].module; }
        }

        // Skip hidden fields.
        if (display.type == 'hidden') { continue; }

        // Save the field display and weight. Use the weight from the field's render element if it's available,
        // otherwise fallback to the weight mentioned in the display.
        field_displays[field_name] = display;
        field_weights[field_name] = typeof entity[field_name].weight !== 'undefined' ?
            entity[field_name].weight : display.weight;
    }

    // Give modules a chance to alter the build content.
    module_invoke_all('entity_post_build_content', entity, entity_type, bundle);

    // Extract the field weights and sort them.
    var extracted_weights = [];
    for (var field_name in field_weights) {
        if (!field_weights.hasOwnProperty(field_name)) { continue; }
        var weight = field_weights[field_name];
        extracted_weights.push(weight);
    }
    extracted_weights.sort(function(a, b) { return a - b; });

    // Give modules a chance to pre alter the content.
    module_invoke_all('entity_pre_render_content', entity, entity_type, bundle);

    // For each sorted weight, locate the field with the corresponding weight,
    // then render it's field content.
    var completed_fields = [];
    for (var weight_index in extracted_weights) {
        if (!extracted_weights.hasOwnProperty(weight_index)) { continue; }
        var target_weight = extracted_weights[weight_index];
        for (var field_name in field_weights) {
            if (!field_weights.hasOwnProperty(field_name) || typeof entity[field_name] === 'undefined') { continue; }
            if (typeof entity[field_name].access !== 'undefined' && !entity[field_name].access) { continue; }
            var weight = field_weights[field_name];
            if (target_weight == weight) {
              if (completed_fields.indexOf(field_name) == -1) {
                completed_fields.push(field_name);
                entity.content += drupalgap_entity_render_field(
                  entity_type,
                  entity,
                  field_name,
                  field_info[field_name],
                  field_displays[field_name]
                );
                break;
              }
            }
        }
    }

    // Give modules a chance to alter the content.
    module_invoke_all('entity_post_render_content', entity, entity_type, bundle);

    // Update this entity in local storage so the content property sticks.
    if (entity_caching_enabled(entity_type, bundle)) {
      _entity_local_storage_save(
        entity_type,
        entity[entity_primary_key(entity_type)],
        entity
      );
    }

  }
  catch (error) {
    console.log('drupalgap_entity_render_content - ' + error);
  }
}

/**
 * Given an entity_type, the entity, a field name, and the field this will
 * render the field using the appropriate hook_field_formatter_view().
 * @param {String} entity_type
 * @param {Object} entity
 * @param {String} field_name
 * @param {Object} field
 * @param {*} display
 * @return {String}
 */
function drupalgap_entity_render_field(entity_type, entity, field_name,
  field, display) {
  try {
    var content = '';
    // Determine module that implements the hook_field_formatter_view,
    // then determine the hook's function name, then render the field content.
    // If there wasn't a module specified in the display, look to the module
    // specified in the field widget. If we still don't find it, then just
    // return.
    var module = display['module'];
    if (!module) {
      if (!field.widget.module) {
        var msg = 'drupalgap_entity_render_field - ' +
          'unable to locate the module for the field (' + field_name + ')';
        console.log(msg);
        return content;
      }
      else { module = field.widget.module; }
    }
    var function_name = module + '_field_formatter_view';
    if (drupalgap_function_exists(function_name)) {
      // Grab the field formatter function, then grab the field items
      // from the entity, then call the formatter function and append its result
      // to the entity's content.
      var fn = window[function_name];
      var items = null;
      // Check to see if translated content based on app's language setting
      // is present or not. If yes, then use that language as per setting.
      // Determine the language code. Note, multi lingual sites may have a
      // language code on the entity, but still have 'und' on the field, so
      // fall back to 'und' if the field's language code doesn't match the
      // entity's language code.

      var default_lang = language_default();
      var language = entity.language;
      if (entity[field_name]) {
        if (entity[field_name][default_lang]) {
          items = entity[field_name][default_lang];
        }
        else if (entity[field_name][language]) {
          items = entity[field_name][language];
        }
        else if (entity[field_name]['und']) {
          items = entity[field_name]['und'];
          language = 'und';
        }
        else { items = entity[field_name]; }
      }
      // @TODO - We've been sending 'field' as the instance
      // (drupalgap_field_info_instance), and the 'instance' as the field
      // (drupalgap_field_info_field). This is backwards, and should be
      // reversed. All contrib modules with field support will need to be
      // udpated to reflect this. Lame.
      var elements = fn(
        entity_type,
        entity,
        field, /* This is actually the instance, doh! (I think) */
        drupalgap_field_info_field(field_name),
        language,
        items,
        display
      );
      for (var delta in elements) {
          if (!elements.hasOwnProperty(delta)) { continue; }
          var element = elements[delta];
          // If the element has markup, render it as is, if it is
          // themeable, then theme it.
          var element_content = '';
          if (element.markup) { element_content = element.markup; }
          else if (element.theme) {
            element_content = theme(element.theme, element);
          }
          content += element_content;
      }
    }
    else {
      console.log(
        'WARNING: drupalgap_entity_render_field - ' + function_name + '() ' +
        'does not exist! (' + field_name + ')'
      );
    }
    // Render the field label, if necessary.
    if (content != '' && display['label'] != 'hidden') {
      var label = '<h3>' + field.label + '</h3>';
      // Place the label above or below the field content.
      label = '<div>' + label + '</div>';
      switch (display['label']) {
        case 'below':
          content += label;
          break;
        case 'above':
        default:
          content = label + content;
          break;
      }
    }
    // Finally, wrap the rendered field in a div, and set the field name as the
    // class name on the wrapper.
    content = '<div class="' + field_name + '">' + content + '</div>';
    // Give modules a chance to alter the field content.
    var reference = {'content': content};
    module_invoke_all(
      'entity_post_render_field', entity, field_name, field, reference
    );
    if (reference.content != content) { return reference.content; }
    return content;
  }
  catch (error) { console.log('drupalgap_entity_render_field - ' + error); }
}

/**
 * Given a form and form_state, this will assemble an entity from the form_state
 * values and return the entity as a JSON object.
 * @param {Object} form
 * @param {Object} form_state
 * @return {Object}
 */
function drupalgap_entity_build_from_form_state(form, form_state) {
  try {
    var entity = {};
    var language = language_default();
    for (var name in form_state.values) {
        if (!form_state.values.hasOwnProperty(name)) { continue; }
        var value = form_state.values[name];

        // Skip elements with restricted access.
        if (
          typeof form.elements[name].access !== 'undefined' &&
          !form.elements[name].access
        ) { continue; }

        // Determine wether or not this element is a field. If it is, determine
        // it's module and field assembly hook.
        var is_field = false;
        var module = false;
        var hook = false;
        if (form.elements[name].is_field) {
          is_field = true;
          module = form.elements[name].field_info_field.module;
          hook = module + '_assemble_form_state_into_field';
          if (!function_exists(hook)) { hook = false; }
        }

        // Retrieve the potential key for the element, if we don't get one
        // then it is a flat field that should be attached as a property to the
        // entity. Otherwise attach the key and value to the entity.
        var key = drupalgap_field_key(name); // e.g. value, fid, tid, nid, etc.
        if (key) {

          // Determine how many allowed values for this field.
          var allowed_values = form.elements[name].field_info_field.cardinality;

          // Convert unlimited value fields to one, for now...
          if (allowed_values == -1) { allowed_values = 1; }

          // Make sure there is at least one value before creating the form
          // element on the entity.
          if (typeof value[language][0] === 'undefined') { continue; }

          // Create an empty object to house the field on the entity.
          entity[name] = {};

          // Some fields do not use a delta value in the service call, so we
          // prepare for that here.
          // @todo - Do all options_select widgets really have no delta value?
          // Or is it only single value fields that don't have it? We need to
          // test this.
          var use_delta = true;
          if (
            form.elements[name].type ==
              'taxonomy_term_reference' ||
            form.elements[name].field_info_instance.widget.type ==
              'options_select'
          ) {
            use_delta = false;
            entity[name][language] = {};
          }
          else { entity[name][language] = []; }

          // Now iterate over each delta on the form element, and add the value
          // to the entity.
          for (var delta = 0; delta < allowed_values; delta++) {
            if (typeof value[language][delta] !== 'undefined') {

              // @TODO - the way values are determined here is turning into
              // spaghetti code. Every form element needs its own
              // value_callback, just like Drupal's FAPI. Right now DG has
              // something similar going on with the use of
              // hook_assemble_form_state_into_field(). So replace any spaghetti
              // below with a value_callback. Provide a deprecated hook warning
              // for any fields not haven't caught up yet, and fallback to the
              // hook for a while.
              // @UPDATE - Actually, the DG FAPI
              // hook_assemble_form_state_into_field() is a good idea, and
              // should be used by all field form elements, then in
              // drupalgap_field_info_instances_add_to_form(), that function
              // should use the value_callback idea to properly map entity data
              // to the form element's value.

              // Extract the value.
              var field_value = value[language][delta];

              // By default, we'll assume we'll be attaching this element item's
              // value according to a key (usually 'value' is the default key
              // used by Drupal fields). However, we'll give modules that
              // implement hook_assemble_form_state_into_field() an opportunity
              // to specify no usage of a key if their item doesn't need one.
              // The geofield module is an example of field that doesn't use a
              // key. The use_wrapper flag allows others to completely override
              // the use of a wrapper around the field value, e.g. taxonomy term
              // reference autocomplete. We'll attach any other helpful
              // variables here as well (element name, form id, etc).
              var field_key = {
                value: 'value',
                use_key: true,
                use_wrapper: true,
                use_delta: use_delta,
                name: name,
                form_id: form.id,
                element_id: form.elements[name][language][delta].id
              };

              // If this element is a field, give the field's module an
              // opportunity to assemble its own value, otherwise we'll just
              // use the field value extracted above.
              if (is_field && hook) {
                var fn = window[hook];
                field_value = fn(form.entity_type,
                  form.bundle,
                  field_value,
                  form.elements[name].field_info_field,
                  form.elements[name].field_info_instance,
                  language,
                  delta,
                  field_key,
                  form
                );
              }

              // If someone updated the key, use it.
              if (key != field_key.value) { key = field_key.value; }

              // If we don't need a delta value, place the field value using the
              // key, if posible. If we're using a delta value, push the key
              // and value onto the field to indicate the delta.
              if (!field_key.use_delta) {
                if (!field_key.use_wrapper) {
                  entity[name][language] = field_value;
                }
                else {
                  if ($.isArray(entity[name][language])) {
                    console.log(
                      'WARNING: drupalgap_entity_build_from_form_state - ' +
                      'cannot use key (' + key + ') on field (' + name + ') ' +
                      'language code array, key will be ignored.'
                    );
                    entity[name][language].push(field_value);
                  }
                  else { entity[name][language][key] = field_value; }
                }
              }
              else {
                if (field_key.use_key) {
                  var item = {};
                  item[key] = field_value;
                  entity[name][language].push(item);
                }
                else {
                  entity[name][language].push(field_value);
                }
              }

              // If the field value was null, we won't send along the field, so
              // just remove it. Except for list_boolean fields, they need a
              // null value to set the field value to false.
              if (
                field_value === null &&
                typeof entity[name] !== 'undefined' &&
                form.elements[name].type != 'list_boolean'
              ) {
                if (is_field) {
                  if (delta == 0) { delete entity[name]; }
                  else if (typeof entity[name][language][delta] !== 'undefined') {
                    delete entity[name][language][delta];
                  }
                }
                else { delete entity[name]; }
              }

              // If we had an optional select list, and no options were
              // selected, delete the empty field from the assembled entity.
              // @TODO - will this cause multi value issues?
              if (
                is_field && !use_delta &&
                form.elements[name].field_info_instance.widget.type ==
                  'options_select' && !form.elements[name].required &&
                field_value === '' && typeof entity[name] !== 'undefined'
              ) { delete entity[name]; }

            }
          }
      }
      else if (typeof value !== 'undefined') { entity[name] = value; }
    }
    return entity;
  }
  catch (error) {
    console.log('drupalgap_entity_build_from_form_state - ' + error);
  }
}

/**
 * Given a form, form_state and entity, this will call the appropriate service
 * resource to create or update the entity.
 * @param {Object} form
 * @param {Object} form_state
 * @param {Object} entity
 * @return {*}
 */
function drupalgap_entity_form_submit(form, form_state, entity) {
  try {

    // Grab the primary key name for this entity type.
    var primary_key = entity_primary_key(form.entity_type);

    // Determine if we are editing an entity or creating a new one.
    var editing = false;
    if (entity[primary_key] && entity[primary_key] != '') {
      editing = true;
    }

    // Let's set up the api call arguments.
    var call_arguments = {};

    // Setup the success call back to go back to the entity page view.
    call_arguments.success = function(result) {
      try {
        // If no one has provided a form.action to submit this form to,
        // by default we'll try to redirect to [entity-type]/[entity-id] to view
        // the entity. For taxonomy, we replace the underscore with a forward
        // slash in the path.
        var destination = form.action;
        if (!destination) {
          var prefix = form.entity_type;
          if (prefix == 'taxonomy_vocabulary' || prefix == 'taxonomy_term') {
            prefix = prefix.replace('_', '/');
          }
          destination = prefix + '/' + result[primary_key];
        }
        // Is there a destination URL query parameter overwriting the action?
        if (_GET('destination')) { destination = _GET('destination'); }
        // Set up the default goto options, and use any options provided by the
        // form.
        var goto_options = { form_submission: true };
        if (form.action_options) {
          goto_options = $.extend({}, goto_options, form.action_options);
        }
        // Finally goto our destination.
        drupalgap_goto(destination, goto_options);
      }
      catch (error) {
        console.log('drupalgap_entity_form_submit - success - ' + error);
      }
    };

    // Setup the error call back.
    call_arguments.error = function(xhr, status, message) {
      try {
        // If there were any form errors, display them in an alert.
        var msg = _drupalgap_form_submit_response_errors(form, form_state, xhr,
          status, message);
        if (msg) { drupalgap_alert(msg); }
      }
      catch (error) {
        console.log('drupalgap_entity_form_submit - error - ' + error);
      }
    };

    // Change the jQM loader mode to saving.
    drupalgap.loader = 'saving';

    // Depending on if we are creating a new entity, or editing an existing one,
    // call the appropriate service resource.
    var crud = 'create';
    if (editing) {
      crud = 'update';
      // Remove the entity from local storage.
      // @todo This should be moved to jDrupal.
      window.localStorage.removeItem(
        entity_local_storage_key(form.entity_type, entity[primary_key])
      );
    }
    var fn = window[
      services_get_resource_function_for_entity(form.entity_type, crud)
    ];
    fn(entity, call_arguments);
  }
  catch (error) { console.log('drupalgap_entity_form_submit - ' + error); }
}

/**
 * Given an entity type, this returns its core fields as forms api elements.
 * @param {String} entity_type
 * @param {String} bundle
 * @return {Object}
 */
function drupalgap_entity_get_core_fields(entity_type, bundle) {
  try {
    // @todo - was this function what we were tyring to accomplish with the
    // early entity_info hook imitations?
    // @todo - And why is this function not populated dynamically via Drupal?
    var fields = {};
    switch (entity_type) {
      case 'comment':
        var content_type = bundle.replace('comment_node_', '');
        // Add each schema field to the field collection.
        var base_table = drupalgap.entity_info[entity_type].schema_fields_sql['base table'];
        for (var index in base_table) {
            if (!base_table.hasOwnProperty(index)) { continue; }
            var name = base_table[index];
            var field = {
              type: 'hidden',
              required: false,
              default_value: '',
              title: ucfirst(name)
            };
            fields[name] = field;
        }
        // Make the node id required.
        fields['nid'].required = true;
        // Only anonymous users can fill out the name field, authenticated users
        // have their name auto filled and disabled.
        fields['name'].type = 'textfield';
        if (Drupal.user.uid != 0) {
          fields['name'].default_value = Drupal.user.name;
          fields['name'].disabled = true;
        }
        // If the 'Allow comment title' is enabled on the content type, show
        // the comment subject field.
        if (drupalgap.content_types_list[content_type].comment_subject_field) {
          fields['subject'].type = 'textfield';
        }
        // Depending on this content type's comment settings, let's make
        // modifications to the form elements.
        // admin/structure/types/manage/article
        // 0 = Anonymous posters may not enter their contact information
        // 1 = Anonymous posters may leave their contact information
        // 2 = Anonymous posters must leave their contact information
        var comment_anonymous =
          drupalgap.content_types_list[content_type].comment_anonymous;
        switch (comment_anonymous) {
          case '0':
            delete(fields['mail']);
            delete(fields['homepage']);
            break;
          case '1':
            break;
          case '2':
            fields['mail'].required = true;
            fields['homepage'].required = true;
            break;
        }
        // Only anonymous users get the mail and homepage fields.
        if (Drupal.user.uid == 0) {
          if (fields['mail']) { fields['mail'].type = 'textfield'; }
          if (fields['homepage']) { fields['homepage'].type = 'textfield'; }
        }
        break;
      case 'node':
        fields.nid = {
          'type': 'hidden',
          'required': false,
          'default_value': ''
        };
        fields.title = {
          'type': 'textfield',
          'title': t('Title'),
          'required': true,
          'default_value': '',
          'description': ''
        };
        fields.type = {
          'type': 'hidden',
          'required': true,
          'default_value': ''
        };
        fields.language = {
          'type': 'hidden',
          'required': true,
          'default_value': language_default()
        };
        break;
      case 'user':
        fields.uid = {
          'type': 'hidden',
          'required': false,
          'default_value': ''
        };
        fields.name = {
          'type': 'textfield',
          'title': t('Username'),
          'required': true,
          'default_value': '',
          'description': ''
        };
        fields.mail = {
          'type': 'email',
          'title': t('E-mail address'),
          'required': true,
          'default_value': '',
          'description': ''
        };
        fields.picture = {
          'type': 'image',
          'widget_type': 'imagefield_widget',
          'title': t('Picture'),
          'required': false,
          'value': t('Add Picture')
        };
        break;
      case 'taxonomy_term':
        fields = {
          'vid': {
            'type': 'hidden',
            'required': true,
            'default_value': ''
          },
          'tid': {
            'type': 'hidden',
            'required': false,
            'default_value': ''
          },
          'name': {
            'type': 'textfield',
            'title': t('Name'),
            'required': true,
            'default_value': ''
          },
          'description': {
            'type': 'textarea',
            'title': t('Description'),
            'required': false,
            'default_value': ''
          }
        };
        break;
      case 'taxonomy_vocabulary':
        fields = {
          'vid': {
            'type': 'hidden',
            'required': false,
            'default_value': ''
          },
          'name': {
            'type': 'textfield',
            'title': t('Name'),
            'required': true,
            'default_value': ''
          },
          'machine_name': {
            'type': 'textfield',
            'title': t('Machine Name'),
            'required': true,
            'default_value': ''
          },
          'description': {
            'type': 'textarea',
            'title': t('Description'),
            'required': false,
            'default_value': ''
          }
        };
        break;
      default:
        console.log(
          'drupalgap_entity_get_core_fields - entity type not supported yet (' +
            entity_type +
          ')'
        );
        break;
    }
    return fields;
  }
  catch (error) { console.log('drupalgap_entity_get_core_fields - ' + error); }
}

/**
 * Given an entity_type, this returns the entity JSON info, if it exists, false
 * otherwise. You may optionally call this function with no arguments to
 * retrieve the JSON info for all entity types. See also
 * @see http://api.drupal.org/api/drupal/includes%21common.inc/function/entity_get_info/7
 * @return {Object|Boolean}
 */
function drupalgap_entity_get_info() {
  try {
    if (arguments[0]) {
      var entity_type = arguments[0];
      if (entity_type && drupalgap.entity_info[entity_type]) {
        return drupalgap.entity_info[entity_type];
      }
      else {
        return false;
      }
    }
    return drupalgap.entity_info;
  }
  catch (error) { console.log('drupalgap_entity_get_info - ' + error); }
}

/**
 * @deprecated Since 7.x-1.7-alpha you should use entity_primary_key() instead.
 * Given an entity type, this returns the primary key identifier for it.
 * @param {String} entity_type
 * @return {String}
 */
function drupalgap_entity_get_primary_key(entity_type) {
  try {
    console.log(
      'WARNING: drupalgap_entity_get_primary_key() is deprecated! ' +
      'Use entity_primary_key() instead.'
    );
    return entity_primary_key(entity_type);
  }
  catch (error) { console.log('drupalgap_entity_get_primary_key - ' + error); }
}

/**
 * Given an entity type, an entity id and a mode, this will return a render
 * object for the entity's page container.
 * @param {String} entity_type
 * @param {Number} entity_id
 * @param {String} mode
 * @return {Object}
 */
function _drupalgap_entity_page_container(entity_type, entity_id, mode) {
  try {
    var id = _drupalgap_entity_page_container_id(entity_type, entity_id, mode);
    var attrs = {
      id: id,
      'class': entity_type + ' ' + entity_type + '-' + mode
    };
    return {
      markup: '<div ' + drupalgap_attributes(attrs) + '></div>'
    };
  }
  catch (error) { console.log('_drupalgap_entity_page_container - ' + error); }
}

/**
 * Given an entity type, an entity id, and a mode, this will return the unique
 * id to be used for the entity's page container.
 * @param {String} entity_type
 * @param {Number} entity_id
 * @param {String} mode
 * @return {String}
 */
function _drupalgap_entity_page_container_id(entity_type, entity_id, mode) {
    return entity_type + '_' + entity_id + '_' + mode + '_container';
}

/**
 * Given an entity type, id, mode and page build, this will render the page
 * build and inject it into the container on the page.
 * @param {String} entity_type
 * @param {Number} entity_id
 * @param {String} mode
 * @param {Object} build
 */
function _drupalgap_entity_page_container_inject(entity_type, entity_id, mode, build) {
  try {
    // Get the container id, set the drupalgap.output to the page build, then
    // inject the rendered page into the container.
    var id = _drupalgap_entity_page_container_id(entity_type, entity_id, mode);
    module_invoke_all('entity_view_alter', entity_type, entity_id, mode, build);
    drupalgap.output = build;
    $('#' + id).html(drupalgap_render_page()).trigger('create');
    _drupalgap_entity_page_add_css_class_names(entity_type, entity_id, build);
  }
  catch (error) {
    console.log('_drupalgap_entity_page_container_inject - ' + error);
  }
}

/**
 * An internal function used to add css class names to an entity's jQM page container.
 * @param {String} entity_type
 * @param {Number} entity_id
 * @param {Object} build
 * @private
 */
function _drupalgap_entity_page_add_css_class_names(entity_type, entity_id, build) {
  try {
    var className = entity_type;
    var bundleName = entity_get_bundle(entity_type, build[entity_type]);
    if (bundleName) { className += '-' + bundleName; }
    className += ' ' + entity_type.replace(/_/g, '-') + '-' + entity_id;
    $('#' + drupalgap_get_page_id()).addClass(className);
  }
  catch (error) { console.log('_drupalgap_entity_page_add_css_class_names - ' + error); }
}

/**
 * The page callback for entity edit forms.
 * @param {String} form_id
 * @param {String} entity_type
 * @param {Number} entity_id
 * @return {Object}
 */
function entity_page_edit(form_id, entity_type, entity_id) {
  try {
    var content = {
      container: _drupalgap_entity_page_container(
        entity_type,
        entity_id,
        'edit'
      )
    };
    return content;
  }
  catch (error) { console.log('entity_page_edit - ' + error); }
}

/**
 * The pageshow callback for entity edit forms.
 * @param {String} form_id
 * @param {String} entity_type
 * @param {Number} entity_id
 */
function entity_page_edit_pageshow(form_id, entity_type, entity_id) {
  try {
    entity_load(entity_type, entity_id, {
        success: function(entity) {
          _drupalgap_entity_page_container_inject(
            entity_type,
            entity_id,
            'edit',
            drupalgap_get_form(form_id, entity)
          );
        }
    });
  }
  catch (error) { console.log('entity_page_edit_pageshow - ' + error); }
}

/**
 * Returns an entity type's primary title key.
 * @param {String} entity_type
 * @return {String}
 */
function entity_primary_key_title(entity_type) {
  try {
    var key;
    switch (entity_type) {
      case 'comment': key = 'subject'; break;
      case 'file': key = 'filename'; break;
      case 'node': key = 'title'; break;
      case 'taxonomy_term': key = 'name'; break;
      case 'taxonomy_vocabulary': key = 'name'; break;
      case 'user': key = 'name'; break;
      default:
        console.log(
          'entity_primary_key_title - unsupported entity type (' +
            entity_type +
          ')'
        );
        break;
    }
    return key;
  }
  catch (error) { console.log('entity_primary_key_title - ' + error); }
}

/**
 * Implements hook_services_request_pre_postprocess_alter().
 * @param {Object} options
 * @param {*} result
 */
function entity_services_request_pre_postprocess_alter(options, result) {
  try {
    // If we're retrieving an entity, render the entity's content, if it isn't
    // already set.
    if (
      options.resource == 'retrieve' &&
      in_array(options.service, entity_types()
    )) {
      // @TODO - does this condition ever evaluate to true?
      if (typeof result.content !== 'undefined') { return; }
      drupalgap_entity_render_content(options.service, result);
    }
    // If we're indexing comments, render its content, if it isn't already set.
    else if (options.service == 'comment' && options.resource == 'index') {
      for (var index in result) {
          if (!result.hasOwnProperty(index)) { continue; }
          var object = result[index];
          // @TODO - does this condition ever evaluate to true?
          if (typeof object.content !== 'undefined') { continue; }
          drupalgap_entity_render_content(options.service, result[index]);
      }
    }
  }
  catch (error) {
    console.log('entity_services_request_pre_postprocess_alter - ' + error);
  }
}


/**
 * Given a field name, this will return its field info.
 * @param {String} field_name
 * @return {Object}
 */
function drupalgap_field_info_field(field_name) {
  try {
    return drupalgap.field_info_fields[field_name];
  }
  catch (error) { console.log('drupalgap_field_info_field - ' + error); }
}

/**
 * Returns info on all fields.
 * @return {Object}
 */
function drupalgap_field_info_fields() {
  try {
    return drupalgap.field_info_fields;
  }
  catch (error) { console.log('drupalgap_field_info_fields - ' + error); }
}

/**
 * Given an entity type, field name, and bundle name this will return a JSON
 * object with data for the specified field name.
 * @param {String} entity_type
 * @param {String} field_name
 * @param {String} bundle_name
 * @return {Object}
 */
function drupalgap_field_info_instance(entity_type, field_name, bundle_name) {
  try {
    var instances = drupalgap_field_info_instances(entity_type, bundle_name);
    if (!instances) {
      var msg = 'WARNING: drupalgap_field_info_instance - instance was null ' +
      'for entity (' + entity_type + ') bundle (' + bundle_name + ') using ' +
      'field (' + field_name + ')';
      console.log(msg);
      return null;
    }
    if (!instances[field_name]) {
      var msg = 'WARNING: drupalgap_field_info_instance - ' +
        '"' + field_name + '" does not exist for entity (' + entity_type + ')' +
        ' bundle (' + bundle_name + ')';
      console.log(msg);
      return null;
    }
    return instances[field_name];
  }
  catch (error) { console.log('drupalgap_field_info_instance - ' + error); }
}

/**
 * Given an entity type and/or a bundle name, this returns the field info
 * instances for the entity or the bundle.
 * @param {String} entity_type
 * @param {String} bundle_name
 * @return {Object}
 */
function drupalgap_field_info_instances(entity_type, bundle_name) {
  try {
    var field_info_instances;
    // If there is no bundle, pull the fields out of the wrapper.
    // @TODO there appears to be a special case with commerce_products, in that
    // they aren't wrapped like normal entities (see the else statement when a
    // bundle name isn't present). Or do we have a bug here, and we shouldn't
    // be expecting the wrapper in the first place?
    if (!bundle_name) {
      if (entity_type == 'commerce_product') {
        field_info_instances =
          drupalgap.field_info_instances[entity_type];
      }
      else {
        field_info_instances =
          drupalgap.field_info_instances[entity_type][entity_type];
      }
    }
    else {
      if (typeof drupalgap.field_info_instances[entity_type] !== 'undefined') {
        field_info_instances =
          drupalgap.field_info_instances[entity_type][bundle_name];
      }
    }
    return field_info_instances;
  }
  catch (error) { console.log('drupalgap_field_info_instances - ' + error); }
}

/**
 * Given an entity type, bundle, form and entity, this will add the
 * entity's fields to the given form.
 * @param {String} entity_type
 * @param {String} bundle
 * @param {Object} form
 * @param {Object} entity
 */
function drupalgap_field_info_instances_add_to_form(entity_type, bundle, form, entity) {
  try {
    // Grab the field info instances for this entity type and bundle.
    var fields = drupalgap_field_info_instances(entity_type, bundle);
    // If there is no bundle, pull the fields out of the wrapper.
    //if (!bundle) { fields = fields[entity_type]; }
    // Use the default language, unless the entity has one specified.
    var language = language_default();
    if (entity && entity.language) { language = entity.language; }
    // Iterate over each field in the entity and add it to the form. If there is
    // a value present in the entity, then set the field's form element default
    // value equal to the field value.
    if (fields) {
      for (var name in fields) {
        if (!fields.hasOwnProperty(name)) { continue; }
        var field = fields[name];
        // The user registration form is a special case, in that we only want
        // to place fields that are set to display on the user registration
        // form. Skip any fields not set to display.
        if (form.id == 'user_register_form' &&
          !field.settings.user_register_form) {
          continue;
        }
        var field_info = drupalgap_field_info_field(name);
        if (field_info) {
          form.elements[name] = {
            type: field_info.type,
            title: field.label,
            required: field.required,
            description: field.description
          };
          var default_value = field.default_value;
          var cardinality = parseInt(field_info.cardinality);
          if (cardinality == -1) {
            cardinality = 1; // we'll just add one element for now, until we
                             // figure out how to handle the 'add another
                             // item' feature.
          }
          if (entity && entity[name] && entity[name].length != 0) {

            // Make sure the field has some type of language code on it, or just skip it. An entity will sometimes have
            // a language code that a field doesn't have, so fall back to und on the field if the language code isn't
            // present.
            if (!entity[name][language]) {
              if (!entity[name].und) { continue; }
              language = 'und';
            }

            if (!form.elements[name][language]) { form.elements[name][language] = {}; }

            for (var delta = 0; delta < cardinality; delta++) {

              // @TODO - is this where we need to use the idea of the
              // value_callback property present in Drupal's FAPI? That way
              // each element knows how to map the entity data to its element
              // value property.
              if (
                entity[name][language][delta] &&
                typeof entity[name][language][delta].value !== 'undefined'
              ) { default_value = entity[name][language][delta].value; }

              // If the default_value is null, set it to an empty string.
              if (default_value == null) { default_value = ''; }

              // Note, not all fields have a language code to use here, e.g. taxonomy term reference fields do not.
              form.elements[name][language][delta] = {
                value: default_value
              };

              // Place the field item onto the element.
              if (entity[name][language][delta]) {
                form.elements[name][language][delta].item =
                  entity[name][language][delta];
              }

            }

            // Set the language back to the entity's language in case it was temporarily changed because of an un
            // translated field.
            if (entity && entity.language) { language = entity.language; }

          }

          // Give module's a chance to alter their own element during the form
          // build, that way element properties will be saved to local storage
          // and then available during hook_field_widget_form() and the form
          // submission process.
          var fn = field.widget.module + '_field_info_instance_add_to_form';
          if (drupalgap_function_exists(fn)) {
            window[fn](entity_type, bundle, form, entity, form.elements[name]);
          }

        }
      }
    }
  }
  catch (error) {
    console.log('drupalgap_field_info_instances_add_to_form - ' + error);
  }
}

/**
 * Given a field name, this will return the key that should be used when
 * setting its value on an entity. If the field name is not a field, it returns
 * false.
 * @param {String} field_name
 * @return {String}
 */
function drupalgap_field_key(field_name) {
  try {
    // Determine the key to use for the value. By default, most fields
    // use 'value' as the key.
    var key = false;
    var field_info = drupalgap_field_info_field(field_name);
    if (field_info) {
      key = 'value';
      // Images use fid as the key.
      if (field_info.module == 'image' && field_info.type == 'image') {
        key = 'fid';
      }
      else if (
        field_info.module == 'taxonomy' &&
        field_info.type == 'taxonomy_term_reference'
      ) { key = 'tid'; }
    }
    return key;
  }
  catch (error) { console.log('drupalgap_field_key - ' + error); }
}

/**
 * Implements hook_field_formatter_view().
 * @param {String} entity_type
 * @param {Object} entity
 * @param {Object} field
 * @param {Object} instance
 * @param {String} langcode
 * @param {Object} items
 * @param {*} display
 * @return {Object}
 */
function list_field_formatter_view(entity_type, entity, field, instance, langcode, items, display) {
  try {
    var element = {};
    if (!empty(items)) {
      for (var delta in items) {
          if (!items.hasOwnProperty(delta)) { continue; }
          var item = items[delta];
          var markup = '';
          // list_default or list_key
          if (display.type == 'list_default') {
            markup = instance.settings.allowed_values[item.value];
            // Single on/off checkboxes need an empty space as their markup so
            // just the label gets rendered.
            if (
              instance.type == 'list_boolean' &&
              field.widget.type == 'options_onoff'
            ) { markup = '&nbsp;'; }
          }
          else { markup = item.value; }
          element[delta] = { markup: markup };
      }
    }
    return element;
  }
  catch (error) { console.log('list_field_formatter_view - ' + error); }
}

/**
 * Implements hook_assemble_form_state_into_field().
 * @param {Object} entity_type
 * @param {String} bundle
 * @param {String} form_state_value
 * @param {Object} field
 * @param {Object} instance
 * @param {String} langcode
 * @param {Number} delta
 * @param {Object} field_key
 *
 * @return {*}
 */
function list_assemble_form_state_into_field(entity_type, bundle, form_state_value, field, instance, langcode, delta, field_key) {
  try {
    var result = form_state_value;
    switch (field.type) {
      case 'list_boolean':
        // For single on/off checkboxes, if the checkbox is unchecked, then we
        // send a null value on the language code. We know the checkbox is
        // unchecked if the form_state_value is equal to the first allowed value
        // on the field.
        if (instance.widget.type == 'options_onoff') {
          var index = 0;
          var on = true;
          for (var value in field.settings.allowed_values) {
              if (!field.settings.allowed_values.hasOwnProperty(value)) { continue; }
              var label = field.settings.allowed_values[value];
              if (form_state_value == value && index == 0) {
                on = false;
                break;
              }
              index++;
          }
          if (!on) {
            field_key.use_delta = false;
            field_key.use_wrapper = false;
            result = null;
          }
        }
        else {
          console.log(
            'WARNING: list_assemble_form_state_into_field - unknown widget (' +
              field.type +
            ') on list_boolean'
          );
        }
        break;
      case 'list_text':
        // For radio buttons on the user entity form, field values must be
        // "flattened", i.e. this field_foo: { und: [ { value: 123 }]}, should
        // be turned into field_foo: { und: 123 }
        if (
          entity_type == 'user' &&
          instance.widget.type == 'options_buttons'
        ) {
          field_key.use_delta = false;
          field_key.use_wrapper = false;
        }
        break;
      default:
        console.log(
          'WARNING: list_assemble_form_state_into_field - unknown type (' +
            field.type +
          ')'
        );
        break;
    }
    return result;
  }
  catch (error) {
    console.log('list_assemble_form_state_into_field - ' + error);
  }
}

/**
 * Implements hook_views_exposed_filter().
 * @param {Object} form
 * @param {Object} form_state
 * @param {Object} element
 * @param {Object} filter
 * @param {Object} field
 */
function list_views_exposed_filter(form, form_state, element, filter, field) {
  try {

    //console.log('list_views_exposed_filter');
    //console.log(form);
    //console.log(form_state);
    //console.log(element);
    //console.log(filter);
    //console.log(field);

    var widget = filter.options.group_info.widget;

    // List fields.
    if (widget == 'select') {

      // Set the element value if we have one in the filter.
      if (!empty(filter.value)) { element.value = filter.value[0]; }

      // Set the options, then depending on whether or not it is required, set
      // the default value accordingly.
      element.options = filter.value_options;
      if (!element.required) {
        element.options['All'] = '- ' + t('Any') + ' -';
        if (typeof element.value === 'undefined') { element.value = 'All'; }
      }

    }
    else {
      console.log('WARNING: list_views_exposed_filter - unsupported widget:' + widget);
    }
  }
  catch (error) { console.log('list_views_exposed_filter - ' + error); }
}

/**
 * Implements hook_field_formatter_view().
 * @param {String} entity_type
 * @param {Object} entity
 * @param {Object} field
 * @param {Object} instance
 * @param {String} langcode
 * @param {Object} items
 * @param {*} display
 * @return {Object}
 */
function number_field_formatter_view(entity_type, entity, field, instance, langcode, items, display) {
  try {
    var element = {};
    // If items is a string, convert it into a single item JSON object.
    if (typeof items === 'string') {
      items = {0: {value: items}};
    }
    if (!empty(items)) {
      var prefix = '';
      if (!empty(field.settings.prefix)) { prefix = field.settings.prefix; }
      var suffix = '';
      if (!empty(field.settings.suffix)) { suffix = field.settings.suffix; }
      for (var delta in items) {
          if (!items.hasOwnProperty(delta)) { continue; }
          var item = items[delta];
          element[delta] = {
            markup: prefix + item.value + suffix
          };
      }
    }
    return element;
  }
  catch (error) { console.log('number_field_formatter_view - ' + error); }
}

/**
 * Implements hook_field_widget_form().
 * @param {Object} form
 * @param {Object} form_state
 * @param {Object} field
 * @param {Object} instance
 * @param {String} langcode
 * @param {Object} items
 * @param {Number} delta
 * @param {Object} element
 */
function number_field_widget_form(form, form_state, field, instance, langcode, items, delta, element) {
  try {
    switch (element.type) {
      case 'number_integer':
      case 'number_float':
      case 'number_decimal':
      case 'range':
        // Change the form element into a number, unless we're using a range
        // slider. Then set its min/max attributes along with the step.
        if (element.type != 'range') { items[delta].type = 'number'; }
        if (!empty(instance.settings.max)) {
          items[delta].options.attributes['min'] = instance.settings.min;
        }
        if (!empty(instance.settings.max)) {
          items[delta].options.attributes['max'] = instance.settings.max;
        }
        var step = 1;
        if (element.type == 'number_float') { step = 0.01; }
        if (element.type == 'number_decimal') { step = 0.01; }
        items[delta].options.attributes['step'] = step;
        break;
      default:
        console.log(
          'number_field_widget_form - element type not supported (' +
            element.type +
          ')'
        );
        break;
    }
  }
  catch (error) { console.log('number_field_widget_form - ' + error); }
}

/**
 * Implements hook_field_widget_form().
 * @param {Object} form
 * @param {Object} form_state
 * @param {Object} field
 * @param {Object} instance
 * @param {String} langcode
 * @param {Object} items
 * @param {Number} delta
 * @param {Object} element
 * @return {*}
 */
function options_field_widget_form(form, form_state, field, instance, langcode, items, delta, element) {
  try {
    switch (element.type) {
      case 'checkbox':
        // If the checkbox has a default value of 1, check the box.
        if (items[delta].default_value == 1) { items[delta].checked = true; }
        break;
      case 'radios':
        break;
      case 'list_boolean':
        switch (instance.widget.type) {
          case 'options_onoff':
            // Switch an on/off boolean to a checkbox and place its on/off
            // values as attributes. Depending on the allowed values, we may
            // have to iterate over an array, or an object to get the on/off
            // values.
            items[delta].type = 'checkbox';
            var off = null;
            var on = null;
            if ($.isArray(field.settings.allowed_values)) {
              for (var key in field.settings.allowed_values) {
                if (off === null) { off = key; }
                else { on = key; }
              }
            }
            else {
              for (var value in field.settings.allowed_values) {
                  if (!field.settings.allowed_values.hasOwnProperty(value)) { continue; }
                  var label = field.settings.allowed_values[value];
                  if (off === null) { off = value; }
                  else { on = value; }
              }
            }
            items[delta].options.attributes['off'] = off;
            items[delta].options.attributes['on'] = on;
            // If the value equals the on value, then check the box.
            if (
              typeof items[delta] !== 'undefined' && items[delta].value == on
            ) { items[delta].options.attributes['checked'] = 'checked'; }
            break;
          default:
            console.log(
              'WARNING: options_field_widget_form list_boolean with ' +
              'unsupported type (' + instance.widget.type + ')'
            );
            break;
        }
        break;
      case 'select':
      case 'list_text':
      case 'list_float':
      case 'list_integer':
        if (instance) {
          switch (instance.widget.type) {
            case 'options_select':
              items[delta].type = 'select';
              // If the select list is required, add a 'Select' option and set
              // it as the default.  If it is optional, place a "none" option
              // for the user to choose from.
              var text = '- None -';
              if (items[delta].required) {
                text = '- ' + t('Select a value') + ' -';
              }
              items[delta].options[''] = text;
              if (empty(items[delta].value)) { items[delta].value = ''; }
              // If more than one value is allowed, turn it into a multiple
              // select list.
              if (field.cardinality != 1) {
                items[delta].options.attributes['data-native-menu'] = 'false';
                items[delta].options.attributes['multiple'] = 'multiple';
              }
              break;
            case 'options_buttons':
              // If there is one value allowed, we turn this into radio
              // button(s), otherwise they will become checkboxes.
              var type = 'checkboxes';
              if (field.cardinality == 1) { type = 'radios'; }
              items[delta].type = type;
              break;
            default:
              console.log(
                'WARNING: options_field_widget_form - unsupported widget (' +
                instance.widget.type + ')'
              );
              return false;
              break;
          }
          // If there are any allowed values, place them on the options
          // list. Then check for a default value, and set it if necessary.
          if (field && field.settings.allowed_values) {
            for (var key in field.settings.allowed_values) {
                if (!field.settings.allowed_values.hasOwnProperty(key)) { continue; }
                var value = field.settings.allowed_values[key];
                // Don't place values that are objects onto the options
                // (i.e. commerce taxonomy term reference fields).
                if (typeof value === 'object') { continue; }
                // If the value already exists in the options, then someone
                // else has populated the list (e.g. commerce), so don't do
                // any processing.
                if (typeof items[delta].options[key] !== 'undefined') {
                  break;
                }
                // Set the key and value for the option.
                items[delta].options[key] = value;
            }
            if (instance.default_value && instance.default_value[delta] &&
              typeof instance.default_value[delta].value !== 'undefined') {
                items[delta].value = instance.default_value[delta].value;
            }
          }
        }
        break;
      case 'taxonomy_term_reference':
          // Change the item type to a hidden input.
          items[delta].type = 'hidden';
          // What vocabulary are we using?
          var machine_name = field.settings.allowed_values[0].vocabulary;
          var taxonomy_vocabulary =
            taxonomy_vocabulary_machine_name_load(machine_name);

          var widget_type = false;
          if (instance.widget.type == 'options_select') {
            widget_type = 'select';
          }
          else {
            console.log(
              'WARNING: options_field_widget_form() - ' + instance.widget.type +
              ' not yet supported for ' + element.type + ' form elements!'
            );
            return false;
          }
          var widget_id = items[delta].id + '-' + widget_type;
          // If the select list is required, add a 'Select' option and set
          // it as the default.  If it is optional, place a "none" option
          // for the user to choose from.
          var text = '- ' + t('None') + ' -';
          if (items[delta].required) {
            text = '- ' + t('Select a value') + ' -';
          }
          items[delta].children.push({
              type: widget_type,
              attributes: {
                id: widget_id,
                onchange: "_theme_taxonomy_term_reference_onchange(this, '" +
                  items[delta].id +
                "');"
              },
              options: { '': text }
          });
          // Attach a pageshow handler to the current page that will load the
          // terms into the widget.
          var options = {
            'page_id': drupalgap_get_page_id(drupalgap_path_get()),
            'jqm_page_event': 'pageshow',
            'jqm_page_event_callback':
              '_theme_taxonomy_term_reference_load_items',
            'jqm_page_event_args': JSON.stringify({
                'taxonomy_vocabulary': taxonomy_vocabulary,
                'widget_id': widget_id
            })
          };
          // Pass the field name so the page event handler can be called for
          // each item.
          items[delta].children.push({
              markup: drupalgap_jqm_page_event_script_code(
                  options,
                  field.field_name
              )
          });
        break;
      default:
          var msg = 'options_field_widget_form - unknown widget type: ' + element.type;
          console.log(msg);
        break;
    }
  }
  catch (error) { console.log('options_field_widget_form - ' + error); }
}

/**
 * Implements hook_field_formatter_view().
 * @param {String} entity_type
 * @param {Object} entity
 * @param {Object} field
 * @param {Object} instance
 * @param {String} langcode
 * @param {Object} items
 * @param {*} display
 * @return {Object}
 */
function text_field_formatter_view(entity_type, entity, field, instance, langcode, items, display) {
  try {
    var element = {};
    if (items.length) {
      for (var delta in items) {
        if (!items.hasOwnProperty(delta)) { continue; }

        // Grab the item, then grab the field value or its safe_value if we have it.
        var item = items[delta];
        var value = typeof item.safe_value !== 'undefined' ? item.safe_value : item.value;

        // Any trim?
        if (display.type == 'text_summary_or_trimmed') {
          var length = display.settings.trim_length;
          value = value.length > length ? value.substring(0, length - 3) + "..." : value.substring(0, length);
        }

        element[delta] = { markup: value };
      }
    }
    return element;
  }
  catch (error) { console.log('text_field_formatter_view - ' + error); }
}

/**
 * Implements hook_field_widget_form().
 * @param {Object} form
 * @param {Object} form_state
 * @param {Object} field
 * @param {Object} instance
 * @param {String} langcode
 * @param {Object} items
 * @param {Number} delta
 * @param {Object} element
 */
function text_field_widget_form(form, form_state, field, instance, langcode, items, delta, element) {
  try {
    // Determine the widget type, then set the delta item's type property.
    var type = null;
    switch (element.type) {
      case 'search': type = 'search'; break;
      case 'text': type = 'textfield'; break;
      case 'textarea':
      case 'text_long':
      case 'text_with_summary':
      case 'text_textarea':
        type = 'textarea';
        break;
    }
    items[delta].type = type;

    // If the item has a value and its attribute value hasn't yet been set, then set the attribute value.
    if (
        typeof items[delta].value !== 'undefined' &&
        typeof items[delta].options.attributes.value === 'undefined'
    ) { items[delta].options.attributes.value = items[delta].value; }
  }
  catch (error) { console.log('text_field_widget_form - ' + error); }
}


/**
 * Implements hook_field_formatter_view().
 */
function file_field_formatter_view(entity_type, entity, field, instance, langcode, items, display) {
  try {

    //console.log(entity_type);
    //console.log(entity);
    //console.log(field);
    //console.log(instance);
    //console.log(langcode);
    //console.log(items);
    //console.log(display);

    // Iterate over each item, and place a widget onto the render array.
    var content = {};
    for (var delta in items) {
      if (!items.hasOwnProperty(delta)) { continue; }
      var item = items[delta];
      switch (display.type) {

        case 'file_table':

            // Instantiate the table only once.
            if (!content['file_table']) {
              content['file_table'] = {
                theme: 'jqm_table',
                header: [{ data: t('Attachment') }, { data: t('Size') }],
                rows: [],
                attributes: {
                  border: 1
                }
              };
            }

            // Build the path to the file.
            var file_path = drupalgap_image_path(item.uri);

            // Android can't open .pdf files in the in app browser, so have
            // Google Docs open it instead.
            if (item.filemime == 'application/pdf' &&
              typeof device !== 'undefined' && device.platform == 'Android'
            ) { file_path = 'https://docs.google.com/gview?embedded=true&url=' + file_path; }

            // Add the row to the table.
            content.file_table.rows.push([
              l(item.filename, file_path, { InAppBrowser: true }),
              Math.round(item.filesize/1000).toFixed(2) + ' KB'
            ]);

          break;

        default:
          console.log('file_field_formatter_view() - unsupported display type: ' + display.type);
          break;
      }
    }
    return content;
  }
  catch (error) { console.log('file_field_formatter_view - ' + error); }
}

/**
 * Implements hook_field_formatter_view().
 */
function file_entity_field_formatter_view(entity_type, entity, field, instance, langcode, items, display) {
  try {
    //console.log(entity_type);
    //console.log(entity);
    //console.log(field);
    //console.log(instance);
    //console.log(langcode);
    //console.log(items);
    //console.log(display);

    // Special case for media module.
    if (display.type == 'file_rendered') {
      return media_field_formatter_view(entity_type, entity, field, instance, langcode, items, display);
    }

    // Iterate over each item, and place a widget onto the render array.
    var content = {};
    for (var delta in items) {
        if (!items.hasOwnProperty(delta)) { continue; }
        var item = items[delta];

        switch (display.type) {
          default:
            console.log('file_entity_field_formatter_view() - unsupported display type: ' + display.type);
            break;
        }
    }
    return content;
  }
  catch (error) { console.log('file_entity_field_formatter_view - ' + error); }
}



// Holds onto the phonegap getPicture success image data. It is keyed by field
// name, then delta value.
var image_phonegap_camera_options = {};

/**
 * Implements hook_field_formatter_view().
 /**
 * Implements hook_field_formatter_view().
 * @param {String} entity_type
 * @param {Object} entity
 * @param {Object} field
 * @param {Object} instance
 * @param {String} langcode
 * @param {Object} items
 * @param {*} display
 * @return {Object}
 */
function image_field_formatter_view(entity_type, entity, field, instance,
  langcode, items, display) {
  try {
    var element = {};
    // Toss on the default image if we one is specified and we have no items.
    // "In addition, any code which programmatically generates a link to an
    // image derivative without using the standard image_style_url() API
    // function will no longer work correctly if the image does not already
    // exist in the file system, since the necessary token will not be present
    // in the URL." @see http://drupal.stackexchange.com/a/76827/10645
    if (empty(items) && instance.settings.default_image) {
      items = [{
          uri: instance.settings.default_image_uri
      }];
    }
    if (!empty(items)) {
      for (var delta in items) {
          if (!items.hasOwnProperty(delta)) { continue; }
          var item = items[delta];
          var theme = empty(display.settings.image_style) ?
            'image' : 'image_style';
          var image = {
            theme: theme,
            alt: item.alt,
            title: item.title
          };
          if (theme == 'image_style') {
            image.style_name = display.settings.image_style;
            image.path = item.uri;
          }
          else { image.path = drupalgap_image_path(item.uri); }
          element[delta] = image;
      }
    }
    return element;
  }
  catch (error) { console.log('image_field_formatter_view - ' + error); }
}

/**
 * Implements hook_field_widget_form().
 * @param {Object} form
 * @param {Object} form_state
 * @param {Object} field
 * @param {Object} instance
 * @param {String} langcode
 * @param {Object} items
 * @param {Number} delta
 * @param {Object} element
 */
function image_field_widget_form(form, form_state, field, instance, langcode,
  items, delta, element) {
  try {
    // Change the item type to a hidden input to hold the file id.
    items[delta].type = 'hidden';

    // If we're dealing with the user profile 'picture' it isn't a real field,
    // so we need to spoof some field settings to get the widget to render
    // properly.
    // @TODO the field label doesn't show up.
    if (form.id == 'user_profile_form' && element.name == 'picture') {
      field = { field_name: 'picture' };
    }

    // If we already have an image for this item, show it.
    if (typeof items[delta].item !== 'undefined' && items[delta].item.fid) {
      // Set the hidden input's value equal to the file id.
      items[delta].value = items[delta].item.fid;
      // Show the image on the form, the file name as a link to the actual file,
      // the file size, and a remove button.
      var path = drupalgap_image_path(items[delta].item.uri);
      // @TODO - show the filesize.
      // @TODO - show the remove button.
      var html = theme('image', { path: path }) +
        '<div class="filename">' +
          l(items[delta].item.filename, path, { InAppBrowser: true }) +
        '</div>';
        /*theme('button_link', {
            text: 'Remove',
            path: null,
            attributes: {
              onclick: "_image_field_widget_form_remove_image()",
              'data-icon': 'delete',
              'data-iconpos': 'right'
            }
        });*/
        //'<div class="filesize">(' + items[delta].item.filesize + ')</div>';
      // Add html to the item's children.
      items[delta].children.push({markup: html});
      return; // No further processing required.
    }

    // Set the default button text, and if a value was provided,
    // overwrite the button text.
    var button_text = t('Take Photo');
    if (items[delta].value) { button_text = items[delta].value; }
    var browse_button_text = t('Browse');
    if (items[delta].value2) { browse_button_text = items[delta].value2; }

    // Place variables into document for PhoneGap image processing.
    var item_id_base = items[delta].id.replace(/-/g, '_');
    var image_field_source = item_id_base + '_imagefield_source';
    var imagefield_destination_type =
      item_id_base + '_imagefield_destination_type';
    var imagefield_data = item_id_base + '_imagefield_data';
    eval('var ' + image_field_source + ' = null;');
    eval('var ' + imagefield_destination_type + ' = null;');
    eval('var ' + imagefield_data + ' = null;');
    // Build an imagefield widget with PhoneGap. Contains a message
    // div, an image item, a button to add an image, and a button to browse for
    // images.
    var browse_button_id = items[delta].id + '-browse-button';
    var html = '<div>' +
      '<div id="' + items[delta].id + '-imagefield-msg"></div>' +
      '<img id="' + items[delta].id + '-imagefield" style="display: none;" />' +
      '<a href="#" data-role="button" data-icon="camera" ' +
        'id="' + items[delta].id + '-button">' +
        button_text +
      '</a>' +
      '<a href="#" data-role="button" data-icon="grid" ' +
        'id="' + browse_button_id + '">' +
        browse_button_text +
      '</a>' +
    '</div>';
    // Open extra javascript declaration.
    html += '<script type="text/javascript">';
    // Add device ready listener for PhoneGap camera.
    var event_listener = item_id_base + '_imagefield_ready';
    html += '$("#' + drupalgap_get_page_id(
      drupalgap_path_get()) + '").on("pageshow",function(){' +
        'document.addEventListener(' +
        '"deviceready", ' +
        event_listener + ', ' +
        'false);' +
      '});' +
    'function ' + event_listener + '() {' +
      image_field_source + ' = navigator.camera.PictureSourceType;' +
      imagefield_destination_type + ' = navigator.camera.DestinationType;' +
    '}';
    // Define error callback function.
    var imagefield_error = item_id_base + '_error';
    html += 'function ' + imagefield_error + '(message) {' +
      'if (message != "Camera cancelled." && ' +
        'message != "Selection cancelled." && ' +
        'message != "no image selected")' +
      '{' +
        'console.log("' + imagefield_error + '");' +
        'drupalgap_alert(message);' +
      '}' +
    '}';
    // Define success callback function.
    var imagefield_success = item_id_base + '_success';
    html += 'function ' + imagefield_success + '(imageData) {' +
      '_image_phonegap_camera_getPicture_success(' +
      '{field_name:"' + field.field_name + '", ' +
        'image:imageData, id:"' + items[delta].id + '"' +
       '})' +
    '}';
    // Determine image quality.
    var quality = 50;
    if (drupalgap.settings.camera.quality) {
      quality = drupalgap.settings.camera.quality;
    }
    // Add click handler for photo button.
    html += '$("#' + items[delta].id + '-button").on("click",function(){' +
      'var photo_options = {' +
        'quality: ' + quality + ',' +
        'destinationType: ' + imagefield_destination_type + '.DATA_URL,' +
        'correctOrientation: true' +
      '};' +
      'navigator.camera.getPicture(' +
        imagefield_success + ', ' +
        imagefield_error + ', ' +
        'photo_options);' +
    '});';
    // Add click handler for browse button.
    html += '$("#' + browse_button_id + '").on("click",function(){' +
      'var browse_photo_options = {' +
        'quality: ' + quality + ',' +
        'sourceType: ' + image_field_source + '.PHOTOLIBRARY,' +
        'destinationType: ' + imagefield_destination_type + '.DATA_URL,' +
        'correctOrientation: true' +
      '};' +
      'navigator.camera.getPicture(' +
        imagefield_success + ', ' +
        imagefield_error + ', ' +
        'browse_photo_options);' +
    '});';
    // Close extra javascript declaration.
    html += '</script>';
    // Add html to the item's children.
    items[delta].children.push({markup: html});
  }
  catch (error) { console.log('image_field_widget_form - ' + error); }
}

/**
 * On an entity edit form, this removes an image file from the server, then from
 * the form elements and user interface.
 */
function _image_field_widget_form_remove_image() {
  try {
    alert('_image_field_widget_form_remove_image');
  }
  catch (error) {
    console.log('_image_field_widget_form_remove_image - ' + error);
  }
}

/**
 * Given an entity type and optional bundle name, this will return an array
 * containing any image field names present, false otherwise.
 * @param {String} entity_type
 * @param {String} bundle
 * @return {Object}
 */
function image_fields_present_on_entity_type(entity_type, bundle) {
  try {
    var results = [];
    var fields = drupalgap_field_info_instances(entity_type, bundle);
    if (!fields) { return false; }
    for (var name in fields) {
        if (!fields.hasOwnProperty(name)) { continue; }
        var field = fields[name];
        if (
          field.widget &&
          field.widget.type &&
          field.widget.type == 'image_image'
        ) { results.push(name); }
    }
    if (results.length == 0) { return false; }
    return results;
  }
  catch (error) {
    console.log('image_fields_present_on_entity_type - ' + error);
  }
}

/**
 * Implements hook_form_alter().
 * @param {Object} form
 * @param {Object} form_state
 * @param {String} form_id
 */
function image_form_alter(form, form_state, form_id) {
  try {
    // Make potential alterations to any entity edit form that has an image
    // field element(s).
    if (form.entity_type) {
      var bundle = form.bundle;
      var image_fields =
        image_fields_present_on_entity_type(form.entity_type, bundle);
      if (image_fields) {
        // Attach the image field names to the form for later reference.
        form.image_fields = image_fields;
        // For each image field, create a place for it in the global var.
        if ($.isArray(image_fields)) {
          for (var index in image_fields) {
              if (!image_fields.hasOwnProperty(index)) { continue; }
              var name = image_fields[index];
              image_phonegap_camera_options[name] = { 0: null };
          }
        }
      }
    }
  }
  catch (error) { console.log('image_form_alter - ' + error); }
}

/**
 * Given an image style name and image uri, this will return the absolute URL
 * that can be used as a src value for an img element.
 * @param {String} style_name
 * @param {String} path
 * @return {String}
 */
function image_style_url(style_name, path) {
  try {
    // @TODO - bug: the trailing slash on public and private is breaking images
    // that don't live in a sub directory in sites/default/files.
    var src =
      Drupal.settings.site_path + Drupal.settings.base_path + path;
    if (src.indexOf('public://') != -1) {
      src = src.replace(
        'public://',
        Drupal.settings.file_public_path +
          '/styles/' +
          style_name +
          '/public/'
      );
    }
    else if (src.indexOf('private://') != -1) {
      src = src.replace(
        'private://',
        Drupal.settings.file_private_path +
          '/styles/' +
          style_name +
          '/private/'
      );
    }
    return src;
  }
  catch (error) { console.log('image_style_url - ' + error); }
}

/**
 * The success callback function used when handling PhoneGap's camera
 * getPicture() call.
 * @param {Object} options
 */
function _image_phonegap_camera_getPicture_success(options) {
  try {

    // Hold on to the image options in the global var.
    image_phonegap_camera_options[options.field_name] = {0: options};

    // Hide the 'Add image' button and show the 'Upload' button.
    //$('#' + options.id + '-button').hide();
    //$('#' + options.id + '_upload').show();

    // Show the captured photo as a thumbnail. When the photo is loaded, resize
    // it to fit the content area, then show it.
    var image_element_id = options.id + '-imagefield';
    var image = document.getElementById(image_element_id);
    image.src = 'data:image/jpeg;base64,' +
      image_phonegap_camera_options[options.field_name][0].image;
    image.onload = function() {
      var width = this.width;
      var height = this.height;
      var ratio = width / drupalgap_max_width();
      var new_width = width / ratio;
      var new_height = height / ratio;
      image.width = new_width;
      image.height = new_height;
      $('#' + image_element_id).show();
    };
  }
  catch (error) {
    console.log('_image_phonegap_camera_getPicture_success - ' + error);
  }
}

/**
 * An internal function used to upload images to the server, retreive their file
 * id and then populate the corresponding form element's value with the file id.
 * @param {Object} form
 * @param {Object} form_state
 * @param {Object} options
 */
function _image_field_form_process(form, form_state, options) {
  try {
    // @TODO needs mutli value field support (delta)
    // @see https://www.drupal.org/node/2224803

    var lng = language_default();
    var processed_an_image = false;

    // For each image field on the form...
    for (var index in form.image_fields) {
      if (!form.image_fields.hasOwnProperty(index)) { continue; }
      var name = form.image_fields[index];

      // Skip empty images and ones that already have their field id set.
      if (!image_phonegap_camera_options[name][0] || form_state.values[name][lng][0] != '') { continue; }

      // Create a unique file name using the UTC integer value.
      var d = new Date();
      var image_file_name = Drupal.user.uid + '_' + d.valueOf() + '.jpg';

      // Build the data for the file create resource. If it's private, adjust the filepath.
      var file = {
        file: {
          file: image_phonegap_camera_options[name][0].image,
          filename: image_file_name,
          filepath: 'public://' + image_file_name
        }
      };
      if (!empty(Drupal.settings.file_private_path)) {
        file.file.filepath = 'private://' + image_file_name;
      }

      // Change the loader mode to saving, and save the file.
      drupalgap.loader = 'saving';
      processed_an_image = true;
      file_save(file, {
          async: false,
          success: function(result) {
            try {
              // Set the hidden input and form state values with the file id.
              var element_id = drupalgap_form_get_element_id(name, form.id);
              $('#' + element_id).val(result.fid);
              form_state.values[name][lng][0] = result.fid;
              if (options.success) { options.success(); }
            }
            catch (error) {
              console.log('_image_field_form_process - success - ' + error);
            }
          }
      });
    }

    // If no images were processed, we need to continue onward anyway.
    if (!processed_an_image && options.success) { options.success(); }
  }
  catch (error) { console.log('_image_field_form_validate - ' + error); }
}

/**
 * Implements hook_assemble_form_state_into_field().
 * @param {Object} entity_type
 * @param {String} bundle
 * @param {String} form_state_value
 * @param {Object} field
 * @param {Object} instance
 * @param {String} langcode
 * @param {Number} delta
 * @param {Object} field_key
 * @return {*}
 */
function image_assemble_form_state_into_field(entity_type, bundle,
  form_state_value, field, instance, langcode, delta, field_key) {
  try {
    field_key.value = 'fid';
    return form_state_value;
  }
  catch (error) {
    console.log('image_assemble_form_state_into_field - ' + error);
  }
}


/**
 * Implements hook_block_view().
 * @param {String} delta
 * @param {Object} region
 * @return {String}
 */
function menu_block_view(delta, region) {
  // NOTE: When rendering a jQM data-role="navbar" you can't place an
  // empty list (<ul></ul>) in it, this will cause an error:
  // https://github.com/jquery/jquery-mobile/issues/5141
  // So we must check to make sure we have any items before rendering the
  // menu since our theme_item_list implementation returns empty lists
  // for jQM pageshow async list item data retrieval and display.
  try {
    // Load the menu.
    var menu = drupalgap.menus[delta];
    // Since menu link paths may have an 'access_callback' handler that needs
    // to make an async call to the server (e.g. local tasks), we'll utilize a
    // pageshow handler to render the menu, so for now just render an empty
    // placeholder and pageshow handler.
    var container_id = menu_container_id(delta);
    var data_role = null;
    if (region.attributes && region.attributes['data-role']) {
      data_role = region.attributes['data-role'];
    }
    // If the menu is wrapped, check to see if any wrap_options attributes were
    // attached. If any were provided use them to build the div container
    // attributes. We will always overwrite the container id though, since it is
    // generated by the system (and used to dynamically inject the menu html).
    var container_attributes = {};
    if (
      typeof menu.options !== 'undefined' &&
      typeof menu.options.wrap !== 'undefined' &&
      menu.options.wrap && menu.options.wrap_options &&
      menu.options.wrap_options.attributes
    ) { container_attributes = menu.options.wrap_options.attributes; }
    container_attributes.id = container_id;
    return '<div ' + drupalgap_attributes(container_attributes) + '></div>' +
      drupalgap_jqm_page_event_script_code({
          page_id: drupalgap_get_page_id(),
          jqm_page_event: 'pageshow',
          jqm_page_event_callback: 'menu_block_view_pageshow',
          jqm_page_event_args: JSON.stringify({
              menu_name: delta,
              container_id: container_id,
              'data-role': data_role
          })
      }, delta);
  }
  catch (error) { console.log('menu_block_view - ' + error); }
}

/**
 * The pageshow handler for menu blocks.
 * @param {Object} options
 */
function menu_block_view_pageshow(options) {
  try {
    var html = '';

    // Grab current path so we can watch out for any menu links that match it.
    var path = drupalgap_path_get();

    // Are we about to view a normal menu, or the local task menu?
    var delta = options.menu_name;
    if (delta == 'primary_local_tasks') {

      // LOCAL TASKS MENU LINKS

      // For the current page's router path, grab any local task menu links add
      // them into the menu. Note, local tasks are located in a menu link item's
      // children, or its parent's children (including itself), if there are
      // any. Local tasks typically have argument wildcards in them, so we'll
      // replace their wildcards with the current args.
      var router_path = drupalgap_router_path_get();
      if (drupalgap.menu_links[router_path]) {
        // Determine the parent path, if any.
        var parent = null;
        if (drupalgap.menu_links[router_path].parent) {
          parent = drupalgap.menu_links[router_path].parent;
        }
        // Then extract the local tasks paths array.
        var local_tasks = null;
        if (drupalgap.menu_links[router_path].children) {
          local_tasks = drupalgap.menu_links[router_path].children;
        }
        else if (
          parent && drupalgap.menu_links[parent] &&
          drupalgap.menu_links[parent].children
        ) { local_tasks = drupalgap.menu_links[parent].children; }

        var args = arg();

        // Define a success callback that will be called later on...
        var _success = function(result) {
          try {
            var menu_items = [];
            var link_path = '';
            if (local_tasks && !empty(local_tasks)) {
              for (var index in local_tasks) {
                  if (!local_tasks.hasOwnProperty(index)) { continue; }
                  var local_task = local_tasks[index];
                  if (drupalgap.menu_links[local_task] && (
                    drupalgap.menu_links[local_task].type ==
                      'MENU_DEFAULT_LOCAL_TASK' ||
                    drupalgap.menu_links[local_task].type ==
                      'MENU_LOCAL_TASK'
                  )) {
                    if (drupalgap_menu_access(local_task, null, result)) {
                      menu_items.push(drupalgap.menu_links[local_task]);
                    }
                  }
              }
            }
            // If there was only one local task menu item, and it is the default
            // local task, don't render the menu, otherwise render the menu as
            // an item list as long as there are items to render.
            if (menu_items.length == 1 &&
              menu_items[0].type == 'MENU_DEFAULT_LOCAL_TASK'
            ) { html = ''; }
            else {
              var items = [];
              for (var index in menu_items) {
                  if (!menu_items.hasOwnProperty(index)) { continue; }
                  var item = menu_items[index];
                  // Make a deep copy of the menu link so we don't modify it.
                  var link = jQuery.extend(true, {}, item);
                  // If there are no link options, set up defaults.
                  if (!link.options) { link.options = { attributes: { } }; }
                  else if (!link.options.attributes) {
                    link.options.attributes = { };
                  }

                  // If the link points to the current path, set it as active.
                  // We first need to figure out which path to check, by default
                  // use the link path, but if its a default local task, use its
                  // parent path.
                  var path_to_check = link.path;
                  if (link.type == 'MENU_DEFAULT_LOCAL_TASK' && link.parent) {
                    path_to_check = link.parent;
                    link.path = arg(null, link.parent).join('/');
                  }
                  if (path_to_check == router_path) {
                    if (!link.options.attributes['class']) {
                      link.options.attributes['class'] = '';
                    }
                    link.options.attributes['class'] +=
                      ' ui-btn ui-btn-active ui-state-persist ';
                  }
                  items.push(
                    l(
                      link.title,
                      drupalgap_place_args_in_path(link.path),
                      link.options
                    )
                  );
              }
              if (items.length > 0) {
                html = theme('item_list', {'items': items});
              }
            }
            // Inject the html.
            $('#' + options.container_id).html(html).trigger('create');
            // If the block's region is a jQM navbar, refresh the navbar.
            if (options['data-role'] && options['data-role'] == 'navbar') {
              $('#' + options.container_id).navbar();
            }
            // Optionally remove the placeholder wrapper.
            var menu = drupalgap.menus[options.menu_name];
            if (
              typeof menu.options !== 'undefined' &&
              (typeof menu.options.wrap === 'undefined' || !menu.options.wrap)
            ) { $('#' + options.container_id).children().unwrap(); }
          }
          catch (error) {
            console.log('menu_block_view_pageshow - success - ' + error);
          }
        };

        // First, determine if any child has an entity arg in the path, and/or
        // an access_callback handler.
        var has_entity_arg = false;
        var has_access_callback = false;
        if (local_tasks) {
          for (var index in local_tasks) {
              if (!local_tasks.hasOwnProperty(index)) { continue; }
              var local_task = local_tasks[index];
              if (drupalgap.menu_links[local_task] &&
                (
                  drupalgap.menu_links[local_task].type ==
                    'MENU_DEFAULT_LOCAL_TASK' ||
                  drupalgap.menu_links[local_task].type ==
                    'MENU_LOCAL_TASK'
                )
              ) {
                if (drupalgap_path_has_entity_arg(arg(null, local_task))) {
                  has_entity_arg = true;
                }
                if (
                  typeof
                    drupalgap.menu_links[local_task].access_callback !==
                    'undefined'
                ) { has_access_callback = true; }
              }
          }
        }

        // If we have an entity arg, and an access_callback, let's load up the
        // entity asynchronously.
        if (has_entity_arg && has_access_callback) {
          var found_int_arg = false;
          var int_arg_index = null;
          for (var i = 0; i < args.length; i++) {
            if (is_int(parseInt(args[i]))) {
              // Save the arg index so we can replace it later.
              int_arg_index = i;
              found_int_arg = true;
              break;
            }
          }
          if (!found_int_arg) { _success(null); return; }

          // Determine the naming convention for the entity load function.
          var load_function_prefix = args[0]; // default
          if (args[0] == 'taxonomy') {
            if (args[1] == 'vocabulary' || args[1] == 'term') {
              load_function_prefix = args[0] + '_' + args[1];
            }
          }
          var load_function = load_function_prefix + '_load';

          // If the load function exists, load the entity.
          if (drupalgap_function_exists(load_function)) {
            var entity_fn = window[load_function];
            // Load the entity. MVC items need to pass along the module name and
            // model type to its load function. All other entity load functions
            // just need the entity id.
            var entity_id = parseInt(args[int_arg_index]);
            if (args[0] == 'item') {
              entity = entity_fn(args[1], args[2], entity_id);
              _success(entity);
            }
            else {
              // Force a reset if we are editing the entity.
              var reset = false;
              if (arg(2) == 'edit') { reset = true; }
              // Load the entity asynchronously.
              entity_fn(entity_id, { reset: reset, success: _success });
            }
          }
          else {
            console.log('menu_block_view_pageshow - load function not ' +
              'implemented! ' + load_function
            );
          }
        }
        else { _success(null); }
      }
    }
    else {

      // ALL OTHER MENU LINKS

      // If the block's corresponding menu exists, and it has links, iterate
      // over each link, add it to an items array, then theme an item list.
      var menu = false;
      if (drupalgap.menus[delta] && drupalgap.menus[delta].links) {
        menu = drupalgap.menus[delta];
        var items = [];
        for (var index in menu.links) {
            if (!menu.links.hasOwnProperty(index)) { continue; }
            var menu_link = menu.links[index];
            // Make a deep copy of the menu link so we don't modify it.
            var link = jQuery.extend(true, {}, menu_link);
            // If there are no link options, set up defaults.
            if (!link.options) { link.options = {attributes: {}}; }
            else if (!link.options.attributes) { link.options.attributes = {}; }
            if (!link.options.attributes['class']) {
              link.options.attributes['class'] = '';
            }
            // Extract the link's class attribute.
            var class_names = link.options.attributes['class'];
            // If the link points to the current path, set it as active.
            if (link.path == path) {
              if (class_names.indexOf('ui-btn') == -1) {
                class_names += ' ui-btn';
              }
              if (class_names.indexOf('ui-btn-active') == -1) {
                class_names += ' ui-btn-active';
              }
              if (class_names.indexOf('ui-state-persist') == -1) {
                class_names += ' ui-state-persist';
              }
            }
            // If there was a data-icon attibute on the link, let's add its
            // equivalent css class name to the link (if it isn't already
            // present), otherwise jQM won't render the icon properly. Sounds
            // like a jQM bug.
            if (
              link.options.attributes['data-icon'] &&
              class_names.indexOf(link.options.attributes['data-icon']) == -1
            ) {
              class_names +=
                ' ui-icon-' + link.options.attributes['data-icon'] + ' ';
            }
            // Finally toss the class attribute back on the link and add the
            // link to the items array.
            link.options.attributes['class'] = class_names + ' ';
            items.push(l(t(link.title), link.path, link.options));
        }
        if (items.length > 0) {
          // Pass along any menu attributes.
          var attributes = null;
          if (menu.options && menu.options.attributes) {
            attributes = drupalgap.menus[delta].options.attributes;
          }
          html = theme('item_list', {'items': items, 'attributes': attributes});
        }
      }
      // Inject the html.
      $('#' + options.container_id).html(html).trigger('create');
      // Remove the placeholder wrapper, unless we were instructed not to.
      var wrap = false;
      if (
        menu && typeof menu.options !== 'undefined' &&
        typeof menu.options.wrap !== 'undefined' && menu.options.wrap
      ) { wrap = true; }
      if (!wrap) { $('#' + options.container_id).children().unwrap(); }
    }
  }
  catch (error) { console.log('menu_block_view_pageshow - ' + error); }
}

/**
 * Implements hook_install().
 */
function menu_install() {
  try {
    // Grab the list of system menus and save each.
    var system_menus = menu_list_system_menus();
    for (var menu_name in system_menus) {
      if (!system_menus.hasOwnProperty(menu_name)) { continue; }
      var menu = system_menus[menu_name];
      menu_save(menu);
    }
  }
  catch (error) { console.log('menu_install - ' + error); }
}

/**
 * Returns a JSON object that can be used as default options for a menu object.
 * @return {Object}
 */
function menu_popup_get_default_options() {
    return {
      attributes: {
        'data-role': 'listview'
      },
      wrap: true,
      wrap_options: {
        attributes: {
          'data-role': 'popup'
        }
      }
    };
}

/**
 * Given a menu region link, this will return its data JSON object, or null if
 * no data exists.
 * @param {Object} region_link
 * @return {*)
 */
function menu_region_link_get_data(region_link) {
  try {
    // Extract the data associated with this link. If it has a 'region'
    // property then it is coming from a hook_menu, if it doesn't then it
    // is coming from settings.js.
    var data = null;
    if (typeof region_link.region === 'undefined') {
      data = region_link; // link defined in settings.js
      // @TODO - we need to warn people that they can't make a custom menu
      // with a machine name of 'regions' now that this machine name is a
      // "system" name for rendering links in regions.
    }
    // link defined via hook_menu()
    else { data = region_link.region; }
    return data;
  }
  catch (error) { console.log('menu_region_link_get_data - ' + error); }
}

/**
 * Given a menu region link's class name, this will return what side of the ui
 * it is on, returns left by default, unless it specifically contains the
 * ui-btn-right class.
 * @param {String} class_name
 * @return {String)
 */
function menu_region_link_get_side(class_name) {
  try {
    var side = 'left';
    if (class_name.indexOf('ui-btn-right') != -1) { side = 'right'; }
    return side;
  }
  catch (error) { console.log('menu_region_link_get_side - ' + error); }
}

/**
 * Given a menu, this adds it to drupalgap.menus. See menu_list_system_menus
 * for examples of a menu JSON object.
 * @param {Object} menu
 */
function menu_save(menu) {
  try {
    drupalgap.menus[menu.menu_name] = menu;
  }
  catch (error) { console.log('menu_save - ' + error); }
}

/**
 * Given a menu name, this will return it from drupalgap.menus, or return null
 * if it doesn't exist.
 * @param {String} name
 * @return {*}
 */
function menu_load(name) {
  try {
    if (typeof drupalgap.menus[name] !== 'undefined') {
      return drupalgap.menus[name];
    }
    return null;
  }
  catch (error) { console.log('menu_load - ' + error); }
}

/**
 * Given a menu name, this will return its container id for that page. You may
 * optionally pass in a page id as the second argument, otherwise it will use
 * the current page id.
 * @param {String} menu_name
 * @return {String}
 */
function menu_container_id(menu_name) {
  try {
    var page_id = null;
    if (arguments[1]) { page_id = arguments[1]; }
    else { page_id = drupalgap_get_page_id(); }
    return page_id + '_menu_' + menu_name;
  }
  catch (error) { console.log('menu_container_id - ' + error); }
}


/**
 * The page callback for mvc/collection/list/%/%.
 * @param {String} module
 * @param {String} type
 * @return {Object}
 */
function collection_list_page(module, type) {
  try {
    var content = {
      'collection_list': {
        'theme': 'jqm_item_list',
        'title': t('Collection')
      }
    };
    var items = [];
    var collection = collection_load(module, type);
    if (collection) {
      for (var id in collection) {
          if (!collection.hasOwnProperty(id)) { continue; }
          var item = collection[id];
          var path = 'mvc/item/' + module + '/' + type + '/' + id;
          items.push(l(item.name, path));
      }
      content.collection_list.items = items;
    }
    return content;
  }
  catch (error) { console.log('collection_list_page - ' + error); }
}

/**
 * Given a module name and model type, this will return the collection
 * JSON object.
 * @param {String} module
 * @param {String} type
 * @return {*}
 */
function collection_load(module, type) {
  try {
    return JSON.parse(
      window.localStorage.getItem(
        mvc_get_collection_key('collection', module, type)
      )
    );
  }
  catch (error) { console.log('collection_load - ' + error); }
}

/**
 * Given a module name, model type and item collection, this will save the
 * collection JSON object to local storage.
 * @param {String} module
 * @param {String} type
 * @param {Object} collection
 */
function collection_save(module, type, collection) {
  try {
    window.localStorage.setItem(
      mvc_get_collection_key('collection', module, type),
      JSON.stringify(collection)
    );
  }
  catch (error) { console.log('collection_save - ' + error); }
}

/**
 * Given a bucket (e.g. collection, settings), module name and mvc model type,
 * this will return the local storage key used for the model type's item
 * collection.
 * @param {String} bucket
 * @param {String} module
 * @param {String} model_type
 * @return {String}
 */
function mvc_get_collection_key(bucket, module, model_type) {
    return 'mvc_' + bucket + '_' + module + '_' + model_type;
}

/**
 * Implements hook_install().
 */
function mvc_install() {
  try {
    // Load models...
    // For each module that implements hook_mvc_model(), iterate over each model
    // placing it into drupalgap.mvc.models, each model will be placed into
    // a namespace according to the module that implements it, to avoid
    // namespace collisions.
    var modules = module_implements('mvc_model');
    for (var i = 0; i < modules.length; i++) {
      var module = modules[i];
      var models = module_invoke(module, 'mvc_model');
      if (models) {
        // Create namespace for model, keyed by module name.
        if (!drupalgap.mvc.models[module]) {
          drupalgap.mvc.models[module] = {};
        }
        // For each model type...
        for (var model_type in models) {
            if (!models.hasOwnProperty(model_type)) { continue; }
            var model = models[model_type];
            // Set the primary key 'id', the module name, and model type
            // on the model fields. These are the mvc_model_system_fields().
            model.fields.id = {
              'type': 'hidden',
              'title': t('ID'),
              'required': false
            };
            model.fields.module = {
              'type': 'hidden',
              'title': t('Module'),
              'required': true,
              'default_value': module
            };
            model.fields.type = {
              'type': 'hidden',
              'title': t('Model Type'),
              'required': true,
              'default_value': model_type
            };
            // Add each model type to its namespace within drupalgap.mvc.models
            drupalgap.mvc.models[module][model_type] = model;
            // Save an empty collection to local storage for this model type, if
            // one doesn't already exist.
            var collection_key = mvc_get_collection_key(
              'collection',
              module,
              model_type
            );
            if (!window.localStorage.getItem(collection_key)) {
              window.localStorage.setItem(collection_key, '[]');
              // Save settings for the collection to local storage. The auto
              // increment value represents an item id, we start counting at
              // zero since the collection is an array.
              window.localStorage.setItem(
                mvc_get_collection_key('settings', module, model_type),
                '{"auto_increment":0}'
              );
            }
        }
      }
    }
    //console.log(JSON.stringify(drupalgap.mvc.models));
    //drupalgap_alert('drupalgap_mvc_init');
    // These may not be needed. Perhaps we should just call assumed hooks that
    // should be implemented for any custom views and controllers at the time
    // they are needed, probably no need to bundle them inside drupalgap.mvc.
    //var views = module_invoke_all('mvc_view');
    //var controllers = module_invoke_all('mvc_controller');
  }
  catch (error) { console.log('mvc_install - ' + error); }
}

/**
 * Implements hook_menu().
 * @return {Object}
 */
function mvc_menu() {
    var items = {
      'mvc/collection/list/%/%': {
        'page_callback': 'collection_list_page',
        'page_arguments': [3, 4]
      },
      'mvc/item/%/%/%': {
        'page_callback': 'item_view_page',
        'page_arguments': [2, 3, 4]
      },
      'mvc/item-add/%/%': {
        title: t('Add'),
        page_callback: 'drupalgap_get_form',
        page_arguments: ['item_create_form', 2, 3]
      }
    };
    return items;
}

/**
 * Returns an array of system fields (properties) to be used on item JSON
 * object.
 * @return {Array}
 */
function mvc_model_system_fields() {
    return ['id', 'module', 'type'];
}

// We'll need developer friendly front end functions, e.g.
// model_load();
// model_save();
// model_delete();
// item_load();
// item_save();
// item_delete();
// etc...

/**
 * Given a module name and a corresponding model name, this will load the model
 * from drupalgap.mvc.models.
 * @param {String} module
 * @param {String} name
 * @return {Object}
 */
function model_load(module, name) {
  try {
    if (
      drupalgap.mvc.models[module] === 'undefined' ||
      drupalgap.mvc.models[module][name] === 'undefined'
    ) { return false; }
    return drupalgap.mvc.models[module][name];
  }
  catch (error) { console.log('model_load - ' + error); }
}

/**
 * Given a module type, and model type, this generates and returns the form JSON
 * object to create a model item.
 * @param {Object} form
 * @param {Object} form_state
 * @param {String} module
 * @param {String} type
 * @return {Object}
 */
function item_create_form(form, form_state, module, type) {
   try {
    //var form = drupalgap_form_defaults('item_create_form');
    var model = model_load(module, type);
    if (model) {
      // @todo - this could be dangerous just overriding the elements variable,
      // we should iterate over the model fields and add them one by one
      // instead.
      form.elements = model.fields;
      form.buttons.cancel = drupalgap_form_cancel_button();
      form.elements.submit = {
        type: 'submit',
        value: t('Create')
      };
    }
    return form;
  }
  catch (error) { console.log('item_create_form - ' + error); }
}

/**
 * Handles the submission of an mvc model item creation form.
 * @param {Object} form
 * @param {Object} form_state
 */
function item_create_form_submit(form, form_state) {
  try {
    // Save the item and then view it.
    if (item_save(form_state.values)) {
      /*var path = 'item/' +
                 form_state.values.module + '/' +
                 form_state.values.type + '/' +
                 form_state.values.id;*/
      var path = 'mvc/collection/list/' +
                 form_state.values.module + '/' +
                 form_state.values.type;
      // If there is a form action path set, use that instead.
      if (form.action) { path = form.action; }
      // Go to our destination path, and force a reload on the page.
      drupalgap_goto(path, {reloadPage: true});
    }
    else {
      var msg = 'item_create_form_submit - failed to save item!';
      drupalgap_alert(msg);
    }
  }
  catch (error) {
    console.log('item_create_form_submit - ' + error);
  }
}

/**
 * Given a module name, mvc model type, and item id, this will return the item,
 * or false if the item fails to load.
 * @param {String} module
 * @param {String} type
 * @param {String} id
 * @return {Object}
 */
function item_load(module, type, id) {
  try {
    var item = false;
    var collection = collection_load(module, type);
    if (collection && typeof collection[id] !== 'undefined') {
      item = collection[id];
    }
    return item;
  }
  catch (error) { console.log('item_load - ' + error); }
}

/**
 * Given an mvc item, this saves it to local storage.
 * @param {Object} item
 * @return {Boolean}
 */
function item_save(item) {
  try {
    if (typeof item === 'undefined') { return false; }

    // Grab the settings for this collection.
    var settings_key = mvc_get_collection_key(
      'settings',
      item.module,
      item.type
    );
    var settings = JSON.parse(window.localStorage.getItem(settings_key));

    // If there is no id, then this is a new item, grab the next id to use.
    if (!item.id) {
      item.id = settings.auto_increment;
    }
    // Load the collection from local storage.
    var collection = collection_load(item.module, item.type);

    // Add the item onto the collection.
    collection[item.id] = item;

    // Save the collection to local storage.
    collection_save(item.module, item.type, collection);

    // Increment to the next item id and save the settings.
    settings.auto_increment = item.id + 1;
    window.localStorage.setItem(settings_key, JSON.stringify(settings));

    return true;
  }
  catch (error) { console.log('item_save - ' + error); }
}

/**
 * The page callback for mvc/item/%/%/%.
 * @param {String} module
 * @param {String} type
 * @param {Object} item
 * @return {String}
 */
function item_view_page(module, type, item) {
  try {
    var html = '';
    var model = model_load(module, type);
    if (model) { html = theme('item', {'model': model, 'item': item}); }
    return html;
  }
  catch (error) { console.log('item_view_page - ' + error); }
}


/**
 * Given a node, this determines if the current user has access to it. Returns
 * true if so, false otherwise. This function implementation is incomplete, use
 * with caution.
 * @param {Object} node
 * @return {Boolean}
 */
function node_access(node) {
  try {
    if (
      (
        node.uid == Drupal.user.uid &&
        user_access('edit own ' + node.type + ' content')
      ) ||
      user_access('edit any ' + node.type + ' content')
    ) { return true; }
    else { return false; }
  }
  catch (error) { console.log('node_access - ' + error); }
}

/**
 * Page call back for node/add.
 * @return {Object}
 */
function node_add_page() {
  try {
    var content = {
      header: { markup: '<h2>' + t('Create Content') + '</h2>' },
      node_type_listing: {
        theme: 'jqm_item_list',
        title: t('Content Types'),
        attributes: { id: 'node_type_listing_items' }
      }
    };
    var items = [];
    var user_permissions = Drupal.user.content_types_user_permissions;
    for (var type in user_permissions) {
        if (!user_permissions.hasOwnProperty(type)) { continue; }
        var permissions = user_permissions[type];
        if (permissions.create) {
          items.push(l(drupalgap.content_types_list[type].name,
          'node/add/' + type));
        }
    }
    content.node_type_listing.items = items;
    return content;
  }
  catch (error) { console.log('node_add_page - ' + error); }
}

/**
 * Page call back function for node/add/[type].
 * @param {String} type
 * @return {Object}
 */
function node_add_page_by_type(type) {
  try {
    return drupalgap_get_form('node_edit', {'type': type});
  }
  catch (error) { console.log('node_add_page_by_type - ' + error); }
}

/**
 * Title call back function for node/add/[type].
 * @param {Function} callback
 * @param {String} type
 * @return {Object}
 */
function node_add_page_by_type_title(callback, type) {
  try {
    var title = t('Create') + ' ' + drupalgap.content_types_list[type].name;
    return callback.call(null, title);
  }
  catch (error) { console.log('node_add_page_by_type_title - ' + error); }
}

/**
 * The node edit form.
 * @param {Object} form
 * @param {Object} form_state
 * @param {Object} node
 * @return {Object}
 */
function node_edit(form, form_state, node) {
  try {
    // Setup form defaults.
    form.entity_type = 'node';
    form.bundle = node.type;

    // Add the entity's core fields to the form.
    drupalgap_entity_add_core_fields_to_form('node', node.type, form, node);

    // Add the fields for this content type to the form.
    drupalgap_field_info_instances_add_to_form('node', node.type, form, node);

    // Add submit to form.
    form.elements.submit = {
      'type': 'submit',
      'value': t('Save')
    };

    // Add cancel button to form.
    form.buttons['cancel'] = drupalgap_form_cancel_button();

    // Add delete button to form if we're editing a node.
    if (node && node.nid) {
      form.buttons['delete'] =
        drupalgap_entity_edit_form_delete_button('node', node.nid);
    }

    return form;
  }
  catch (error) { console.log('node_edit - ' + error); }
}

/**
 * The node edit form's submit function.
 * @param {Object} form
 * @param {Object} form_state
 */
function node_edit_submit(form, form_state) {
  try {
    var node = drupalgap_entity_build_from_form_state(form, form_state);
    drupalgap_entity_form_submit(form, form_state, node);
  }
  catch (error) { console.log('node_edit_submit - ' + error); }
}

/**
 * Implements hook_menu().
 * @return {Object}
 */
function node_menu() {
    var items = {
      'node': {
        'title': t('Content'),
        'page_callback': 'node_page',
        'pageshow': 'node_page_pageshow'
      },
      'node/add': {
        'title': t('Add content'),
        'page_callback': 'node_add_page'
      },
      'node/add/%': {
        title: t('Add content'),
        title_callback: 'node_add_page_by_type_title',
        title_arguments: [2],
        page_callback: 'node_add_page_by_type',
        page_arguments: [2],
        options: { reloadPage: true }
      },
      'node/%': {
        'title': t('Node'),
        'page_callback': 'node_page_view',
        'page_arguments': [1],
        'pageshow': 'node_page_view_pageshow',
        'title_callback': 'node_page_title',
        'title_arguments': [1]
      },
      'node/%/view': {
        'title': t('View'),
        'type': 'MENU_DEFAULT_LOCAL_TASK',
        'weight': -10
      },
      'node/%/edit': {
        'title': t('Edit'),
        'page_callback': 'entity_page_edit',
        'pageshow': 'entity_page_edit_pageshow',
        'page_arguments': ['node_edit', 'node', 1],
        'weight': 0,
        'type': 'MENU_LOCAL_TASK',
        'access_callback': 'node_access',
        'access_arguments': [1],
        options: {reloadPage: true}
      }
    };
    return items;
}

/**
 * Page callback for node.
 * @return {Object}
 */
function node_page() {
    var content = {
      'create_content': {
        'theme': 'button_link',
        'path': 'node/add',
        'text': t('Create Content')
      },
      'node_listing': {
        'theme': 'jqm_item_list',
        'title': t('Content List'),
        'items': [],
        'attributes': {'id': 'node_listing_items'}
      }
    };
    return content;
}

/**
 * The jQM pageshow callback for the node listing page.
 */
function node_page_pageshow() {
  try {
    // Grab some recent content and display it.
    views_datasource_get_view_result(
      'drupalgap/views_datasource/drupalgap_content', {
        success: function(content) {
          // Extract the nodes into items, then drop them in the list.
          var items = [];
          for (var index in content.nodes) {
              if (!content.nodes.hasOwnProperty(index)) { continue; }
              var object = content.nodes[index];
              items.push(l(object.node.title, 'node/' + object.node.nid));
          }
          drupalgap_item_list_populate('#node_listing_items', items);
        }
      }
    );
  }
  catch (error) { console.log('node_page_pageshow - ' + error); }
}

/**
 * Page callback for node/%.
 * @param {Number} nid
 * @return {Object}
 */
function node_page_view(nid) {
  try {
    if (nid) {
      var content = {
        container: _drupalgap_entity_page_container('node', nid, 'view')
      };
      return content;
    }
    else { drupalgap_error(t('No node id provided!')); }
  }
  catch (error) { console.log('node_page_view - ' + error); }
}

/**
 * jQM pageshow handler for node/% pages.
 * @param {Number} nid
 */
function node_page_view_pageshow(nid) {
  try {
    node_load(nid, {
        success: function(node) {

          // By this point the node's content has been assembled into an html
          // string. This is because when a node is retrieved from the server,
          // we use a services post processor to render its content.
          // @see entity_services_request_pre_postprocess_alter()

          // Does anyone want to take over the rendering of this content type?
          // Any implementors of hook_node_page_view_alter_TYPE()?
          // @TODO this should probably be moved up to the entity level.
          var hook = 'node_page_view_alter_' + node.type;
          var modules = module_implements(hook);
          if (modules.length > 0) {
            if (modules.length > 1) {
              var msg = 'node_page_view_pageshow - WARNING - there is more ' +
                'than one module implementing hook_' + hook + '(), we will ' +
                'use the first one: ' + modules[0];
              console.log(msg);
            }
            var function_name = modules[0] + '_' + hook;
            var fn = window[function_name];
            fn(node, {
                success: function(content) {
                  _drupalgap_entity_page_container_inject(
                    'node', node.nid, 'view', content
                  );
                }
            });
            return;
          }

          // Build a done handler which will inject the given build into the page container. If there was a success
          // callback attached to the page options call it.
          var done = function(build) {
            _drupalgap_entity_page_container_inject(
                'node', node.nid, 'view', build
            );
            if (drupalgap.page.options.success) { drupalgap.page.options.success(node); }
          };

          // Figure out the title, and watch for translation.
          var default_language = language_default();
          var node_title = node.title;
          if (node.title_field && node.title_field[default_language]) {
            node_title = node.title_field[default_language][0].safe_value;
          }
          // Build the node display. Set the node onto the build so it makes it to the theme layer variables.
          var build = {
            'theme': 'node',
            'node': node,
            // @todo - this is a core field and should by fetched from entity.js
            'title': { markup: node_title },
            'content': { markup: node.content }
          };

          // If comments are undefined, just inject the page.
          if (typeof node.comment === 'undefined') { done(build); }

          // If the comments are closed (1) or open (2), show the comments.
          else if (node.comment != 0) {
            if (node.comment == 1 || node.comment == 2) {
              // Render the comment form, so we can add it to the content later.
              var comment_form = '';
              if (node.comment == 2) {
                comment_form = drupalgap_get_form(
                  'comment_edit',
                  { nid: node.nid },
                  node
                );
              }
              // If there are any comments, load them.
              if (node.comment_count != 0) {
                var query = {
                  parameters: {
                    nid: node.nid
                  }
                };
                comment_index(query, {
                    success: function(results) {
                      try {
                        // Render the comments.
                        var comments = '';
                        for (var index in results) {
                            if (!results.hasOwnProperty(index)) { continue; }
                            var comment = results[index];
                            comments += theme('comment', { comment: comment });
                        }
                        build.content.markup += theme('comments', {
                            node: node,
                            comments: comments
                        });
                        // If the comments are open, show the comment form.
                        if (node.comment == 2 && user_access('post comments')) {
                          build.content.markup += comment_form;
                        }
                        // Finally, inject the page.
                        done(build);
                      }
                      catch (error) {
                        var msg = 'node_page_view_pageshow - comment_index - ' +
                          error;
                        console.log(msg);
                      }
                    },
                    error: function(xhr, status, msg) {
                      if (drupalgap.page.options.error) { drupalgap.page.options.error(xhr, status, msg); }
                    }
                });
              }
              else {
                // There weren't any comments, append an empty comments wrapper
                // and show the comment form if comments are open, then inject
                // the page.
                if (node.comment == 2) {
                  build.content.markup += theme('comments', { node: node });
                  if (user_access('post comments')) { build.content.markup += comment_form; }
                }
                done(build);
              }
            }
          }
          else {
            // Comments are hidden (0), append an empty comments wrapper to the
            // content and inject the content into the page.
            build.content.markup += theme('comments', { node: node });
            done(build);
          }
        },
      error: function(xhr, status, msg) {
        if (drupalgap.page.options.error) { drupalgap.page.options.error(xhr, status, msg); }
      }
    });
  }
  catch (error) { console.log('node_page_view_pageshow - ' + error); }
}

/**
 * The title call back function for the node view page.
 * @param {Function} callback
 * @param {Number} nid
 */
function node_page_title(callback, nid) {
  try {
    // Try to load the node title, then send it back to the given callback.
    var title = '';
    var node = node_load(nid, {
        success: function(node) {
          if (node && node.title) { title = node.title; }
          callback.call(null, title);
        }
    });
  }
  catch (error) { console.log('node_page_title - ' + error); }
}

/**
 * Implements hook_theme().
 * @return {Object}
 */
function node_theme() {
    return { node: { template: 'node' } };
}


/**
 * Implements hook_block_info().
 * @return {Object}
 */
function search_block_info() {
  try {
    var blocks = {};
    blocks['search'] = {
      delta: 'search',
      module: 'search'
    };
    return blocks;
  }
  catch (error) { console.log('search_block_info - ' + error); }
}

/**
 * Implements hook_block_view().
 * @param {String} delta
 * @param {String} region
 * @return {String}
 */
function search_block_view(delta, region) {
  try {
    var content = '';
    if (delta == 'search') {
      if (user_access('search content')) {
        content = drupalgap_get_form('search_block_form');
      }
    }
    return content;
  }
  catch (error) { console.log('search_block_view - ' + error); }
}

/**
 * Implements hook_menu().
 * @return {Object}
 */
function search_menu() {
  try {
    var items = {};
    items['search/%/%'] = {
      title: t('Search'),
      'page_callback': 'drupalgap_get_form',
      'pageshow': 'search_form_pageshow',
      'page_arguments': ['search_form'],
      'access_arguments': ['search content']
    };
    return items;
  }
  catch (error) { console.log('search_menu - ' + error); }
}


/**
 * The search block form.
 * @param {Object} form
 * @param {Object} form_state
 * @return {Object}
 */
function search_block_form(form, form_state) {
  try {
    form.elements['type'] = {
      type: 'hidden',
      default_value: 'node'
    };
    form.elements['keys'] = {
      type: 'search',
      title: '',
      title_placeholder: true,
      required: true,
      default_value: ''
    };
    // Since there is no submit button on the form, we'll catch the onsubmit
    // action and the trigger the form submission.
    form.options.attributes['onsubmit'] =
      "_drupalgap_form_submit('" + form.id + "'); return false;";
    return form;
  }
  catch (error) { console.log('search_block_form - ' + error); }
}

/**
 * The search block form submit handler.
 * @param {Object} form
 * @param {Object} form_state
 */
function search_block_form_submit(form, form_state) {
  try {
    var type = form_state.values['type'];
    var keys = form_state.values['keys'];
    drupalgap_goto('search/' + type + '/' + keys);
  }
  catch (error) { console.log('search_block_form_submit - ' + error); }
}

/**
 * The search form.
 * @param {Object} form
 * @param {Object} form_state
 * @return {Object}
 */
function search_form(form, form_state) {
  try {
    var type = arg(1);
    var keys = arg(2);
    form.elements.type = {
      type: 'hidden',
      default_value: type ? type : 'node'
    };
    form.elements.keys = {
      type: 'textfield',
      title: t('Enter your keywords'),
      required: true,
      default_value: keys ? keys : ''
    };
    form.elements.submit = {
      type: 'submit',
      value: t('Go'),
      options: {
        attributes: {
          'data-icon': 'search'
        }
      }
    };
    form.suffix += theme('jqm_item_list', {
        title: t('Search results'),
        items: [],
        options: {
          attributes: {
            id: 'search_form_results'
          }
        }
    });
    return form;
  }
  catch (error) { console.log('search_form - ' + error); }
}

/**
 * The search form submit handler.
 * @param {Object} form
 * @param {Object} form_state
 */
function search_form_submit(form, form_state) {
  try {
    var type = form_state.values['type'];
    var keys = form_state.values['keys'];
    switch (type) {
      case 'node':
        search_node(keys, {
            success: function(results) {
              var items = [];
              for (var index in results) {
                  if (!results.hasOwnProperty(index)) { continue; }
                  var result = results[index];
                  var link = theme('search_result_node', result);
                  items.push(link);
              }
              drupalgap_item_list_populate('#search_form_results', items);
            }
        });
        break;
      default:
        console.log('search_form_submit - unsupported type (' + type + ')');
        break;
    }
  }
  catch (error) { console.log('search_form_submit - ' + error); }
}

/**
 * The pageshow callback for the search form page.
 * @param {String} form_id
 */
function search_form_pageshow(form_id) {
  try {
    var type = arg(1);
    var keys = arg(2);
    switch (type) {
      case 'node':
        search_node(keys, {
            success: function(results) {
              var items = [];
              for (var index in results) {
                  if (!results.hasOwnProperty(index)) { continue; }
                  var result = results[index];
                  var link = theme('search_result_node', result);
                  items.push(link);
              }
              drupalgap_item_list_populate('#search_form_results', items);
            }
        });
        break;
      default:
        console.log('search_form_pageshow - unsupported type (' + type + ')');
        break;
    }
  }
  catch (error) { console.log('search_form_pageshow - ' + error); }
}

/**
 * The search node service.
 * @param {String} keys The keyword(s) to search for.
 * @param {Object} options
 */
function search_node(keys, options) {
  try {
    options.method = 'GET';
    options.path = 'search_node/retrieve.json&keys=' + encodeURIComponent(keys);
    options.service = 'search_node';
    options.resource = 'retrieve';
    Drupal.services.call(options);
  }
  catch (error) { console.log('search_node - ' + error); }
}

/**
 * Themes a search result node.
 * @param {Object} variables
 * @return {String}
 */
function theme_search_result_node(variables) {
  try {
    return l(
      '<h2>' + variables.title + '</h2>' +
        '<p>' + variables.snippet + '</p>',
      'node/' + variables.node.nid
    );
  }
  catch (error) { console.log('theme_search_result_node - ' + error); }
}


/**
 * Given an entity type, this will return its corresponding service resource, or
 * null if the resource doesn't exist.
 * @param {String} entity_type
 * @return {?Type|Object|null}
 */
function drupalgap_services_get_entity_resource(entity_type) {
  try {
    console.log(
      'WARNING: drupalgap_services_get_entity_resource() is deprecated! ' +
      'Use services_get_resource_function_for_entity() instead.'
    );
    // @todo - deprecate this function, it is no longer needed now that entity
    // c.r.u.d. is built into jDrupal.
    if (drupalgap.services[entity_type]) {
      return drupalgap.services[entity_type];
    }
    else { return null; }
  }
  catch (error) {
    console.log('drupalgap_services_get_entity_resource - ' + error);
  }
}

/**
 * Returns the name of the jDrupal function to be used when in need of an entity
 * C.R.U.D. operation.
 * @param {String} entity_type
 * @param {String} crud
 * @return {String}
 */
function services_get_resource_function_for_entity(entity_type, crud) {
    var name = entity_type + '_';
    switch (crud) {
      case 'create': name += 'save'; break;
      case 'retrieve': name += 'load'; break;
      case 'update': name += 'save'; break;
      case 'delete': name += 'delete'; break;
      default: name += 'load'; break;
    }
    return name;
}


/**
 * Given a json drupalgap options array from a service resource results call,
 * this extracts data based on the resource and populates necessary global vars.
 * @param {Object} options
 */
function drupalgap_service_resource_extract_results(options) {
  try {
    if (options.service == 'system' && options.resource == 'connect') {
      // The system connect resource's success function places what is in
      // options.data.user to overwrite Drupal.user, so anything we want in
      // the Drupal.user object must be added to options.data.user isntead.
      // Extract and build the user's permissions.
      options.data.user.permissions = [];
      var permissions = options.data.user_permissions;
      for (var permission in permissions) {
        options.data.user.permissions.push(permissions[permission]);
      }
      // Pull out the content types, and set them by their type.
      var content_types_list = options.data.content_types_list;
      for (var index in content_types_list) {
          if (!content_types_list.hasOwnProperty(index)) { continue; }
          var object = content_types_list[index];
          drupalgap.content_types_list[object.type] = object;
      }
      // Pull out the content types user permissions.
      options.data.user.content_types_user_permissions =
        options.data.content_types_user_permissions;
      // Pull out the site settings.
      drupalgap.site_settings = options.data.site_settings;
      // Pull out the date formats and types.
      if (typeof options.data.date_formats !== 'undefined') {
        drupalgap.date_formats = options.data.date_formats;
      }
      if (typeof options.data.date_types !== 'undefined') {
        drupalgap.date_types = options.data.date_types;
      }
    }
  }
  catch (error) {
    console.log('drupalgap_service_resource_extract_results - ' + error);
  }
}

/**
 * Given the result of a drupalgap.services.rss.retrieve.call, this will iterate
 * over the RSS items and assemble them into a nice array of JSON objects and
 * return them. Returns null if it fails.
 * @param {Object} data
 * @return {?Type|Array|null}
 */
function drupalgap_services_rss_extract_items(data) {
  try {
    var items = null;
    var $xml = $(data);
    if ($xml) {
      // Extract the feeds items, then drop them in the list.
      var items = [];
      $xml.find('item').each(function() {
          var $this = $(this), item = {
            title: $this.find('title').text(),
            link: $this.find('link').text(),
            description: $this.find('description').text(),
            pubDate: $this.find('pubDate').text(),
            author: $this.find('author').text()
          };
          items.push(item);
      });
    }
    return items;
  }
  catch (error) {
    console.log('drupalgap_services_rss_extract_items - ' + error);
  }
}


var _system_reload_page = null;
var _system_reload_messages = null;

/**
 * Implements hook_install().
 */
function system_install() {

  // Remove any old forms from local storage, then purge the form expiration tracker.
  for (var form_id in Drupal.cache_expiration.forms) {
    if (!Drupal.cache_expiration.forms.hasOwnProperty(form_id)) { continue; }
    drupalgap_form_local_storage_delete(form_id);
  }
  Drupal.cache_expiration.forms = {};
  window.localStorage.setItem('cache_expiration', JSON.stringify(Drupal.cache_expiration));

}

/**
 * Implements hook_block_info().
 * @return {Object}
 */
function system_block_info() {
    // System blocks.
    var blocks = {
      'main': {
        'delta': 'main',
        'module': 'system'
      },
      messages: {
        delta: 'messages',
        module: 'system'
      },
      'logo': {
        'delta': 'logo',
        'module': 'system'
      },
      logout: {
        delta: 'logout',
        module: 'system'
      },
      'title': {
        'delta': 'title',
        'module': 'system'
      },
      'powered_by': {
        'delta': 'powered_by',
        'module': 'system'
      },
      'help': {
        'delta': 'help',
        'module': 'system'
      }
    };
    // Make additional blocks for each system menu.
    var system_menus = menu_list_system_menus();
    for (var menu_name in system_menus) {
        if (!system_menus.hasOwnProperty(menu_name)) { continue; }
        var menu = system_menus[menu_name];
        var block_delta = menu.menu_name;
        blocks[block_delta] = {
          name: block_delta,
          delta: block_delta,
          module: 'menu'
        };
    }
    return blocks;
}

/**
 * Implements hook_block_view().
 * @param {String} delta
 * @return {String}
 */
function system_block_view(delta) {
  try {
    switch (delta) {
      case 'main':
        // This is the main content block, it is required to be in a theme's
        // region for the content of a page to show up (nodes, users, taxonomy,
        // comments, etc). Depending on the menu link router, we need to route
        // this through the appropriate template files and functions.
        return drupalgap_render_page();
        break;
      case 'messages':
        // If there are any messages waiting to be displayed, render them, then
        // clear out the messages array.
        var html = '';
        if (drupalgap.messages.length == 0) { return html; }
        for (var index in drupalgap.messages) {
            if (!drupalgap.messages.hasOwnProperty(index)) { continue; }
            var msg = drupalgap.messages[index];
            html += '<div class="messages ' + msg.type + '">' +
              msg.message +
            '</div>';
        }
        drupalgap.messages = [];
        return html;
        break;
      case 'logo':
        if (drupalgap.settings.logo) {
          return '<div class="logo">' +
            l(theme('image', {'path': drupalgap.settings.logo}), '') +
          '</div>';
        }
        return '';
        break;
      case 'logout':
        if (Drupal.user.uid) { return theme('logout'); }
        return '';
        break;
      case 'title':
        var title_id = system_title_block_id(drupalgap_path_get());
        return '<h1 id="' + title_id + '" class="page-title"></h1>';
        break;
      case 'powered_by':
        return '<p style="text-align: center;">' + t('Powered by') + ': ' +
          l('DrupalGap', 'http://www.drupalgap.org', {InAppBrowser: true}) +
        '</p>';
        break;
      case 'help':
        return l('Help', 'http://www.drupalgap.org/support');
        break;
      default:
        return '';
        break;
    }
  }
  catch (error) { console.log('system_block_info - ' + error); }
}

/**
 * Implements hook_menu().
 * @return {Object}
 */
function system_menu() {
    var items = {
      'dashboard': {
        'title': t('Dashboard'),
        'page_callback': 'system_dashboard_page'
      },
      'error': {
        'title': t('Error'),
        'page_callback': 'system_error_page'
      },
      'offline': {
        'title': t('Offline'),
        'page_callback': 'system_offline_page'
      },
      '401': {
        title: '401 - ' + t('Not Authorized'),
        page_callback: 'system_401_page'
      },
      '404': {
        title: '404 - ' + t('Not Found'),
        page_callback: 'system_404_page'
      }
    };
    items['_reload'] = {
      title: t('Reloading') + '...',
      page_callback: 'system_reload_page',
      pageshow: 'system_reload_pageshow'
    };
    return items;
}

/**
 * Page callback for the 401 page.
 * @param {String} path
 * @return {String}
 */
function system_401_page(path) {
  return t('Sorry, you are not authorized to view this page.');
}

/**
 * Page callback for the 404 page.
 * @param {String} path
 * @return {String}
 */
function system_404_page(path) {
  return t('Sorry, the page you requested was not found.');
}

/**
 * The page callback for the reload page.
 * @return {String}
 */
function system_reload_page() {
  try {
    // Set aside any messages, then return an empty page.
    var messages = drupalgap_get_messages();
    if (!empty(messages)) {
      _system_reload_messages = messages.slice();
      drupalgap_set_messages([]);
    }
    return '';
  }
  catch (error) { console.log('system_reload_page - ' + error); }
}

/**
 * The pageshow callback for the reload page.
 */
function system_reload_pageshow() {
  try {
    // Set any messages that were set aside.
    if (_system_reload_messages && !empty(_system_reload_messages)) {
      for (var i = 0; i < _system_reload_messages.length; i++) {
        drupalgap_set_message(
          _system_reload_messages[i].message,
          _system_reload_messages[i].type
        );
      }
      _system_reload_messages = null;
    }
    drupalgap_loading_message_show();
  }
  catch (error) { console.log('system_reload_pageshow - ' + error); }
}

/**
 * Implements hook_system_drupalgap_goto_post_process().
 * @param {String} path
 */
function system_drupalgap_goto_post_process(path) {
  try {

    // To reload the "current" page, grab the path we have been requested to
    // reload, clear out our global reference to it, then go!
    // @see https://github.com/signalpoint/DrupalGap/issues/254
    if (path == '_reload') {
      if (!_system_reload_page) { return; }
      var path = '' + _system_reload_page;
      _system_reload_page = null;
      drupalgap_loading_message_show();
      drupalgap_goto(path, { reloadPage: true });
    }

  }
  catch (error) {
    console.log('system_drupalgap_goto_post_process - ' + error);
  }
}

/**
 * Page callback for the dashboard page.
 * @return {Object}
 */
function system_dashboard_page() {
  try {
    var content = {};
    content.site_info = {
      markup: '<h4 style="text-align: center;">' +
        Drupal.settings.site_path +
      '</h4>'
    };
    content.welcome = {
      markup: '<h2 style="text-align: center;">' +
        t('Welcome to DrupalGap') +
      '</h2>' +
      '<p style="text-align: center;">' +
        t('The open source application development kit for Drupal!') +
      '</p>'
    };
    if (drupalgap.settings.logo) {
      content.logo = {
        markup: '<center>' +
                 theme('image', {path: drupalgap.settings.logo}) +
               '</center>'
      };
    }
    content.get_started = {
      theme: 'button_link',
      text: t('Getting Started Guide'),
      path: 'http://www.drupalgap.org/get-started',
      options: {InAppBrowser: true}
    };
    content.support = {
      theme: 'button_link',
      text: t('Support'),
      path: 'http://www.drupalgap.org/support',
      options: {InAppBrowser: true}
    };
    return content;
  }
  catch (error) { console.log('system_dashboard_page - ' + error); }
}

/**
 * The page callback for the error page.
 * @return {Object}
 */
function system_error_page() {
    var content = {
      info: {
        markup: '<p>' + t('An unexpected error has occurred!') + '</p>'
      }
    };
    return content;
}

/**
 * Call back for the offline page.
 * @return {Object}
 */
function system_offline_page() {
  try {
    var content = {
      'message': {
        'markup': '<h2>' + t('Failed Connection') + '</h2>' +
          '<p>' + t("Oops! We couldn't connect to") + ':</p>' +
          '<p>' + Drupal.settings.site_path + '</p>'
      },
      'try_again': {
        'theme': 'button',
        'text': t('Try Again'),
        'attributes': {
          'onclick': 'javascript:offline_try_again();'
        }
      },
      'footer': {
        'markup': '<p>' +
          t("Check your device's network settings and try again.") +
        '</p>'
      }
    };
    return content;
  }
  catch (error) { console.log('system_offline_page - ' + error); }
}

/**
 * When the 'try again' button is clicked, check for a connection and if it has
 * one make a call to system connect then go to the front page, otherwise just
 * inform user the device is still offline.
 * @return {*}
 */
function offline_try_again() {
  try {
    var connection = drupalgap_check_connection();
    if (drupalgap.online) {
      system_connect({
        success: function() {
          drupalgap_goto('');
        }
      });
    }
    else {
      var msg = t('Sorry, no connection found!') + ' (' + connection + ')';
      drupalgap_alert(msg, {
          title: 'Offline'
      });
      return false;
    }
  }
  catch (error) { console.log('offline_try_again - ' + error); }
}

/**
 * Returns an array of region names defined by the system that themes must use.
 * We do this so Core and Contrib Modules can use these regions for UI needs.
 * @return {Array}
 */
function system_regions_list() {
    var regions = ['header', 'content', 'footer'];
    return regions;
}

/**
 * Add default buttons to a form and set its prefix.
 * @param {Object} form
 * @param {Object} form_state
 * @return {Object}
 */
function system_settings_form(form, form_state) {
  try {
    // Add submit button to form if one isn't present.
    if (!form.elements.submit) {
      form.elements.submit = {
        type: 'submit',
        value: t('Save configuration')
      };
    }
    // Add cancel button to form if one isn't present.
    if (!form.buttons.cancel) {
      form.buttons['cancel'] = drupalgap_form_cancel_button();
    }
    // Attach submit handler.
    form.submit.push('system_settings_form_submit');
    return form;
  }
  catch (error) { console.log('system_settings_form - ' + error); }
}

/**
 * Execute the system_settings_form.
 * @param {Object} form
 * @param {Object} form_state
 */
function system_settings_form_submit(form, form_state) {
  try {
    if (form_state.values) {
      for (var variable in form_state.values) {
          if (!form_state.values.hasOwnProperty(variable)) { continue; }
          var value = form_state.values[variable];
          variable_set(variable, value);
      }
    }
  }
  catch (error) { console.log('system_settings_form_submit - ' + error); }
}

/**
 * Returns the block id used on the system's title block.
 * @param {String} path
 * @return {String}
 */
function system_title_block_id(path) {
  try {
    var id = 'drupalgap_page_title_' + drupalgap_get_page_id(path);
    return id;
  }
  catch (error) { console.log('system_title_block_id - ' + error); }
}

/**
 * The default access callback function for the logout block. Allows the block
 * to only be shown when a user is viewing their own profile.
 * @param {Object} options
 * @return {Boolean}
 */
function system_logout_block_access_callback(options) {
  try {
    var args = arg(null, options.path);
    if (
      args &&
      args.length == 2 &&
      args[0] == 'user' &&
      args[1] == Drupal.user.uid
    ) { return true; }
    return false;
  }
  catch (error) {
    console.log('system_logout_block_access_callback - ' + error);
  }
}


// Used to hold onto the terms once they've been loaded into a widget, keyed by
// the form element's id, this allows (views exposed filters particularly) forms
// to easily retrieve the terms after they've been fetch from the server.
var _taxonomy_term_reference_terms = {};

/**
 * Extracts the taxonomy vocabularies JSON objects bundled by the Drupal module
 * into the system connect's resource results.
 * @param {Object} taxonomy_vocabularies
 * @return {Object}
 */
function drupalgap_taxonomy_vocabularies_extract(taxonomy_vocabularies) {
  try {
    var results = false;
    if (taxonomy_vocabularies && taxonomy_vocabularies.length > 0) {
      results = {};
      for (var index in taxonomy_vocabularies) {
          if (!taxonomy_vocabularies.hasOwnProperty(index)) { continue; }
          var vocabulary = taxonomy_vocabularies[index];
          results[vocabulary.machine_name] = vocabulary;
      }
    }
    return results;
  }
  catch (error) {
    console.log('drupalgap_taxonomy_vocabularies_extract - ' + error);
  }
}

/**
 * Implements hook_field_formatter_view().
 * @param {String} entity_type
 * @param {Object} entity
 * @param {Object} field
 * @param {Object} instance
 * @param {String} langcode
 * @param {Object} items
 * @param {*} display
 * @return {Object}
 */
function taxonomy_field_formatter_view(entity_type, entity, field, instance,
  langcode, items, display) {
  try {
    var element = {};
    // If items is a string, convert it into a single item JSON object.
    if (typeof items === 'string') { items = {0: {tid: items}}; }
    // It's possible the term items are wrapped in a language code, if they are,
    // pull them out.
    if (typeof items[language_default()] !== 'undefined') {
      items = items[language_default()];
    }
    if (!empty(items)) {
      for (var delta in items) {
          if (!items.hasOwnProperty(delta)) { continue; }
          var item = items[delta];
          var text = item.tid;
          if (item.name) { text = item.name; }
          var content = null;
          switch (display.type) {
            case 'taxonomy_term_reference_link':
              content = {
                theme: 'button_link',
                text: text,
                path: 'taxonomy/term/' + item.tid
              };
              break;
            case 'taxonomy_term_reference_plain':
              content = { markup: text };
              break;
            default:
              content = { markup: text };
              break;
          }
          element[delta] = content;
      }
    }
    return element;
  }
  catch (error) { console.log('taxonomy_field_formatter_view - ' + error); }
}

/**
 * Implements hook_field_widget_form().
 * @param {Object} form
 * @param {Object} form_state
 * @param {Object} field
 * @param {Object} instance
 * @param {String} langcode
 * @param {Object} items
 * @param {Number} delta
 * @param {Object} element
 */
function taxonomy_field_widget_form(form, form_state, field, instance, langcode,
  items, delta, element) {
  try {
    items[delta].type = 'hidden';
    // Build the widget and attach it to the item.
    var list_id = items[delta].id + '-list';
    var widget = {
      theme: 'item_list',
      items: [],
      attributes: {
        'id': list_id,
        'data-role': 'listview',
        'data-filter': 'true',
        'data-inset': 'true',
        'data-filter-placeholder': '...'
      }
    };
    items[delta].children.push(widget);
    // Attach JS to handle the widget's data fetching.
    var machine_name = field.settings.allowed_values[0].vocabulary;
    var vocabulary = taxonomy_vocabulary_machine_name_load(machine_name);
    var vid = vocabulary.vid;
    var js = '<script type="text/javascript">' +
      '$("#' + list_id + '").on("filterablebeforefilter",' +
        'function(e, d) {' +
          '_taxonomy_field_widget_form_autocomplete(' +
            '"' + items[delta].id + '", ' + vid + ', this, e, d' +
          ');' +
        '}' +
      ');' +
    '</script>';
    items[delta].children.push({
        markup: js
    });
  }
  catch (error) { console.log('taxonomy_field_widget_form - ' + error); }
}

var _taxonomy_field_widget_form_autocomplete_input = null;

/**
 * Handles the remote data fetching for taxonomy term reference autocomplete
 * tagging widget.
 * @param {String} id The id of the hidden input that will hold the term id.
 * @param {Number} vid
 * @param {Object} list The unordered list that displays the terms.
 * @param {Object} e
 * @param {Object} data
 */
function _taxonomy_field_widget_form_autocomplete(id, vid, list, e, data) {
  try {
    // Setup the vars to handle this widget.
    var $ul = $(list),
        $input = $(data.input),
        value = $input.val(),
        html = '';
    // Save a reference to this text input field. Then attach an on change
    // handler that will set the hidden input's value when the text field
    // changes. Keep in mind, later on we listen for clicks on autocomplete
    // results to populate this same hidden input's field.
    _taxonomy_field_widget_form_autocomplete_input = $input;
    $(_taxonomy_field_widget_form_autocomplete_input).on('change', function() {
        $('#' + id).val($(this).val());
    });
    // Clear the list, then set up its input handlers.
    $ul.html('');
    if (value && value.length > 0) {
        $ul.html('<li><div class="ui-loader">' +
          '<span class="ui-icon ui-icon-loading"></span>' +
          '</div></li>');
        $ul.listview('refresh');
        var query = {
          fields: ['tid', 'name'],
          parameters: {
            vid: vid,
            name: '%' + value + '%'
          },
          parameters_op: {
            name: 'like'
          }
        };
        taxonomy_term_index(query, {
            success: function(terms) {
              if (terms.length != 0) {
                // Extract the terms into items, then drop them in the list.
                var items = [];
                for (var index in terms) {
                    if (!terms.hasOwnProperty(index)) { continue; }
                    var term = terms[index];
                    var attributes = {
                      tid: term.tid,
                      vid: vid,
                      name: term.name,
                      onclick: '_taxonomy_field_widget_form_click(' +
                        "'" + id + "', " +
                        "'" + $ul.attr('id') + "', " +
                        'this' +
                      ')'
                    };
                    html += '<li ' + drupalgap_attributes(attributes) + '>' +
                      term.name +
                    '</li>';
                }
                $ul.html(html);
                $ul.listview('refresh');
                $ul.trigger('updatelayout');
              }
            }
        });
    }
  }
  catch (error) {
    console.log('_taxonomy_field_widget_form_autocomplete - ' + error);
  }
}

/**
 * Handles clicks on taxonomy term reference autocomplete widgets.
 * @param {String} id The id of the hidden input that will hold the term name.
 * @param {String} list_id The id of the list that holds the terms.
 * @param {Object} item The list item that was just clicked.
 */
function _taxonomy_field_widget_form_click(id, list_id, item) {
  try {
    var tid = $(item).attr('name');
    $('#' + id).val(tid);
    $(_taxonomy_field_widget_form_autocomplete_input).val($(item).attr('name'));
    $('#' + list_id).html('');
  }
  catch (error) { console.log('_taxonomy_field_widget_form_click - ' + error); }
}

/**
 * Implements hook_assemble_form_state_into_field().
 * @param {Object} entity_type
 * @param {String} bundle
 * @param {String} form_state_value
 * @param {Object} field
 * @param {Object} instance
 * @param {String} langcode
 * @param {Number} delta
 * @param {Object} field_key
 * @return {Object}
 */
function taxonomy_assemble_form_state_into_field(entity_type, bundle,
  form_state_value, field, instance, langcode, delta, field_key) {
  try {
    var result = null;
    switch (instance.widget.type) {
      case 'taxonomy_autocomplete':
        field_key.use_wrapper = false;
        result = form_state_value;
        break;
      case 'options_select':
        result = form_state_value;
        break;
    }
    return result;
  }
  catch (error) {
    console.log('taxonomy_assemble_form_state_into_field - ' + error);
  }
}

/**
 * Implements hook_menu().
 * @return {Object}
 */
function taxonomy_menu() {
    var items = {
      'taxonomy/vocabularies': {
        'title': t('Taxonomy'),
        'page_callback': 'taxonomy_vocabularies_page',
        'pageshow': 'taxonomy_vocabularies_pageshow'
      },
      'taxonomy/vocabulary/%': {
        'title': t('Taxonomy vocabulary'),
        'page_callback': 'taxonomy_vocabulary_page',
        'page_arguments': [2],
        'pageshow': 'taxonomy_vocabulary_pageshow'
      },
      'taxonomy/vocabulary/%/view': {
        'title': t('View'),
        'type': 'MENU_DEFAULT_LOCAL_TASK',
        'weight': -10
      },
      'taxonomy/vocabulary/%/edit': {
        'title': t('Edit'),
        'page_callback': 'entity_page_edit',
        'pageshow': 'entity_page_edit_pageshow',
        'page_arguments': [
          'taxonomy_form_vocabulary',
          'taxonomy_vocabulary',
          2
        ],
        'weight': 0,
        'type': 'MENU_LOCAL_TASK',
        'access_arguments': ['administer taxonomy'],
        options: {reloadPage: true}
      },
      'taxonomy/term/%': {
        'title': t('Taxonomy term'),
        'page_callback': 'taxonomy_term_page',
        'page_arguments': [2],
        'pageshow': 'taxonomy_term_pageshow'
      },
      'taxonomy/term/%/view': {
        'title': t('View'),
        'type': 'MENU_DEFAULT_LOCAL_TASK',
        'weight': -10
      },
      'taxonomy/term/%/edit': {
        'title': t('Edit'),
        'page_callback': 'entity_page_edit',
        'pageshow': 'entity_page_edit_pageshow',
        'page_arguments': ['taxonomy_form_term', 'taxonomy_term', 2],
        'weight': 0,
        'type': 'MENU_LOCAL_TASK',
        'access_arguments': ['administer taxonomy'],
        options: {reloadPage: true}
      }
    };
    return items;
}

/**
 * The taxonomy vocabulary form.
 * @param {Object} form
 * @param {Object} form_state
 * @param {Object} vocabulary
 * @return {Object}
 */
function taxonomy_form_vocabulary(form, form_state, vocabulary) {
  try {

    // Setup form defaults.
    form.entity_type = 'taxonomy_vocabulary';
    form.bundle = null;
    form.action = 'taxonomy/vocabularies';

    // Add the entity's core fields to the form.
    drupalgap_entity_add_core_fields_to_form(
      'taxonomy_vocabulary',
      null,
      form,
      vocabulary
    );

    // Add submit to form.
    form.elements.submit = {
      'type': 'submit',
      'value': t('Save')
    };

    // Add cancel button to form.
    form.buttons['cancel'] = drupalgap_form_cancel_button();

    // If we're editing a vocabulary add a delete button, if the user has
    // access.
    if (vocabulary && vocabulary.vid && user_access('administer taxonomy')) {
      form.buttons['delete'] = drupalgap_entity_edit_form_delete_button(
        'taxonomy_vocabulary',
        vocabulary.vid
      );
    }

    return form;
  }
  catch (error) { console.log('taxonomy_form_vocabulary - ' + error); }
}

/**
 * The taxonomy vocabulary form submit handler.
 * @param {Object} form
 * @param {Object} form_state
 */
function taxonomy_form_vocabulary_submit(form, form_state) {
  try {
    var vocabulary = drupalgap_entity_build_from_form_state(form, form_state);
    drupalgap_entity_form_submit(form, form_state, vocabulary);
  }
  catch (error) { console.log('taxonomy_form_vocabulary_submit - ' + error); }
}

/**
 * The taxonomy term form.
 * @param {Object} form
 * @param {Object} form_state
 * @param {Object} term
 * @return {Object}
 */
function taxonomy_form_term(form, form_state, term) {
  try {
    // Setup form defaults.
    form.entity_type = 'taxonomy_term';
    form.bundle = null;
    form.action = 'taxonomy/vocabularies';

    // Add the entity's core fields to the form.
    drupalgap_entity_add_core_fields_to_form('taxonomy_term', null, form, term);

    // Add submit to form.
    form.elements.submit = {
      'type': 'submit',
      'value': t('Save')
    };

    // Add cancel button to form.
    form.buttons['cancel'] = drupalgap_form_cancel_button();

    // If we are editing a term, add a delete button.
    if (term && term.tid && user_access('administer taxonomy')) {
      form.buttons['delete'] = drupalgap_entity_edit_form_delete_button(
        'taxonomy_term',
        term.tid
      );
    }

    return form;
  }
  catch (error) { console.log('taxonomy_form_term - ' + error); }
}

/**
 * The taxonomy term form submit handler.
 * @param {Object} form
 * @param {Object} form_state
 */
function taxonomy_form_term_submit(form, form_state) {
  try {
    var term = drupalgap_entity_build_from_form_state(form, form_state);
    drupalgap_entity_form_submit(form, form_state, term);
  }
  catch (error) { console.log('taxonomy_form_term_submit - ' + error); }
}

/**
 * Page callback for taxonomy/term/%.
 * @param {Number} tid
 * @return {Object}
 */
function taxonomy_term_page(tid) {
  try {
    if (tid) {
      var content = {
        container: _drupalgap_entity_page_container(
          'taxonomy_term',
          tid,
          'view'
        ),
        taxonomy_term_node_listing: {
          theme: 'jqm_item_list',
          items: [],
          attributes: {
            id: 'taxonomy_term_node_listing_items_' + tid
          }
        }
      };
      return content;
    }
    else { console.log('taxonomy_term_pageshow - No term id provided!'); }
  }
  catch (error) { console.log('taxonomy_term_pageshow - ' + error); }
}

/**
 * jQM pageshow callback for taxonomy/term/%
 * @param {Number} tid
 */
function taxonomy_term_pageshow(tid) {
  try {
    taxonomy_term_load(tid, {
        success: function(term) {
          var description = term.description ? term.description : '';
          var content = {
            'name': {
              'markup': '<h2>' + term.name + '</h2>'
            },
            'description': {
              'markup': '<p>' + description + '</p>'
            }
          };
          _drupalgap_entity_page_container_inject(
            'taxonomy_term',
            term.tid,
            'view',
            content
          );
          taxonomy_term_selectNodes(term.tid, {
              success: function(results) {
                // Extract the nodes into items, then drop them in the list.
                var items = [];
                for (var index in results) {
                    if (!results.hasOwnProperty(index)) { continue; }
                    var node = results[index];
                    items.push(l(node.title, 'node/' + node.nid));
                }
                drupalgap_item_list_populate(
                  '#taxonomy_term_node_listing_items_' + term.tid,
                  items
                );
              }
          });
        }
    });
  }
  catch (error) { console.log('taxonomy_term_pageshow - ' + error); }
}

/**
 * The selectNodes resource from the Taxonomy Term service.
 * @param {Number} tid The taxonomy term id.
 * @param {Object} options
 */
function taxonomy_term_selectNodes(tid, options) {
  try {
    // @TODO - move this function to jDrupal.
    options.method = 'POST';
    options.path = 'taxonomy_term/selectNodes.json';
    options.service = 'taxonomy_term';
    options.resource = 'selectNodes';
    options.data = JSON.stringify({ tid: tid});
    Drupal.services.call(options);
  }
  catch (error) { console.log('taxonomy_term_selectNodes - ' + error); }
}

/**
 * Page call back for taxonomy/vocabularies.
 * @return {Object}
 * @return {Object}
 */
function taxonomy_vocabularies_page() {
    // Place an empty item list that will hold a list of users.
    var content = {
      'vocabulary_listing': {
        'theme': 'jqm_item_list',
        'title': t('Vocabularies'),
        'items': [],
        'attributes': {'id': 'vocabulary_listing_items'}
      }
    };
    return content;
}

/**
 * jQM pageshow call back for taxonomy/vocabularies.
 */
function taxonomy_vocabularies_pageshow() {
  try {
    taxonomy_vocabulary_index(null, {
        success: function(vocabularies) {
          // Extract the vocabs into items, then drop them in the list.
          var items = [];
          for (var index in vocabularies) {
              if (!vocabularies.hasOwnProperty(index)) { continue; }
              var vocabulary = vocabularies[index];
              items.push(
                l(vocabulary.name, 'taxonomy/vocabulary/' + vocabulary.vid)
              );
          }
          drupalgap_item_list_populate('#vocabulary_listing_items', items);
        }
    });
  }
  catch (error) { console.log('taxonomy_vocabularies_pageshow - ' + error); }
}

/**
 * Page callback for taxonomy/vocabulary/%
 * @param {Number} vid
 * @return {String|Object}
 */
function taxonomy_vocabulary_page(vid) {
  try {
    if (vid) {
      var content = {
        container: _drupalgap_entity_page_container(
          'taxonomy_vocabulary',
          vid,
          'view'
        ),
        taxonomy_term_listing: {
          theme: 'jqm_item_list',
          title: t('Terms'),
          items: [],
          attributes: {
            id: 'taxonomy_term_listing_items_' + vid
          }
        }
      };
      return content;
    }
    else {
      console.log('taxonomy_vocabulary_page - No vocabulary id provided!');
    }
  }
  catch (error) { console.log('taxonomy_vocabulary_page - ' + error); }
}

/**
 * jQM pageshow callback for taxonomy/vocabulary/%.
 * @param {Number} vid
 */
function taxonomy_vocabulary_pageshow(vid) {
  try {
    taxonomy_vocabulary_load(vid, {
        success: function(vocabulary) {
          var content = {
            'name': {
              'markup': '<h2>' + vocabulary.name + '</h2>'
            },
            'description': {
              'markup': '<p>' + vocabulary.description + '</p>'
            }
          };
          _drupalgap_entity_page_container_inject(
            'taxonomy_vocabulary',
            vocabulary.vid,
            'view',
            content
          );
          var query = {
            parameters: {
              vid: vid
            }
          };
          taxonomy_term_index(query, {
              success: function(terms) {
                if (terms.length != 0) {
                  // Extract the terms into items, then drop them in the list.
                  var items = [];
                  for (var index in terms) {
                      if (!terms.hasOwnProperty(index)) { continue; }
                      var term = terms[index];
                      items.push(l(term.name, 'taxonomy/term/' + term.tid));
                  }
                  drupalgap_item_list_populate(
                    '#taxonomy_term_listing_items_' + vid,
                    items
                  );
                }
              }
          });
        }
    });
  }
  catch (error) { console.log('taxonomy_vocabulary_pageshow - ' + error); }
}

/**
 * Given a vocabulary machine name this will return the vocabulary id or false.
 * @param {String} name
 * @return {Number|Boolean}
 */
function taxonomy_vocabulary_get_vid_from_name(name) {
  try {
    var vocabulary = taxonomy_vocabulary_machine_name_load(name);
    if (vocabulary) { return vocabulary.vid; }
    return false;
  }
  catch (error) {
    console.log('taxonomy_vocabulary_get_vid_from_name - ' + error);
  }
}

/**
 * Given a vocabulary machine name this will return the JSON
 * object for the vocabulary that is attached to the
 * drupalgap.taxonomy_vocabularies object, or false if it doesn't exist.
 * @param {String} name
 * @return {Object|Boolean}
 */
function taxonomy_vocabulary_machine_name_load(name) {
  try {
    if (drupalgap.taxonomy_vocabularies &&
      drupalgap.taxonomy_vocabularies[name]) {
      return drupalgap.taxonomy_vocabularies[name];
    }
    return false;
  }
  catch (error) {
    console.log('taxonomy_vocabulary_machine_name_load - ' + error);
  }
}

/**
 * Theme's the html for a taxonomy term reference field.
 * @param {Object} variables
 * @return {String}
 */
function theme_taxonomy_term_reference(variables) {
  try {
    var html = '';

    // Make this a hidden field since the widget will just populate a value. If
    // somone wants to skip the input element html generation (e.g. taxonomy
    // term reference views exposed filter), let them do it, but by default
    // we'll render the input for them.
    var render_input_element = true;
    if (typeof variables.render_input_element !== 'undefined') {
      render_input_element = variables.render_input_element;
    }
    if (render_input_element) {
      variables.attributes.type = 'hidden';
      html += '<input ' + drupalgap_attributes(variables.attributes) + '/>';
    }

    // Is anyone flagging this as required? i.e. is it a views exposed filter?
    // @TODO - the field system that assembles the elements onto a form, when it
    // is a taxonomy term reference field, we need to pass along the required
    // option so when we load the items into the select list, we know whether or
    // not to include the "empty string" option. This is probably in field.js.
    var required = false;
    if (typeof variables.required !== 'undefined') {
      required = variables.required;
    }

    // Is this widget exposed (aka views exposed filter)?
    var exposed = false;
    if (typeof variables.exposed !== 'undefined') {
      exposed = variables.exposed;
    }

    // What vocabulary are we using?
    var machine_name =
      variables.field_info_field.settings.allowed_values[0].vocabulary;
    var taxonomy_vocabulary = taxonomy_vocabulary_machine_name_load(
      machine_name
    );

    // Prepare the variables for the widget and render it based on its type.
    var widget_type = variables.field_info_instance.widget.type;
    if (widget_type == 'options_select') { widget_type = 'select'; }
    var widget_function = 'theme_' + widget_type;
    var widget_id = variables.attributes.id + '-' + widget_type;
    if (drupalgap_function_exists(widget_function)) {

      // Grab the function in charge of themeing this widget.
      var fn = window[widget_function];
      // Build the variables for the widget.
      var widget_variables = {
        attributes: {
          id: widget_id,
          onchange: "_theme_taxonomy_term_reference_onchange(this, '" +
            variables.attributes.id + "');"
        }
      };

      // If the options were previously set aside for this widget, use them.
      var options_available = false;
      if (_taxonomy_term_reference_terms[variables.attributes.id]) {
        options_available = true;
        widget_variables.options =
          _taxonomy_term_reference_terms[variables.attributes.id];
      }

      // Was their a value present to include as the default value for the
      // widget, if so include it. If not, and this filter is not required, set
      // the default value to an empty string so the widget renders the default
      // option correctly. A views exposed filter uses 'All' instead of an
      // empty string.
      if (typeof variables.value !== 'undefined') {
        widget_variables.value = variables.value;
      }
      else if (!required) {
        if (exposed) { widget_variables.value = 'All'; }
        else { widget_variables.value = ''; }
      }

      // Render the widget.
      html += fn.call(null, widget_variables);

      // If the options weren't available, attach a pageshow handler to the
      // current page that will load the terms into the widget. Keep in mind,
      // this inline pageshow handler only gets called once when the first view
      // is loaded. That's why we later set aside the options so they can be
      // used again without having to fire the pageshow event.
      if (!options_available) {
        var options = {
          page_id: drupalgap_get_page_id(drupalgap_path_get()),
          jqm_page_event: 'pageshow',
          jqm_page_event_callback: '_theme_taxonomy_term_reference_load_items',
          jqm_page_event_args: JSON.stringify({
              taxonomy_vocabulary: taxonomy_vocabulary,
              element_id: variables.attributes.id,
              widget_id: widget_id,
              required: required,
              exposed: exposed
          })
        };
        html += drupalgap_jqm_page_event_script_code(options);
      }

    }
    else {
      console.log(
        'WARNING: theme_taxonomy_term_reference() - ' +
        'unsupported widget type! (' + widget_type + ')'
      );
    }
    return html;
  }
  catch (error) {
    console.log('theme_taxonomy_term_reference - ' + error);
  }
}

/**
 * An internal function used to load the taxonomy terms from a term reference
 * field and populate them into select list identified by options.widget_id.
 * @param {Object} options
 */
function _theme_taxonomy_term_reference_load_items(options) {
  try {
    // Build the index query, then make the call to the server.
    var query = {
      parameters: {
        vid: options.taxonomy_vocabulary.vid
      },
      options: {
        orderby: {
          weight: 'asc',
          name: 'asc'
        }
      }
    };
    taxonomy_term_index(query, {
        success: function(terms) {
          if (terms.length == 0) { return; }

          // As we iterate over the terms, we'll set them aside in a JSON
          // object so they can be used later.
          _taxonomy_term_reference_terms[options.element_id] = { };

          // Grab the widget.
          var widget = $('#' + options.widget_id);

          // If it's not required, place an empty option on the widget and set
          // it aside.
          if (!options.required) {
            var option = null;
            if (options.exposed) {
              option = '<option value="All">- ' + t('Any') + ' -</option>';
              _taxonomy_term_reference_terms[options.element_id]['All'] =
                '- Any -';
            }
            else {
              option = '<option value="">- ' + t('None') + ' -</option>';
              _taxonomy_term_reference_terms[options.element_id][''] =
                '- None -';
            }
            $(widget).append(option);
          }

          // Place each term in the widget as an option, and set the option
          // aside.
          for (var index in terms) {
              if (!terms.hasOwnProperty(index)) { continue; }
              var term = terms[index];
              var option = '<option value="' + term.tid + '">' +
                term.name +
              '</option>';
              $(widget).append(option);
              _taxonomy_term_reference_terms[options.element_id][term.tid] =
                term.name;
          }

          // Refresh the select list.
          $(widget).selectmenu('refresh', true);
        }
    });
  }
  catch (error) {
    console.log('_theme_taxonomy_term_reference_load_items - ' + error);
  }
}

/**
 * An internal function used by a taxonomy term reference field widget to
 * detect changes on it and populate the hidden field that holds the tid in the
 * form.
 * @param {Object} input
 * @param {String} id
 */
function _theme_taxonomy_term_reference_onchange(input, id) {
  try {
    $('#' + id).val($(input).val());
  }
  catch (error) {
    console.log('_theme_taxonomy_term_reference_onchange - ' + error);
  }
}

/**
 * Implements hook_views_exposed_filter().
 * @param {Object} form
 * @param {Object} form_state
 * @param {Object} element
 * @param {Object} filter
 * @param {Object} field
 */
function taxonomy_views_exposed_filter(
  form, form_state, element, filter, field) {
  try {
    //dpm('taxonomy_views_exposed_filter');
    //console.log(element);
    //console.log(filter);
    //console.log(field);

    // @TODO this filter loses its value after one submission, aka the next
    // submission will submit it as 'All' even though we have a term selected in
    // the widget from the previous submission.

    // Autocomplete.
    if (filter.options.type == 'textfield') {
      element.type = 'autocomplete';
      element.remote = true;
      element.custom = true;
      element.handler = 'index';
      element.entity_type = 'taxonomy_term';
      if (typeof filter.options.vocabulary !== 'undefined') {
        element.vid =
          taxonomy_vocabulary_get_vid_from_name(filter.options.vocabulary);
      }
      element.value = 'name';
      element.label = 'name';
      element.filter = 'name';
    }
    // Dropdown.
    else {
      // Change the input to hidden, then iterate over each vocabulary and inject
      // them into the widget. We'll just use a taxonomy term reference field and
      // fake its instance.
      element.type = 'hidden';
      for (var index in field.settings.allowed_values) {
        if (!field.settings.allowed_values.hasOwnProperty(index)) { continue; }
        var object = field.settings.allowed_values[index];

        // Build the variables for the widget.
        var variables = {
          required: element.required,
          render_input_element: false,
          attributes: {
            id: element.options.attributes.id
          },
          field_info_field: field,
          field_info_instance: {
            widget: {
              type: 'options_select'
            }
          },
          exposed: true
        };

        // If we have a default value, send it along.
        // @TODO add support for multiple values.
        if (!empty(filter.value)) {
          variables.value = parseInt(filter.value[0]);
          variables.attributes.value = variables.value;
        }

        // Add the widget as a child to the form element, including a label if
        // necessary (since the original label is lost because it was turned
        // into a hidden element).
        var child = '';
        if (!empty(element.title)) {
          child += theme('form_element_label', { element: element });
        }
        child += theme('taxonomy_term_reference', variables);
        element.children.push({ markup: child });

      }
    }
  }
  catch (error) { console.log('taxonomy_views_exposed_filter - ' + error); }
}

/**
 * The user login form.
 * @param {Object} form
 * @param {Object} form_state
 * @return {Object}
 */
function user_login_form(form, form_state) {
  try {
    form.entity_type = 'user';
    form.bundle = null;
    form.elements.name = {
      type: 'textfield',
      title: t('Username'),
      title_placeholder: true,
      required: true,
      attributes: {
        autocapitalize: 'none'
      }
    };
    form.elements.pass = {
      type: 'password',
      title: t('Password'),
      title_placeholder: true,
      required: true,
      attributes: {
        onkeypress: "drupalgap_form_onkeypress('" + form.id + "')"
      }
    };
    form.elements.submit = {
      type: 'submit',
      value: t('Login')
    };
    if (user_register_access()) {
      form.buttons['create_new_account'] = {
        title: t('Create new account'),
        attributes: {
          onclick: "drupalgap_goto('user/register')"
        }
      };
    }
    form.buttons['forgot_password'] = {
      title: t('Request new password'),
        attributes: {
          onclick: "drupalgap_goto('user/password')"
        }
    };
    return form;
  }
  catch (error) { console.log('user_login_form - ' + error); }
}

/**
 * The user login form submit handler.
 * @param {Object} form
 * @param {Object} form_state
 */
function user_login_form_submit(form, form_state) {
  try {
    user_login(form_state.values.name, form_state.values.pass, {
      success: function(result) {
        drupalgap_goto(
            typeof form.action !== 'undefined' ?
                form.action : drupalgap.settings.front,
            { reloadPage:true }
        );
      }
    });
  }
  catch (error) { console.log('user_login_form_submit - ' + error); }
}

/**
 * The user registration form.
 * @param {Object} form
 * @param {Object} form_state
 * @return {Object}
 */
function user_register_form(form, form_state) {
  try {
    form.entity_type = 'user';
    form.bundle = null;
    var description = t('Spaces are allowed; punctuation is not allowed except for periods, hyphens, apostrophes, and underscores.');
    form.elements.name = {
      type: 'textfield',
      title: t('Username'),
      title_placeholder: true,
      required: true,
      description: description
    };
    form.elements.mail = {
      type: 'email',
      title: t('E-mail address'),
      title_placeholder: true,
      required: true
    };
    // If e-mail verification is not required, provide password fields and
    // the confirm e-mail address field.
    if (!drupalgap.site_settings.user_email_verification) {
      form.elements.conf_mail = {
        type: 'email',
        title: t('Confirm e-mail address'),
        title_placeholder: true,
        required: true
      };
      form.elements.pass = {
        type: 'password',
        title: t('Password'),
        title_placeholder: true,
        required: true
      };
      form.elements.pass2 = {
        type: 'password',
        title: t('Confirm password'),
        title_placeholder: true,
        required: true
      };
    }
    // @TODO - instead of a null bundle, it appears drupal uses the bundle 'user' instead.
    drupalgap_field_info_instances_add_to_form('user', null, form, null);
    // Add registration messages to form.
    form.user_register = {
      'user_mail_register_no_approval_required_body':
        t('Registration complete!'),
      'user_mail_register_pending_approval_required_body':
        t('Registration complete, waiting for administrator approval.'),
      'user_mail_register_email_verification_body':
        t('Registration complete, check your e-mail inbox to verify the ' +
          'account.')
    };
    // Set the auto login boolean. This only happens when the site's account
    // settings require no e-mail verification. Others can stop this from
    // happening via hook_form_alter().
    form.auto_user_login = true;
    // Add submit button.
    form.elements.submit = {
      'type': 'submit',
      'value': t('Create new account')
    };
    return form;
  }
  catch (error) { console.log('user_register_form - ' + error); }
}

/**
 * Define the form's validation function (optional).
 * @param {Object} form
 * @param {Object} form_state
 */
function user_register_form_validate(form, form_state) {
  try {
    // If e-mail verification is not required, make sure the passwords match.
    if (!drupalgap.site_settings.user_email_verification &&
      form_state.values.pass != form_state.values.pass2) {
      drupalgap_form_set_error('pass', t('Passwords do not match!'));
    }
    // If there are two e-mail address fields on the form, make sure they match.
    if (!empty(form_state.values.mail) && !empty(form_state.values.conf_mail) &&
      form_state.values.mail != form_state.values.conf_mail
    ) { drupalgap_form_set_error('mail', t('E-mail addresses do not match!')); }
  }
  catch (error) {
    console.log('user_register_form_validate - ' + error);
  }
}

/**
 * The user registration form submit handler.
 * @param {Object} form
 * @param {Object} form_state
 */
function user_register_form_submit(form, form_state) {
  try {
    var account = drupalgap_entity_build_from_form_state(form, form_state);
    user_register(account, {
      success: function(data) {
        var config = form.user_register;
        var options = {
          title: t('Registered')
        };
        var destination = typeof form.action !== 'undefined' ?
            form.action : drupalgap.settings.front;
        // Check if e-mail verification is required or not..
        if (!drupalgap.site_settings.user_email_verification) {
          // E-mail verification not needed, if administrator approval is
          // needed, notify the user, otherwise log them in.
          if (drupalgap.site_settings.user_register == '2') {
            drupalgap_alert(
              config.user_mail_register_pending_approval_required_body,
              options
            );
            drupalgap_goto(destination);
          }
          else {
            drupalgap_alert(
              config.user_mail_register_no_approval_required_body,
              options
            );
            // If we're automatically logging in do it, otherwise just go to
            // the front page.
            if (form.auto_user_login) {
              user_login(account.name, account.pass, {
                  success: function(result) {
                    drupalgap_goto('');
                  }
              });
            }
            else { drupalgap_goto(destination); }
          }
        }
        else {
          // E-mail verification needed... notify the user.
          drupalgap_alert(
            config.user_mail_register_email_verification_body,
            options
          );
          drupalgap_goto(destination);
        }
      },
      error: function(xhr, status, message) {
        // If there were any form errors, display them.
        var msg = _drupalgap_form_submit_response_errors(form, form_state, xhr,
          status, message);
        if (msg) { drupalgap_alert(msg); }
      }
    });
  }
  catch (error) { console.log('user_register_form_submit - ' + error); }
}

/**
 * The user profile form.
 * @param {Object} form
 * @param {Object} form_state
 * @param {Object} account
 * @return {Object}
 */
function user_profile_form(form, form_state, account) {
  try {
    // Setup form defaults.
    form.entity_type = 'user';
    form.bundle = null;

    // Add the entity's core fields to the form.
    drupalgap_entity_add_core_fields_to_form('user', null, form, account);

    // Add the fields for accounts to the form.
    drupalgap_field_info_instances_add_to_form('user', null, form, account);

    // If the user can't change their user name, remove access to it.
    if (!user_access('change own username')) {
      form.elements['name'].access = false;
      form.elements['name'].required = false;
    }

    // If profile pictures are disabled, remove the core field from the form.
    if (drupalgap.site_settings.user_pictures == 0) {
      delete form.elements.picture;
    }

    // Add password fields to the form. We show the current password field only
    // if the user is editing their account. We show the password and confirm
    // password field no matter what.
    if (Drupal.user.uid == account.uid) {
      form.elements.current_pass = {
        'title': t('Current password'),
        'type': 'password',
        'description': t('Enter your current password to change the E-mail ' +
          'address or Password.')
      };
    }
    form.elements.pass_pass1 = {
      'title': t('Password'),
      'type': 'password'
    };
    form.elements.pass_pass2 = {
      'title': t('Confirm password'),
      'type': 'password',
      'description': t('To change the current user password, enter the new ' +
        'password in both fields.')
    };

    // Add submit to form.
    form.elements.submit = {
      'type': 'submit',
      'value': t('Save')
    };

    // Add cancel button to form.
    form.buttons['cancel'] = {
      'title': t('Cancel'),
      attributes: {
        onclick: 'javascript:drupalgap_back();'
      }
    };

    return form;
  }
  catch (error) { console.log('user_profile_form - ' + error); }
}

/**
 * The user profile form validate handler.
 * @param {Object} form
 * @param {Object} form_state
 */
function user_profile_form_validate(form, form_state) {
  try {
    // If they entered their current password, and entered new passwords, make
    // sure the new passwords match.
    if (!empty(form_state.values['current_pass'])) {
      if (
        !empty(form_state.values['pass_pass1']) &&
        !empty(form_state.values['pass_pass2']) &&
        form_state.values['pass_pass1'] != form_state.values['pass_pass2']
      ) {
        drupalgap_form_set_error('pass_pass1', t('Passwords do not match.'));
      }
    }
    // If they didn't enter their current password and entered new passwords,
    // tell them they need to enter their current password.
    else if (
      empty(form_state.values['current_pass']) &&
      !empty(form_state.values['pass_pass1']) &&
      !empty(form_state.values['pass_pass2'])
    ) {
      drupalgap_form_set_error(
        'current_pass',
        t('You must enter your current password to change your password.')
      );
    }
  }
  catch (error) { console.log('user_profile_form_validate - ' + error); }
}

/**
 * The user profile form submit handler.
 * @param {Object} form
 * @param {Object} form_state
 */
function user_profile_form_submit(form, form_state) {
  try {
    var account = drupalgap_entity_build_from_form_state(form, form_state);
    // If they provided their current password, and their new password, prepare
    // the account submission values.
    if (
      account.current_pass &&
      !empty(account.pass_pass1) &&
      !empty(account.pass_pass2)
    ) {
      account.pass = account.pass_pass1;
      delete account.pass_pass1;
      delete account.pass_pass2;
    }
    drupalgap_entity_form_submit(form, form_state, account);
  }
  catch (error) { console.log('user_profile_form_submit - ' + error); }
}

/**
 * The request new password form.
 * @param {Object} form
 * @param {Object} form_state
 * @return {Object}
 */
function user_pass_form(form, form_state) {
  try {
    form.elements['name'] = {
      type: 'textfield',
      title: t('Username or e-mail address'),
      required: true,
      attributes: {
        onkeypress: "drupalgap_form_onkeypress('" + form.id + "')"
      }
    };
    form.elements['submit'] = {
      type: 'submit',
      value: t('E-mail new password')
    };
    return form;
  }
  catch (error) { console.log('user_pass_form - ' + error); }
}

/**
 * The request new password form submission handler.
 * @param {Object} form
 * @param {Object} form_state
 */
function user_pass_form_submit(form, form_state) {
  try {
    user_request_new_password(form_state.values['name'], {
        success: function(result) {
          if (result[0]) {
            var msg =
              t('Further instructions have been sent to your e-mail address.');
            drupalgap_set_message(msg);
          }
          else {
            var msg =
              t('There was a problem sending an e-mail to your address.');
            drupalgap_set_message(msg, 'warning');
          }
          drupalgap_goto('user/login');
        }
    });
  }
  catch (error) { console.log('user_pass_form_submit - ' + error); }
}


/**
 * Determine whether the user has a given privilege. Optionally pass in a user
 * account JSON object for the second paramater to check that particular
 * account.
 * @param {String} string The permission, such as "administer nodes", being
 *                        checked for.
 * @return {Boolean}
 */
function user_access(string) {
  try {
    var account;
    if (arguments[1]) { account = arguments[1]; }
    else { account = Drupal.user; }
    if (account.uid == 1) { return true; }
    var access = false;
    for (var index in account.permissions) {
        if (!account.permissions.hasOwnProperty(index)) { continue; }
        var object = account.permissions[index];
        if (object.permission == string) {
          access = true;
          break;
        }
    }
    return access;
  }
  catch (error) { console.log('user_access - ' + error); }
}

/**
 * The access callback for the user/%/edit page.
 * @param {Object} account
 * @return {Boolean}
 */
function user_edit_access(account) {
  try {
    // If the current user is looking at their own account, or if they have the
    // 'administer users' permission, then they are allowed to edit the account.
    if (Drupal.user.uid == account.uid || user_access('administer users')) {
      return true;
    }
    return false;
  }
  catch (error) { console.log('user_edit_access - ' + error); }
}

/**
 * A page call back function to display a simple list of drupal users.
 * @return {Object}
 */
function user_listing() {
  var content = {};
  content['user_list'] = {
    theme: 'view',
    format: 'ul',
    path: 'drupalgap/views_datasource/drupalgap_users',
    row_callback: 'user_listing_row',
    empty_callback: 'user_listing_empty',
    attributes: {
      id: 'user_listing_' + user_password() /* make a random id */
    }
  };
  return content;
}

/**
 * The row callback for the simple user listing page.
 * @param view
 * @param row
 * @returns {String}
 */
function user_listing_row(view, row) {
  try {
    return l(t(row.name), 'user/' + row.uid);
  }
  catch (error) { console.log('user_listing_row - ' + error); }
}

/**
 * The empty callback for the simple user listing page.
 * @param view
 * @returns {String}
 */
function user_listing_empty(view) { return t('Sorry, no users were found.'); }

/**
 * The user logout page callback.
 * @return {String}
 */
function user_logout_callback() {
  return '<p>' + t('Logging out') + '...</p>';
}

/**
 * The user logout pageshow callback. This actually handles the call to the
 * user logout service resource.
 */
function user_logout_pageshow() {
  try {
    user_logout({
      success: function(data) {
        drupalgap_remove_pages_from_dom();
        drupalgap_goto(drupalgap.settings.front, { reloadPage:true });
      }
    });
  }
  catch (error) { console.log('user_logout_pagechange - ' + error); }
}

/**
 * Implements hook_menu().
 * @return {Object}
 */
function user_menu() {
    var items = {
      'user': {
        'page_callback': 'user_page'
      },
      'user/login': {
        'title': t('Login'),
        'page_callback': 'drupalgap_get_form',
        'page_arguments': ['user_login_form'],
        options: {reloadPage: true}
      },
      'user/logout': {
        'title': t('Logout'),
        'page_callback': 'user_logout_callback',
        'pageshow': 'user_logout_pageshow',
        options: {reloadPage: true}
      },
      'user/register': {
        'title': t('Register'),
        'page_callback': 'drupalgap_get_form',
        'page_arguments': ['user_register_form'],
        'access_callback': 'user_register_access',
        options: {reloadPage: true}
      },
      'user/%': {
        title: t('My account'),
        title_callback: 'user_view_title',
        title_arguments: [1],
        page_callback: 'user_view',
        pageshow: 'user_view_pageshow',
        page_arguments: [1]
      },
      'user/%/view': {
        'title': t('View'),
        'type': 'MENU_DEFAULT_LOCAL_TASK',
        'weight': -10
      },
      'user/%/edit': {
        'title': t('Edit'),
        'page_callback': 'entity_page_edit',
        'pageshow': 'entity_page_edit_pageshow',
        'page_arguments': ['user_profile_form', 'user', 1],
        'access_callback': 'user_edit_access',
        'access_arguments': [1],
        'weight': 0,
        'type': 'MENU_LOCAL_TASK',
        options: {reloadPage: true}
      }
    };
    items['user-listing'] = {
      title: t('Users'),
      page_callback: 'user_listing',
      access_arguments: ['access user profiles']
    };
    items['user/password'] = {
      title: t('Request new password'),
      page_callback: 'drupalgap_get_form',
      page_arguments: ['user_pass_form']
    };
    return items;
}

/**
 * Page callback for the user page.
 * @return {String}
 */
function user_page() {
    // NOTE, this page call back isn't actually used, because the 'user' path
    // in DrupalGap is redirected to either 'user/login' or e.g.
    // 'user/123'.
    return 'user_page()';
}

/**
 * Access callback for the user registration page.
 * @return {Boolean}
 */
function user_register_access() {
  try {
    switch (drupalgap.site_settings.user_register.toString()) {
      case '0': // admins only can register
        return false;
        break;
      case '1': // visitors can register
      case '2': // visitors can register, but admin approval is needed
        return true;
        break;
    }
  }
  catch (error) { console.log('user_register_access - ' + error); }
}

/**
 * Implements hook_services_postprocess().
 * @param {Object} options
 * @param {Object} result
 */
function user_services_postprocess(options, result) {
  try {
    // Don't process any other services.
    if (options.service != 'user') { return; }
    var resources = ['login', 'logout', 'register'];
    // Only process login, logout and registration.
    if (!in_array(options.resource, resources) || (arg(0) != 'user' && !in_array(arg(1), resources))) {
      return;
    }
    // If there were any form errors, alert them to the user.
    if (!result.responseText) { return; }
    var response = JSON.parse(result.responseText);
    if ($.isArray(response)) {
      var msg = '';
      for (var index in response) {
          if (!response.hasOwnProperty(index)) { continue; }
          var message = response[index];
          msg += t(message) + '\n';
      }
      if (msg != '') { drupalgap_alert(msg); }
    }
  }
  catch (error) { console.log('user_services_postprocess - ' + error); }
}

/**
 * Implements hook_theme().
 * @return {Object}
 */
function user_theme() {
    return {
      user_picture: {
        template: 'user-picture'
      },
      user_profile: {
        template: 'user-profile'
      }
    };
}

/**
 * Page callback for user/%.
 * @param {Number} uid
 * @return {Object}
 */
function user_view(uid) {
  try {
    if (uid) {
      var content = {
        container: _drupalgap_entity_page_container('user', uid, 'view')
      };
      return content;
    }
    else { console.log('user_view - No user id provided!'); }
  }
  catch (error) { console.log('user_view - ' + error); }
}

/**
 * jQM pageshow handler for node/% pages.
 * @param {Number} uid
 */
function user_view_pageshow(uid) {
  try {
    user_load(uid, {
        success: function(account) {
          // Determine the incoming arguments, and set defaults if necessary.
          var view_mode = 'full';
          var langcode = null;
          if (arguments[1]) { view_mode = arguments[1]; }
          if (arguments[2]) { langcode = arguments[2]; }
          if (!langcode) { langcode = language_default(); }
          if (account) {
            var build = {
              'theme': 'user_profile',
              'account': account,
              'view_mode': view_mode,
              'language': langcode,
              'name': {'markup': account.name},
              'created': {
                markup:
                '<div class="user_profile_history"><h3>' +
                  t('History') +
                '</h3>' +
                '<dl><dt>' + t('Member since') + '</td></dt><dd>' +
                  (new Date(parseInt(account.created) * 1000)).toDateString() +
                '</dd></div>'
              }
            };
            // Any content?
            if (typeof account.content !== 'undefined') {
              build.content = { markup: account.content };
            }
            // Any picture?
            if (account.picture && account.picture.fid) {
              build.picture = {
                'theme': 'image',
                'path': image_style_url(
                  drupalgap.site_settings.user_picture_style,
                  account.picture.uri
                )
              };
            }
            _drupalgap_entity_page_container_inject(
              'user', account.uid, 'view', build
            );
          }
        }
    });
  }
  catch (error) { console.log('user_view_pageshow - ' + error); }
}

/**
 * Title callback for the user profile view page.
 * @param {Function} callback
 * @param {Number} uid
 */
function user_view_title(callback, uid) {
  try {
    user_load(uid, {
        success: function(account) {
          callback.call(null, account.name);
        }
    });
  }
  catch (error) { console.log('user_view_title - ' + error); }
}

/**
 * Given a user role (string), this determines if the current user has the role.
 * Returns true if the user has the role, false otherwise. You may pass in a
 * user account object to check against a certain account, instead of the
 * current user.
 * @param {String} role
 * @return {Boolean}
 */
function drupalgap_user_has_role(role) {
  try {
    var has_role = false;
    var account = null;
    if (arguments[1]) { account = arguments[1]; }
    else { account = Drupal.user; }
    for (var rid in account.roles) {
        if (!account.roles.hasOwnProperty(rid)) { continue; }
        var value = account.roles[rid];
        if (role == value) {
          has_role = true;
          break;
        }
    }
    return has_role;
  }
  catch (error) { console.log('drupalgap_user_has_role - ' + error); }
}


// Holds onto views contexts on a per page basis.
var _views_embedded_views = {};

/**
 * Gets an embedded view for the given page id.
 * @param {String} page_id
 * @returns {Object}
 */
function views_embedded_view_get(page_id) {
  try {
    if (!_views_embedded_views[page_id]) { return null; }
    var property = arguments[1];
    if (!property) { return _views_embedded_views[page_id]; }
    return _views_embedded_views[page_id][property];
  }
  catch (error) { console.log('views_embedded_view_get - ' + error); }
}

/**
 * Given a page id, property name and value, this will set the value for the
 * embedded view.
 * @param {String} page_id
 * @param {String} property
 * @param {*} value
 */
function views_embedded_view_set(page_id, property, value) {
  try {
    if (!_views_embedded_views[page_id]) {
      _views_embedded_views[page_id] = {};
    }
    _views_embedded_views[page_id][property] = value;
  }
  catch (error) { console.log('views_embedded_view_set - ' + error); }
}

/**
 * Given a page id, this will delete the embedded view for the page from
 * memory. Returns true if successful, false otherwise.
 * @param page_id
 * @returns {boolean}
 */
function views_embedded_view_delete(page_id) {
  try {
    if (!_views_embedded_views[page_id]) { return false; }
    var property = arguments[1];
    if (!property) { delete _views_embedded_views[page_id]; }
    else { delete _views_embedded_views[page_id][property]; }
    return true;
  }
  catch (error) { console.log('views_embedded_view_delete - ' + error); }
}

/**
 * Given a path to a Views Datasource (Views JSON) view, this will get the
 * results and pass them along to the provided success callback.
 * @param {String} path
 * @param {Object} options
 */
function views_datasource_get_view_result(path, options) {
  try {
    // Since we do not use clean URLs, replace any potential question marks from
    // the path with an ampersand so the path will not be invalid.
    if (path.indexOf('?') != -1) {
      var replacement = path.replace('?', '&');
      path = replacement;
    }
    // If local storage caching is enabled, let's see if we can load the results
    // from there. If we successfully loaded the result, make sure it didn't
    // expire. If it did expire, remove it from local storage. If we don't have
    // it in local storage, or local storage caching is disabled, call Drupal to
    // get the results. After the results are fetched, save them to local
    // storage with an expiration time, if necessary. We use the path of the
    // view as the local storage key.
    // If we are resetting, remove the item from localStorage.
    if (options.reset) { window.localStorage.removeItem(path); }
    else if (Drupal.settings.cache.views.enabled) {
      var result = window.localStorage.getItem(path);
      if (result) {
        // Loaded from local storage, did it expire?
        result = JSON.parse(result);
        if (typeof result.expiration !== 'undefined' &&
          result.expiration != 0 &&
          time() > result.expiration
        ) {
          // Expired, remove from local storage.
          window.localStorage.removeItem(path);
        }
        else if (options.success) {
          // Did not expire yet, use it.
          options.success(result);
          return;
        }
      }
    }
    Drupal.services.call({
        endpoint: '',
        service: 'views_datasource',
        resource: '',
        method: 'GET',
        path: path,
        success: function(result) {
          try {
            if (options.success) {
              // Add the path to the result.
              result.path = path;
              // If any views caching is enabled, cache the results in local
              // storage.
              if (Drupal.settings.cache.views.enabled) {
                var expiration =
                  time() + Drupal.settings.cache.views.expiration;
                if (Drupal.settings.cache.views.expiration == 0) {
                  expiration = 0;
                }
                // Saving to local storage.
                result.expiration = expiration;
                window.localStorage.setItem(path, JSON.stringify(result));
              }
              options.success(result);
            }
          }
          catch (error) {
            console.log(
              'views_datasource_get_view_result - success - ' + error
            );
          }
        },
        error: function(xhr, status, message) {
          try {
            if (options.error) { options.error(xhr, status, message); }
          }
          catch (error) {
            console.log('views_datasource_get_view_result - error - ' + error);
          }
        }
    });
  }
  catch (error) { console.log('views_datasource_get_view_result - ' + error); }
}

/**
 * The exposed filter form builder.
 * @param {Object} form
 * @param {Object} form_state
 * @param {Object} options
 * @return {Object}
 */
function views_exposed_form(form, form_state, options) {
  try {

    //console.log(form);
    //console.log(form_state);
    //console.log(options);

    // @TODO we tried to make the filters collapsible, but jQM doesn't seem to
    // like collapsibles with form inputs in them... weird.
    //var title = form.title ? form.title : 'Filter';
    //form.prefix += '<div data-role="collapsible"><h2>' + title + '</h2>';

    // Attach the variables to the form so it can be used later.
    form.variables = options.variables;

    for (var views_field in options.filter) {
        if (!options.filter.hasOwnProperty(views_field)) { continue; }
        var filter = options.filter[views_field];

        // Prep the element basics.
        var element_id = null;
        var element = null;
        // @TODO - This ID is NOT unique enough, it will cause DOM collisions if
        // the same exposed filter gets used twice...
        // @TODO - we should be attaching the value right here, so filter
        // implementors don't need to extract it on their own. Once this is
        // implemented. Then update *_views_exposed_filter() to use this value
        // instead.
        element_id = filter.options.expose.identifier;
        element = {
          id: element_id,
          type: filter.options.group_info.widget,
          title: filter.options.expose.label,
          required: filter.options.expose.required,
          options: {
            attributes: {
              id: drupalgap_form_get_element_id(element_id, form.id)
            }
          },
          views_field: views_field,
          filter: filter,
          children: []
        };
        // Attach the value to the element, if there is one.
        // @TODO Add multi value support.
        //if (!empty(filter.value)) { element.value = filter.value[0]; }

        // Grab the field name and figure out which module is in charge of it.
        var field_name = filter.definition.field_name;
        if (field_name) {

          // This is an entity field...

          // Grab the field info, and determine the module that will handle it.
          // Then see if hook_views_exposed_filter() has been implemented by
          // that module. That module will be used to assemble the element. If
          // we don't have a handler then just skip the filter.
          var field = drupalgap_field_info_field(field_name);
          var module = field.module;
          var handler = module + '_views_exposed_filter';
          if (!drupalgap_function_exists(handler)) {
            dpm(
              'WARNING: views_exposed_form() - ' + handler + '() must be ' +
              'created to assemble the ' + field.type + ' filter used by ' +
              field_name
            );
            continue;
          }

          // We have a handler, let's call it so the element can be assembled.
          window[handler](form, form_state, element, filter, field);

      }
      else {
          // This is NOT an entity field, so it is probably a core field (e.g.
          // nid, status, etc). Let's assemble the element. In some cases we may
          // just be able to forward it to a pre-existing handler.

          // "Has taxonomy term" exposed filter.
          if (filter.definition.handler == 'views_handler_filter_term_node_tid') {
            element.type = 'autocomplete';
            var autocomplete = {
              remote: true,
              custom: true,
              handler: 'index',
              entity_type: 'taxonomy_term',
              value: 'name',
              label: 'name',
              filter: 'name'
            };
            if (filter.options.vocabulary != '') {
              autocomplete.vid = taxonomy_vocabulary_get_vid_from_name(filter.options.vocabulary);
            }
            $.extend(element, autocomplete, true);
          }
          else if (element.type == 'select') {
            list_views_exposed_filter(form, form_state, element, filter, null);
          }
          else {
            dpm(
              'WARNING: views_exposed_form() - I do not know how to handle ' +
              'the exposed filter for the "' + views_field + '" field'
            );
            console.log(filter);
            continue;
          }
      }

      // Finally attach the assembled element to the form.
      if (element_id) { form.elements[element_id] = element; }

    }

    // Add the submit button.
    form.elements['submit'] = {
      type: 'submit',
      value: options.exposed_data.submit
    };

    // Add the reset button, if necessary.
    if (
      options.exposed_data.reset &&
      views_embedded_view_get(form.variables.page_id, 'exposed_filter_reset')
    ) {
      form.buttons['reset'] = {
        title: options.exposed_data.reset,
        attributes: {
          id: form.id + '-reset',
          onclick: 'views_exposed_form_reset()'
        }
      };
    }

    //form.suffix += '</div>';

    return form;
  }
  catch (error) { console.log('views_exposed_form - ' + error); }
}

/**
 * The exposed filter submission handler.
 * @param {Object} form
 * @param {Object} form_state
 */
function views_exposed_form_submit(form, form_state) {
  try {
    var page_id = form.variables.page_id;
    // Assemble the query string from the form state values.
    var query = '';
    for (var key in form_state.values) {
        if (!form_state.values.hasOwnProperty(key)) { continue; }
        var value = form_state.values[key];
        if (empty(value)) { continue; }
        query += key + '=' + encodeURIComponent(value) + '&';
    }
    if (!empty(query)) { query = query.substr(0, query.length - 1); }

    // If there is a query set aside from previous requests, and it is equal to
    // the submitted query, then stop the submission. Otherwise remove it from
    // the path.
    var _query = views_embedded_view_get(page_id, 'exposed_filter_query');
    if (_query) {
      if (_query == query) { return; }
      if (form.variables.path.indexOf('&' + _query) != -1) {
        form.variables.path =
          form.variables.path.replace('&' + _query, '');
      }
    }

    // Set aside a copy of the query string, so it can be removed from the path
    // upon subsequent submissions of the form.
    views_embedded_view_set(page_id, 'exposed_filter_query', query);

    // Indicate that we have an exposed filter, so the reset button can easily
    // be shown/hidden.
    views_embedded_view_set(page_id, 'exposed_filter_reset', true);

    // Update the path for the view, reset the pager, hold onto the variables,
    // then theme the view.
    form.variables.path += '&' + query;
    form.variables.page = 0;
    views_embedded_view_set(
      page_id,
      'exposed_filter_submit_variables',
      form.variables
    );
    _theme_view(form.variables);

  }
  catch (error) { console.log('views_exposed_form_submit - ' + error); }
}

/**
 * Handles clicks on the views exposed filter form's reset button.
 */
function views_exposed_form_reset() {
  try {
    var page_id = drupalgap_get_page_id();
    // Reset the path to the view, the page, and the global vars, then re-theme
    // the view.
    var exposed_filter_submit_variables =
      views_embedded_view_get(page_id, 'exposed_filter_submit_variables');
    exposed_filter_submit_variables.path =
      exposed_filter_submit_variables.path.replace(
        '&' + views_embedded_view_get(page_id, 'exposed_filter_query'),
        ''
      );
    exposed_filter_submit_variables.page = 0;
    views_embedded_view_set(
      page_id,
      'exposed_filter_submit_variables',
      exposed_filter_submit_variables
    );
    views_embedded_view_set(page_id, 'exposed_filter_reset', false);
    views_embedded_view_set(page_id, 'exposed_filter_query', null);
    _theme_view(exposed_filter_submit_variables);
  }
  catch (error) { console.log('views_exposed_form_reset - ' + error); }
}

/**
 * Themes a view.
 * @param {Object} variables
 * @return {String}
 */
function theme_view(variables) {
  try {
    // Throw a warning if no id is provided.
    if (!variables.attributes.id) {
      console.log(
        'WARNING: theme_view() - No id specified on attributes! ' +
        'A random id will be generated instead.'
      );
      variables.attributes.id = 'views-view--' + user_password();
    }
    // Since multiple pages can stack up in the DOM, warn the developer if they
    // re-use a Views ID that is already in the DOM. If the ID hasn't been used
    // yet, add it to the drupalgap.views.ids array.
    if (in_array(variables.attributes.id, drupalgap.views.ids)) {
      // Double check to make sure it is actually in the DOM. If it wasn't
      // really in the DOM, remove the id from drupalgap.views.ids and continue
      // onward.
      if (!$('#' + variables.attributes.id)) {
        // @see http://stackoverflow.com/a/3596141/763010
        drupalgap.views.ids.splice(
          $.inArray(variables.attributes.id, drupalgap.views.ids),
          1
        );
      }
      else {
        dpm(
          'WARNING: theme_view() - this id already exists in the DOM: #' +
          variables.attributes.id +
          ' - the view will be rendered into the first container that is ' +
          'located in the DOM - if you are re-using this same view, it is ' +
          'recommended to append a unique identifier (e.g. an entity id) to ' +
          'your views id, that way you can re-use the same view across ' +
          'multiple pages.'
        );
      }
    }
    else { drupalgap.views.ids.push(variables.attributes.id); }

    // Keep track of the page id for context.
    var page_id = drupalgap_get_page_id();
    variables.page_id = page_id;

    // Since we'll by making an asynchronous call to load the view, we'll just
    // return an empty div container, with a script snippet to load the view.
    variables.attributes['class'] += 'view ';
    var html =
      '<div ' + drupalgap_attributes(variables.attributes) + ' ></div>';
    var options = {
      page_id: page_id,
      jqm_page_event: 'pageshow',
      jqm_page_event_callback: '_theme_view',
      jqm_page_event_args: JSON.stringify(variables)
    };
    return html += drupalgap_jqm_page_event_script_code(options, variables.attributes.id);
  }
  catch (error) { console.log('theme_view - ' + error); }
}

/**
 * An internal function used to theme a view.
 * @param {Object} variables
 */
function _theme_view(variables) {
  try {
    // Determine what page of the view we're on.
    var page = 0;
    if (variables.page) { page = variables.page; }
    // Make a copy of variables and prepare the success callback for embedding
    // the view.
    var variables_copy = $.extend(
      {},
      {
        success: function(html) {
          try {
            $('#' + variables.attributes.id).html(html);
          }
          catch (error) {
            console.log('_theme_view - success - ' + error);
          }
        }
      },
      variables
    );
    // Finally, embed the view.
    views_embed_view(variables.path + '&page=' + page, variables_copy);
  }
  catch (error) { console.log('_theme_view - ' + error); }
}

/**
 * Returns the html string to options.success, used to embed a view.
 * @param {String} path
 * @param {Object} options
 */
function views_embed_view(path, options) {
  try {
    views_datasource_get_view_result(path, {
        success: function(results) {
          try {
            views_embedded_view_set(options.page_id, 'results', results);
            views_embedded_view_set(options.page_id, 'options', options);
            if (!options.success) { return; }
            options.results = results;

            // Render the view if there are some results, or if there are no results and an
            // empty_callback has been specified. Otherwise remove the empty div container for
            // the view from the DOM.
            if (results.view.count != 0 || results.view.count == 0 && options.empty_callback) {
              options.success(theme('views_view', options));
            }
            else {
              var elem = document.getElementById(options.attributes.id);
              elem.parentElement.removeChild(elem);
            }
          }
          catch (error) {
            console.log('views_embed_view - success - ' + error);
          }
        },
        error: function(xhr, status, message) {
          try {
            views_embedded_view_set(options.page_id, 'results', null);
            if (options.error) { options.error(xhr, status, message); }
          }
          catch (error) {
            console.log('views_embed_view - error - ' + error);
          }
        }
    });
  }
  catch (error) { console.log('views_embed_view - ' + error); }
}

/**
 * Theme's a view.
 * @param {Object} variables
 * @return {String}
 */
function theme_views_view(variables) {
  try {
    var html = '';

    // Extract the results.
    var results = views_embedded_view_get(variables.page_id, 'results');
    if (!results) { return html; }

    // Figure out the format.
    if (!variables.format) { variables.format = 'unformatted_list'; }

    // Extract the root and child object name.
    var root = results.view.root;
    var child = results.view.child;

    // Is there a title to display?
    if (variables.title) {
      var title_attributes = variables.title_attributes ?
        variables.title_attributes : null;
      html += theme(
        'header',
        { text: variables.title, attributes: title_attributes }
      );
      // Place spacers after the header for each format, except unformatted.
      if (variables.format != 'unformatted') {
        html += theme('views_spacer', null);
      }
    }

    // Render the exposed filters if there are any, and the developer didn't explicitly exclude
    // them via the Views Render Array.
    var views_exposed_form_html = '';
    if (typeof results.view.exposed_data !== 'undefined' &&
      (typeof variables.exposed_filters === 'undefined' || variables.exposed_filters)
    ) {
      views_exposed_form_html = drupalgap_get_form(
        'views_exposed_form', {
          exposed_data: results.view.exposed_data,
          exposed_raw_input: results.view.exposed_raw_input,
          filter: results.view.filter,
          variables: variables
        }
      );
    }

    // Determine views container selector and place it in the global context.
    var selector = '#' + variables.page_id + ' #' + variables.attributes.id;
    views_embedded_view_set(variables.page_id, 'selector', selector);

    // Are the results empty? If so, return the empty callback's html, if it
    // exists. Often times, the empty callback will want to place html that
    // needs to be enhanced by jQM, therefore we'll set a timeout to trigger
    // the creation of the content area.
    // @TODO putting views_litepager support here is a hack, we should be
    // implementing views_litepager_views_view for theme_views_view() instead.
    var views_litepager_present = module_exists('views_litepager');
    if (
      (results.view.count == 0 && !views_litepager_present) ||
      (views_litepager_present && results.view.pages == null && results.view.count == 0)
    ) {
      $(selector).hide();
      setTimeout(function() {
          $(selector).trigger('create').show('fast');
      }, 100);
      if (
        variables.empty_callback &&
        function_exists(variables.empty_callback)
      ) {
        var empty_callback = window[variables.empty_callback];
        return views_exposed_form_html + drupalgap_render(empty_callback(results.view));
      }
      return html + views_exposed_form_html;
    }

    // The results are not empty...

    // Append the exposed filter html.
    html += views_exposed_form_html;

    // Render all the rows.
    var result_formats = drupalgap_views_get_result_formats(variables);
    var rows = '' + result_formats.open + drupalgap_views_render_rows(
      variables,
      results,
      root,
      child,
      result_formats.open_row,
      result_formats.close_row
    ) + result_formats.close;

    // If we have any pages, render the pager above or below the results
    // according to the pager_pos setting.
    var pager = '';
    if (results.view.pages) { pager = theme('pager', variables); }
    var pager_pos = 'top';
    if (typeof variables.pager_pos !== 'undefined') {
      pager_pos = variables.pager_pos;
    }

    // Append the rendered rows and the pager to the html string according to
    // the pager position, unless the views infinite scroll module is enabled,
    // then no pager at all.
    // @TODO having this special case for views_infinite_scroll is a hack, we
    // obviously need a hook or something around here...
    if (
      module_exists('views_infinite_scroll') &&
      views_infinite_scroll_ok()
    ) { html += rows; }
    else if (pager_pos == 'top') {
      html += pager;
      if (!empty(pager)) { html += theme('views_spacer', null); }
      html += rows;
    }
    else if (pager_pos == 'bottom') {
      html += rows;
      if (!empty(pager)) { html += theme('views_spacer', null); }
      html += pager;
    }
    else {
      console.log('WARNING: theme_views_view - unsupported pager_pos (' +
        pager_pos +
      ')');
    }

    // Since the views content is injected dynamically after the page is loaded,
    // we need to have jQM refresh the page to add its styling.
    $(selector).hide();
    setTimeout(function() {
        $(selector).trigger('create').show('fast');
    }, 100);
    return html;
  }
  catch (error) { console.log('theme_views_view - ' + error); }
}

/**
 * Themes a spacer that can be placed between displayed components of the view.
 * @param {Object} variables
 * @return {String}
 */
function theme_views_spacer(variables) {
    return '<h2 class="dg_empty_list_header">&nbsp;</h2>';
}

/**
 * Themes a pager.
 * @param {Object} variables
 * @return {String}
 */
function theme_pager(variables) {
  try {
    var html = '';
    // Extract the view and pager data.
    var view = variables.results.view;
    var page = view.page;
    var pages = view.pages;
    var count = view.count;
    var limit = view.limit;
    var page = view.page;
    // If we don't have any results, return.
    // @TODO putting views_litepager support here is a hack, we should be
    // implementing views_litepager_pager() for theme_pager() instead.
    var views_litepager_present = module_exists('views_litepager');
    if (
      (count == 0 && !views_litepager_present) ||
      (views_litepager_present && variables.results.view.pages == null)
    ) { return html; }
    // Add the pager items to the list.
    var items = [];
    if (page != 0) { items.push(theme('pager_previous', variables)); }
    if (
      (page != pages - 1 && !views_litepager_present) ||
      views_litepager_present
    ) { items.push(theme('pager_next', variables)); }
    if (items.length > 0) {
      // Make sure we have an id to use since we need to dynamically build the
      // navbar container for the pager. If we don't have one, generate a random
      // one.
      var id = 'theme_pager_' + user_password();
      var attrs = {
        id: id,
        'class': 'pager',
        'data-role': 'navbar'
      };
      html += '<div ' + drupalgap_attributes(attrs) + '>' + theme('item_list', {
          items: items
      }) + '</div>' +
      '<script type="text/javascript">' +
        '$("#' + id + '").navbar();' +
      '</script>';
    }
    return html;
  }
  catch (error) { console.log('theme_pager - ' + error); }
}

/**
 * Themes a pager link.
 * @param {Object} variables
 * @param {Object} link_vars
 * @return {String}
 */
function theme_pager_link(variables, link_vars) {
  try {
    if (!link_vars.attributes) { link_vars.attributes = {}; }
    link_vars.attributes.href = '#';
    var attributes = drupalgap_attributes(link_vars.attributes);
    return "<a onclick='" + _theme_pager_link_onclick(variables) + "' " +
      attributes + '>' +
      link_vars.text +
    '</a>';
  }
  catch (error) { console.log('theme_pager_link - ' + error); }
}

/**
 * An internal function used to generate the onclick handler JS for a pager
 * link.
 * @param {Object} variables
 * @return {String}
 */
function _theme_pager_link_onclick(variables) {
  try {
    // Make a copy of variables. While doing so remove any results from it
    // because we don't want them in the onclick handler's html. The results
    // will be available in the _views_embed_view_results variable if they are
    // needed
    var copy = $.extend({ }, { }, variables);
    if (copy.results) { delete copy.results; }
    var onclick = '_theme_pager_link_click(' + JSON.stringify(copy) + ')';
    return onclick;
  }
  catch (error) { console.log('_theme_pager_link_onclick - ' + error); }
}

/**
 * An internal function used to handle clicks on pager links.
 * @param {Object} variables
 */
function _theme_pager_link_click(variables) {
  try {
    _theme_view(variables);
  }
  catch (error) { console.log('_theme_pager_link_click - ' + error); }
}

/**
 * Themes a pager next link.
 * @param {Object} variables
 * @return {String}
 */
function theme_pager_next(variables) {
  try {
    var html;
    variables.page = parseInt(variables.results.view.page) + 1;
    var link_vars = {
      text: '&raquo;',
      attributes: {
        'class': 'pager_next'
      }
    };
    html = theme_pager_link(variables, link_vars);
    return html;
  }
  catch (error) { console.log('theme_pager_next - ' + error); }
}

/**
 * Themes a pager previous link.
 * @param {Object} variables
 * @return {String}
 */
function theme_pager_previous(variables) {
  try {
    var html;
    variables.page = parseInt(variables.results.view.page) - 1;
    var link_vars = {
      text: '&laquo;',
      attributes: {
        'class': 'pager_previous'
      }
    };
    html = theme_pager_link(variables, link_vars);
    return html;
  }
  catch (error) { console.log('theme_pager_previous - ' + error); }
}

/**
 * A helper function used to retrieve the various open and closing tags for
 * views results, depending on their format.
 * @param {Object} variables
 * @return {Object}
 */
function drupalgap_views_get_result_formats(variables) {
  try {
    var result_formats = {};

    // Depending on the format, let's render the container opening and closing,
    // and then render the rows.
    if (!variables.format) { variables.format = 'unformatted_list'; }
    var open = '';
    var close = '';
    var open_row = '';
    var close_row = '';

    // Prepare the format's container attributes.
    var format_attributes = {};
    if (typeof variables.format_attributes !== 'undefined') {
      format_attributes = $.extend(
        true,
        format_attributes,
        variables.format_attributes
      );
    }

    // Add a views-results class
    if (typeof format_attributes['class'] === 'undefined') {
      format_attributes['class'] = '';
    }
    format_attributes['class'] += ' views-results ';

    switch (variables.format) {
      case 'ul':
        if (typeof format_attributes['data-role'] === 'undefined') {
          format_attributes['data-role'] = 'listview';
        }
        open = '<ul ' + drupalgap_attributes(format_attributes) + '>';
        close = '</ul>';
        open_row = '<li>';
        close_row = '</li>';
        break;
      case 'ol':
        if (typeof format_attributes['data-role'] === 'undefined') {
          format_attributes['data-role'] = 'listview';
        }
        open = '<ol ' + drupalgap_attributes(format_attributes) + '>';
        close = '</ol>';
        open_row = '<li>';
        close_row = '</li>';
        break;
      case 'table':
      case 'jqm_table':
        if (variables.format == 'jqm_table') {
          if (typeof format_attributes['data-role'] === 'undefined') {
            format_attributes['data-role'] = 'table';
          }
          if (typeof format_attributes['data-mode'] === 'undefined') {
            format_attributes['data-mode'] = 'reflow';
          }
          console.log(
            'WARNING: theme_views_view() - jqm_table not supported, yet'
          );
        }
        open = '<table ' + drupalgap_attributes(format_attributes) + '>';
        close = '</table>';
        open_row = '<tr>';
        close_row = '</tr>';
        break;
      case 'unformatted_list':
      default:
        if (typeof format_attributes['class'] === 'undefined') {
          format_attributes['class'] = '';
        }
        format_attributes['class'] += ' views-rows';
        open = '<div ' + drupalgap_attributes(format_attributes) + '>';
        close = '</div>';
        open_row = '';
        close_row = '';
        break;
    }
    result_formats.open = open;
    result_formats.close = close;
    result_formats.open_row = open_row;
    result_formats.close_row = close_row;
    return result_formats;
  }
  catch (error) { console.log('drupalgap_views_get_result_formats - ' + error); }
}

/**
 * A helper function used to render a views result's rows.
 * @param {Object}
 * @param {Object}
 * @param {String}
 * @param {String}
 * @param {String}
 * @param {String}
 * @return {String}
 */
function drupalgap_views_render_rows(variables, results, root, child, open_row, close_row) {
  try {
    var html = '';
    for (var count in results[root]) {
      if (!results[root].hasOwnProperty(count)) { continue; }
      var object = results[root][count];
      // Extract the row.
      var row = object[child];
      // Mark the row position.
      row._position = count;
      // If a row_callback function exists, call it to render the row,
      // otherwise use the default row render mechanism.
      var row_content = '';
      if (variables.row_callback && function_exists(variables.row_callback)) {
        row_callback = window[variables.row_callback];
        row_content = row_callback(results.view, row);
      }
      else { row_content = JSON.stringify(row); }
      html += open_row + row_content + close_row;
    }
    return html;
  }
  catch (error) { console.log('drupalgap_views_render_rows - ' + error); }
}

/**
 * @deprecated - Use views_datasource_get_view_result() instead.
 */
drupalgap.views_datasource = {
  'options': { },
  'call': function(options) {
    try {
      var msg = 'WARNING: drupalgap.views_datasource has been deprecated! ' +
      'Use views_datasource_get_view_result() instead.';
      console.log(msg);
      views_datasource_get_view_result(options.path, options);
    }
    catch (error) { console.log('drupalgap.views_datasource - ' + error); }
  }
};
