/*! contact-data-services.js | https://github.com/experiandataquality/GlobalIntuitive | Apache-2.0
*   Experian Data Quality | https://github.com/experiandataquality */

;(function(window, document, undefined) {

    "use strict";

// Create ContactDataServices constructor and namespace on the window object (if not already present)
var ContactDataServices = window.ContactDataServices = window.ContactDataServices || {};

// Default settings
ContactDataServices.defaults = {
	input: { placeholderText: "Start typing an address...", applyFocus: false },
	formattedAddressContainer: { showHeading: false, headingType: "h3", validatedHeadingText: "Validated address", manualHeadingText: "Manual address entered"  },
	searchAgain: { visible: true, text: "Search again"},
	useAddressEnteredText: "<em>Enter your address manually</em>",
	useSpinner: false,
	language: "en",
	addressLineLabels: [
		"addressLine1",
		"addressLine2",
		"addressLine3",
		"locality",
		"province",
		"postalCode",
		"country"
	]
};

// Create configuration by merging custom and default options
ContactDataServices.mergeDefaultOptions = function(customOptions){
	var instance = customOptions || {};

	instance.enabled = true;
	instance.language = instance.language || ContactDataServices.defaults.language;
	instance.useSpinner = instance.useSpinner || ContactDataServices.defaults.useSpinner;
	instance.lastSearchTerm = "";
	instance.currentSearchTerm = "";
	instance.lastCountryCode = "";
	instance.currentCountryCode = "";
	instance.currentSearchUrl = "";
	instance.currentFormatUrl = "";
	instance.applyFocus = (typeof instance.applyFocus !== "undefined") ? instance.applyFocus : ContactDataServices.defaults.input.applyFocus;
	instance.placeholderText = instance.placeholderText || ContactDataServices.defaults.input.placeholderText;
	instance.searchAgain = instance.searchAgain || {};
	instance.searchAgain.visible = (typeof instance.searchAgain.visible !== "undefined") ? instance.searchAgain.visible : ContactDataServices.defaults.searchAgain.visible;
	instance.searchAgain.text = instance.searchAgain.text || ContactDataServices.defaults.searchAgain.text;
	instance.formattedAddressContainer = instance.formattedAddressContainer || ContactDataServices.defaults.formattedAddressContainer;
	instance.formattedAddressContainer.showHeading = (typeof instance.formattedAddressContainer.showHeading !== "undefined") ? instance.formattedAddressContainer.showHeading : ContactDataServices.defaults.formattedAddressContainer.showHeading;
	instance.formattedAddressContainer.headingType = instance.formattedAddressContainer.headingType || ContactDataServices.defaults.formattedAddressContainer.headingType;
	instance.formattedAddressContainer.validatedHeadingText = instance.formattedAddressContainer.validatedHeadingText || ContactDataServices.defaults.formattedAddressContainer.validatedHeadingText;
	instance.formattedAddressContainer.manualHeadingText = instance.formattedAddressContainer.manualHeadingText || ContactDataServices.defaults.formattedAddressContainer.manualHeadingText;
	instance.elements = instance.elements || {};

	return instance;
};

// Constructor method event listener (pub/sub type thing)
ContactDataServices.eventFactory = function(){
	// Create the events object
	var events = {};

	// Create an object to hold the collection of events
	events.collection = {};

	// Subscribe a new event
	events.on = function (event, action) {
        // Create the property array on the collection object
        events.collection[event] = events.collection[event] || [];
        // Push a new action for this event onto the array
        events.collection[event].push(action);
    };

    // Publish (trigger) an event
	events.trigger = function (event, data) {
        // If this event is in our collection (i.e. anyone's subscribed)
        if (events.collection[event]) {
            // Loop over all the actions for this event
            for (var i = 0; i < events.collection[event].length; i++) {
                // Create array with default data as 1st item
                var args = [data];

                // Loop over additional args and add to array
                for (var a = 2; a < arguments.length; a++){
                    args.push(arguments[a]);
                }

                // Call each action for this event type, passing the args
				try {
					events.collection[event][i].apply(events.collection, args);
				} catch (e) {
					// What to do? Uncomment the below to show errors in your event actions
					//console.error(e);
				}
            }
        }
    };

    // Return the new events object to be used by whoever invokes this factory
    return events;
};

// Translations
ContactDataServices.translations = {
// language / country / property
  en: {
    gbr: {
      locality: "Town/City",
      province: "County",
      postalCode: "Post code"
    },
    usa: {
      locality: "City",
      province: "State",
      postalCode: "Zip code"
    }
  }
  // Add other languages below
};

// Method to handle showing of UA (User Assistance) content
ContactDataServices.ua = {
	banner: {
        show: function(html){
            // Retrieve the existing banner
            var banner = document.querySelector(".ua-banner");
            
            // Create a new banner if necessary
            if(!banner){
                var firstChildElement = document.querySelector("body").firstChild;
                banner = document.createElement("div");
                banner.classList.add("ua-banner");            
                firstChildElement.parentNode.insertBefore(banner, firstChildElement.nextSibling);
            }

            // Apply the HTML content
            banner.innerHTML = html;
        },
        hide: function(){
            var banner = document.querySelector(".ua-banner");
            if(banner) {
                banner.parentNode.removeChild(banner);
            }
        }
    }
};

// Generate the URLs for the various requests
ContactDataServices.urls = {
  endpoint: "https://api.edq.com/capture/address/v2/search",
  construct: {
    address: {
      // Construct the Search URL by appending query, country & take
      search: function(instance){
        var url = ContactDataServices.urls.endpoint;
        url += "?query=" + encodeURIComponent(instance.currentSearchTerm);
        url += "&country=" + instance.currentCountryCode;
        url += "&dataset=" + instance.currentDataSet;

        if (instance.elements.location) {
          url += "&location=" + instance.elements.location;
        }

        url += "&take=" + (instance.maxSize || instance.picklist.maxSize);
        url += "&auth-token=" + instance.token;
        return url;
      },
      // Append the token to the Format URL
      format: function(url, instance){
        return url + "&auth-token=" + instance.token;
      }
    }
  },
  // Get token from query string and set on instance
  getToken: function(instance){
    if(!instance.token) {
      instance.token = ContactDataServices.urls.getParameter("token");
    }
  },
  getParameter: function(name) {
      name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
      var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
          results = regex.exec(location.search);
      return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
  }
};
// Integrate with address searching
ContactDataServices.address = function(customOptions){
  // Build our new instance by merging custom and default options
  var instance = ContactDataServices.mergeDefaultOptions(customOptions);

  // Create a new object to hold the events from the event factory
  instance.events = new ContactDataServices.eventFactory();

  // Initialise this instance
  instance.init = function(){
    // Get token from the query string
    ContactDataServices.urls.getToken(instance);
    if(!instance.token){
      // Disable searching on this instance
      instance.enabled = false;
      // Display a banner informing the user that they need a token
      ContactDataServices.ua.banner.show("<a href='https://github.com/experiandataquality/GlobalIntuitive#tokens'>Please provide a token for GlobalIntuitive.</a>");
      return;
    }

    instance.hasSearchInputBeenReset = true;
    instance.setCountryList();

    if(instance.elements.input){
      instance.input = instance.elements.input;
      instance.countryCodeMapping = instance.elements.countryCodeMapping || {};

      // Bind an event listener on the input
      instance.input.addEventListener("keyup", instance.search);
      instance.input.addEventListener("keydown", instance.checkTab);
      // Set a placeholder for the input
      instance.input.setAttribute("placeholder", instance.placeholderText);
      // Disable autocomplete on the form
      instance.input.parentNode.setAttribute("autocomplete", "off");
      // Disable form submission for demo purposes
      instance.input.parentNode.addEventListener('submit', function(event){
          event.preventDefault();
      });
      // Apply focus to input
      if(instance.applyFocus){
          instance.input.focus();
      }
    }
  };

  instance.unbind = function() {

    if (instance.elements.input) {
      instance.input = instance.elements.input;
      // Unbind previously bound listeners.
      instance.input.removeEventListener("keyup", instance.search);
      instance.input.removeEventListener("keydown", instance.checkTab);
      instance.input.parentNode.removeAttribute("autocomplete");
    }

  };
  // Main function to search for an address from an input string
  instance.search = function(event){
    // Handle keyboard navigation
    var e = event || window.event;
        e = e.which || e.keyCode;
        if (e === 38/*Up*/ || e === 40/*Down*/ || e === 13/*Enter*/) {
            instance.picklist.keyup(e);
            return;
        }

    instance.currentSearchTerm = instance.input.value;

    // Grab the country ISO code and (if it is present) the dataset name from the current value of the countryList (format: {countryIsoCode};{dataset})
    var currentCountryInfo = instance.countryCodeMapping[instance.countryList.value] || instance.countryList.value;
    currentCountryInfo = currentCountryInfo.split(";");

    instance.currentCountryCode = currentCountryInfo[0];
    instance.currentDataSet = currentCountryInfo[1] || "";


    // (Re-)set the property stating whether the search input has been reset.
    // This is needed for instances when the search input is also an address
    // output field. After an address has been returned, you don't want a new 
    // search being triggered until the field has been cleared.
    if (instance.currentSearchTerm === "") {
            instance.hasSearchInputBeenReset = true;
        }
    
    // Check is searching is permitted
    if(instance.canSearch()){
      // Abort any outstanding requests
      if(instance.request.currentRequest){
        instance.request.currentRequest.abort();
      }

      // Fire an event before a search takes place
      instance.events.trigger("pre-search", instance.currentSearchTerm);

      // Construct the new Search URL
      var url = ContactDataServices.urls.construct.address.search(instance);

      // Store the last search term
      instance.lastSearchTerm = instance.currentSearchTerm;

      // Hide any previous results
      instance.result.hide();

      // Hide the inline search spinner
      instance.searchSpinner.hide();

      // Show an inline spinner whilst searching
      instance.searchSpinner.show();

      // Initiate new Search request
      instance.request.get(url, instance.picklist.show);
    } else if(instance.lastSearchTerm !== instance.currentSearchTerm){
      // Clear the picklist if the search term is cleared/empty
      instance.picklist.hide();
    }
  };

  instance.setCountryList = function(){
    instance.countryList = instance.elements.countryList;

    // If the user hasn't passed us a country list, then create new list?
    if(!instance.countryList){
      instance.createCountryDropdown();
    }
  };

  // Determine whether searching is currently permitted
  instance.canSearch = function(){
        // If searching on this instance is enabled, and
    return (instance.enabled &&
        // If search term is not empty, and
        instance.currentSearchTerm !== "" &&
        // If search term is not the same as previous search term, and
        instance.lastSearchTerm !== instance.currentSearchTerm &&
        // If the country is not empty, and
        instance.countryList.value !== undefined && instance.countryList.value !== "" && 
        // If search input has been reset (if applicable)
                instance.hasSearchInputBeenReset === true);
  };

  //Determine whether tab key was pressed
  instance.checkTab = function(event) {
    var e = event || window.event;
        e = e.which || e.keyCode;
        if (e === 9 /*Tab*/) {
            instance.picklist.keyup(e);
            return;
        }
  };

  instance.createCountryDropdown = function(){
    // What countries?
    // Where to position it?
    instance.countryList = {};
  };

  // Get a final (Formatted) address
  instance.format = function(url){
    // Trigger an event
    instance.events.trigger("pre-formatting-search", url);

    // Hide the searching spinner
    instance.searchSpinner.hide();

    // Construct the format URL (append the token)
    instance.currentFormatUrl = ContactDataServices.urls.construct.address.format(url, instance);

    // Initiate a new Format request
    instance.request.get(instance.currentFormatUrl, instance.result.show);
  };

  instance.picklist = {
    // Set initial size
    size: 0,
    // Set initial max size
    maxSize: 25,
    // Render a picklist of search results
    show: function(items){
      // Store the picklist items
      instance.picklist.items = items.results;

      // Reset any previously selected current item
      instance.picklist.currentItem = null;

      // Update picklist size
      instance.picklist.size = instance.picklist.items.length;

      // Get/Create picklist container element
      instance.picklist.list = instance.picklist.list || instance.picklist.createList();

      // Ensure previous results are cleared
      instance.picklist.list.innerHTML = "";

      // Reset the picklist tab count (used for keyboard navigation)
      instance.picklist.resetTabCount();

      // Hide the inline search spinner
      instance.searchSpinner.hide();

      // Prepend an option for "use address entered"
      instance.picklist.useAddressEntered.element = instance.picklist.useAddressEntered.element || instance.picklist.useAddressEntered.create();

      if(instance.picklist.size > 0){
        // Fire an event before picklist is created
        instance.events.trigger("pre-picklist-create", instance.picklist.items);

        // Iterate over and show results
        instance.picklist.items.forEach(function(item){
          // Create a new item/row in the picklist
          var listItem = instance.picklist.createListItem(item);
          instance.picklist.list.appendChild(listItem);

          // Listen for selection on this item
          instance.picklist.listen(listItem);
        });

        // Fire an event after picklist is created
        instance.events.trigger("post-picklist-create");
      } else {
    	  var noResults = '<div class="no-results"><button class="reset-no-results-found" type="button">No matches found. Please try again</button></div>';
    	  instance.picklist.list.classList.add("no-results-list");
    	  instance.picklist.list.innerHTML = noResults;
    	  instance.events.trigger("post-picklist-create");
      }
    },
    // Remove the picklist
    hide: function(){
      // Clear the current picklist item
      instance.picklist.currentItem = null;
      // Remove the "use address entered" option too
      instance.picklist.useAddressEntered.destroy();
      // Remove the main picklist container
      if(instance.picklist.list){
        instance.input.parentNode.removeChild(instance.picklist.container);
        instance.picklist.list = undefined;
      }
    },
    useAddressEntered: {
      // Create a "use address entered" option
      create: function(){
        var item = {
          suggestion: ContactDataServices.defaults.useAddressEnteredText,
          format: ""
        };
        var listItem = instance.picklist.createListItem(item);
        listItem.classList.add("use-address-entered");
        instance.picklist.list.parentNode.insertBefore(listItem, instance.picklist.list.nextSibling);
        listItem.addEventListener("click", instance.picklist.useAddressEntered.click);
        return listItem;
      },
      // Destroy the "use address entered" option
      destroy: function(){
        if(instance.picklist.useAddressEntered.element){
          instance.picklist.list.parentNode.removeChild(instance.picklist.useAddressEntered.element);
          instance.picklist.useAddressEntered.element = undefined;
        }
      },
      // Use the address entered as the Formatted address
      click: function(){
        var inputData = {
          address: []
        };

        if(instance.currentSearchTerm){
          // Try and split into lines by using comma delimiter
          var lines = instance.currentSearchTerm.split(",");
          if(lines.length > 0){
            for(var i = 0; i < lines.length; i++){
              inputData.address.push(instance.picklist.useAddressEntered.formatManualAddressLine(lines, i));
            }
          }

          // Pad with additional blank fields if needed
          var maxLines = 7;
          var additionalLinesNeeded = maxLines - lines.length;
          if(additionalLinesNeeded > 0){
            var counterStart = maxLines - additionalLinesNeeded;
            for(var j = counterStart; j < maxLines; j++){
              inputData.address.push(instance.picklist.useAddressEntered.formatManualAddressLine([], j));
            }
          }
        }

        instance.result.show(inputData);
        instance.result.updateHeading(instance.formattedAddressContainer.manualHeadingText);
      },
      // Create and return an address line object with the key as the label
      formatManualAddressLine: function(lines, i){
        var key = ContactDataServices.defaults.addressLineLabels[i];
        var lineObject = {};
        lineObject[key] = lines[i] || "";
        return lineObject;
      }
    },
    // Create the picklist list (and container) and inject after the input
    createList: function(){
      var container = document.createElement("div");
      container.classList.add("address-picklist-container");
      // Insert the picklist container after the input
      instance.input.parentNode.insertBefore(container, instance.input.nextSibling);
      instance.picklist.container = container;

      var list = document.createElement("div");
      list.classList.add("address-picklist");
      // Append the picklist to the container
      container.appendChild(list);

      list.addEventListener("keydown", instance.picklist.enter);
      return list;
    },
    // Create a new picklist item/row
    createListItem: function(item){
      var row = document.createElement("div");
      row.innerHTML = instance.picklist.addMatchingEmphasis(item);
      // Store the Format URL
      row.setAttribute("format", item.format);
      return row;
    },
    // Tab count used for keyboard navigation
    tabCount: -1,
    resetTabCount: function () {
          instance.picklist.tabCount = -1;
      },
    // Keyboard navigation
    keyup: function(e){
      if(!instance.picklist.list){
              return;
            }

            if (e === 13/*Enter*/ || e === 9 /*Tab*/) {
                instance.picklist.checkEnter();
                return;
            }

            // Get a list of all the addresses in the picklist
            var addresses = instance.picklist.list.querySelectorAll("div"),
                    firstAddress, lastAddress;

            // If the picklist is empty, just return
            if(addresses.length === 0){
                return;
            }

            // Set the tabCount based on previous and direction
            if (e === 38/*Up*/) {
                instance.picklist.tabCount--;
            }
            else {
                instance.picklist.tabCount++;
            }

            // Set top and bottom positions and enable wrap-around
            if (instance.picklist.tabCount < 0) {
                instance.picklist.tabCount = addresses.length - 1;
                lastAddress = true;
            }
            if (instance.picklist.tabCount > addresses.length - 1) {
                instance.picklist.tabCount = 0;
                firstAddress = true;
            }

            // Highlight the selected address
          var currentlyHighlighted = addresses[instance.picklist.tabCount];
          // Remove any previously highlighted ones
          var previouslyHighlighted = instance.picklist.list.querySelector(".selected");
          if(previouslyHighlighted){
            previouslyHighlighted.classList.remove("selected");
          }
          currentlyHighlighted.classList.add("selected");
          // Set the currentItem on the picklist to the currently highlighted address
      instance.picklist.currentItem = currentlyHighlighted;

          // Scroll address into view, if required
            var addressListCoords = {
                top: instance.picklist.list.offsetTop,
                bottom: instance.picklist.list.offsetTop + instance.picklist.list.offsetHeight,
                scrollTop: instance.picklist.list.scrollTop,
                selectedTop: currentlyHighlighted.offsetTop,
                selectedBottom: currentlyHighlighted.offsetTop + currentlyHighlighted.offsetHeight,
                scrollAmount: currentlyHighlighted.offsetHeight
            };
            if (firstAddress) {
                instance.picklist.list.scrollTop = 0;
            }
            else if (lastAddress) {
                instance.picklist.list.scrollTop = 999;
            }
            else if (addressListCoords.selectedBottom + addressListCoords.scrollAmount > addressListCoords.bottom) {
                instance.picklist.list.scrollTop = addressListCoords.scrollTop + addressListCoords.scrollAmount;
            }
            else if (addressListCoords.selectedTop - addressListCoords.scrollAmount - addressListCoords.top < addressListCoords.scrollTop) {
                instance.picklist.list.scrollTop = addressListCoords.scrollTop - addressListCoords.scrollAmount;
            }
    },
    // Add emphasis to the picklist items highlighting the match
    addMatchingEmphasis: function(item){
      var highlights = item.matched || [],
              label = item.suggestion;

            for (var i = 0; i < highlights.length; i++) {
                var replacement = '<b>' + label.substring(highlights[i][0], highlights[i][1]) + '</b>';
                label = label.substring(0, highlights[i][0]) + replacement + label.substring(highlights[i][1]);
            }

            return label;
    },
    // Listen to a picklist selection
    listen: function(row){
      row.addEventListener("click", instance.picklist.pick.bind(null, row));
    },
    checkEnter: function(){
      var picklistItem;
      // If picklist contains 1 address then use this one to format
      if(instance.picklist.size === 1){
        picklistItem = instance.picklist.list.querySelectorAll("div")[0];
      } // Else use the currently highlighted one when navigation using keyboard
      else if(instance.picklist.currentItem){
        picklistItem = instance.picklist.currentItem;
      }
      if(picklistItem){
        instance.picklist.pick(picklistItem);
      }
    },
    // How to handle a picklist selection
    pick: function(item){
      // Fire an event when an address is picked
      instance.events.trigger("post-picklist-selection", item);

      // Get a final address using picklist item
      instance.format(item.getAttribute("format"));
    }
  };

  instance.result = {
    // Render a Formatted address
    show: function(data){
      // Hide the inline search spinner
      instance.searchSpinner.hide();

      // Hide the picklist
      instance.picklist.hide();

      // Clear search input
      instance.input.value = "";
      
      if(data.address && data.address.length > 0){				

        // Create an array to hold the hidden input fields
        var inputArray = [];

        // Calculate if we needed to generate the formatted address input fields later
        instance.result.calculateIfAddressLineGenerationRequired();

        // Get formatted address container element
        // Only create a container if we're creating inputs. otherwise the user will have their own container.
        instance.result.formattedAddressContainer = instance.elements.formattedAddressContainer;
        if(!instance.result.formattedAddressContainer && instance.result.generateAddressLineRequired) {
          instance.result.createFormattedAddressContainer();
        }

        // Loop over each formatted address component
        for(var i = 0; i < data.address.length; i++){
            var addressComponent = data.address[i];
            // The addressComponent object will only have one property, but we don't know the key
            for (var key in addressComponent) {
                if (addressComponent.hasOwnProperty(key)) {
              // Bind the address element to the user's address field (or create a new one)
              instance.result.updateAddressLine(key, addressComponent, "address-line-input");
            }
            }
        }

        // Hide country and address search fields (if they have a 'toggle' class)
        instance.result.hideSearchInputs();

        // Write the 'Search again' link and insert into DOM
        instance.result.createSearchAgainLink();
        
        // If an address line is also the search input, set property to false.
        // This ensures that typing in the field again (after an address has been
        // returned) will not trigger a new search.
        for(var element in instance.elements){
          if (instance.elements.hasOwnProperty(element)) {
            // Excluding the input itself, does another element match the input field?
            if (element !== "input" && instance.elements[element] === instance.elements.input) {
              instance.hasSearchInputBeenReset = false;
              break;
            }
          }
        }
      }

      // Fire an event to say we've got the formatted address
      instance.events.trigger("post-formatting-search", data);
    },
    hide: function(){
      // Delete the formatted address container
      if(instance.result.formattedAddressContainer){
        instance.input.parentNode.removeChild(instance.result.formattedAddressContainer);
        instance.result.formattedAddressContainer = undefined;
      }
      // Delete the search again link
      if(instance.searchAgain.link){
        instance.searchAgain.link.parentNode.removeChild(instance.searchAgain.link);
        instance.searchAgain.link = undefined;
      }
      // Remove previous value from user's result field
      // Loop over their elements
      for(var element in instance.elements){
        if (instance.elements.hasOwnProperty(element)) {
          // If it matches an "address" element
          for(var i = 0; i < ContactDataServices.defaults.addressLineLabels.length; i++){
            var label = ContactDataServices.defaults.addressLineLabels[i];
            // Only reset the value if it's not an input field
            if(label === element && instance.elements[element] !== instance.elements.input) {
              instance.elements[element].value = "";
              break;
            }
          }
        }
      }
    },
    // Create the formatted address container and inject after the input
    createFormattedAddressContainer: function(){
      var container = document.createElement("div");
      container.classList.add("formatted-address");

      // Insert the container after the input
      instance.input.parentNode.insertBefore(container, instance.input.nextSibling);
      instance.result.formattedAddressContainer = container;
    },
    // Create a heading for the formatted address container
    createHeading: function(){
      // Create a heading for the formatted address
      if(instance.formattedAddressContainer.showHeading){
        var heading = document.createElement(instance.formattedAddressContainer.headingType);
        heading.innerHTML = instance.formattedAddressContainer.validatedHeadingText;
        instance.result.formattedAddressContainer.appendChild(heading);
      }
    },
    // Update the heading text in the formatted address container
    updateHeading: function(text){
      //Change the heading text to "Manual address entered"
      if(instance.formattedAddressContainer.showHeading){
        var heading = instance.result.formattedAddressContainer.querySelector(instance.formattedAddressContainer.headingType);
        heading.innerHTML = text;
      }
    },
    calculateIfAddressLineGenerationRequired: function(){
      instance.result.generateAddressLineRequired = true;
      for(var i = 0; i < ContactDataServices.defaults.addressLineLabels.length; i++){
        var key = ContactDataServices.defaults.addressLineLabels[i];
        if(instance.elements[key]){
          instance.result.generateAddressLineRequired = false;
          break;
        }
      }
    },
    updateAddressLine: function(key, addressLineObject, className){
      // Either append the result to the user's address field or create a new field for them
      if (instance.elements[key]){
        var addressField = instance.elements[key];
        var value = addressLineObject[key];
        // If a value is already present, prepend a comma and space
        if(addressField.value && value) {
          value = ", " + value;
        }
        // Decide what property of the node we need to update. i.e. if it's not a form field, update the innerText.
        if (addressField.nodeName === "INPUT" || addressField.nodeName === "TEXTAREA" || addressField.nodeName === "SELECT") {
          addressField.value += value;
        } else {
          addressField.innerText += value;
        }
        // Store a record of their last address field
        instance.result.lastAddressField = addressField;
      } else if (instance.result.generateAddressLineRequired){
        // Create an input to store the address line
        var label = instance.result.createAddressLine.label(key);
        var field = instance.result.createAddressLine.input(label, addressLineObject[key], className);
        // Insert into DOM
        instance.result.formattedAddressContainer.appendChild(field);
      }
    },
    createAddressLine: {
      // Create an input to store the address line
      input: function(key, value, className){
        // Create a wrapper
        var div  = document.createElement("div");
        div.classList.add(className);

        // Create the label
        var label = document.createElement("label");
         label.innerHTML = key.replace(/([A-Z])/g, ' $1') // Add space before capital Letters
                                      .replace(/([0-9])/g, ' $1') // Add space before numbers
                                      .replace(/^./, function (str) { return str.toUpperCase(); }); // Make first letter of word a capital letter
        div.appendChild(label);

        // Create the input
        var input = document.createElement("input");
        input.setAttribute("type", "text");
        input.setAttribute("name", key);
        input.setAttribute("value", value);
        div.appendChild(input);
        return div;
      },
      // Create the address line label based on the country and language
      label: function(key){
        var label = key;
        var language = instance.language.toLowerCase();
        var country = instance.currentCountryCode.toLowerCase();
        var translations = ContactDataServices.translations;
        if(translations){
          try {
            var translatedLabel = translations[language][country][key];
            if(translatedLabel){
              label = translatedLabel;
            }
          } catch(e) {
            // Translation doesn't exist for key
          }
        }
        return label;
      }
    },
    // Create the 'Search again' link that resets the search
    createSearchAgainLink: function(){
      if(instance.searchAgain.visible){
        var link = document.createElement("a");
        link.setAttribute("href", "#");
        link.classList.add("search-again-link");
        link.innerHTML = instance.searchAgain.text;
        // Bind event listener
        link.addEventListener("click", instance.reset);
        // Store a reference to the link element
        instance.searchAgain.link = link;

        // Insert into the formatted address container
        if(instance.result.formattedAddressContainer) {
          instance.result.formattedAddressContainer.appendChild(link);
        } else {
          // Insert after last address field
          instance.result.lastAddressField.parentNode.insertBefore(link, instance.result.lastAddressField.nextSibling);
        }
      }
    },
    // Write the list of hidden address line inputs to the DOM
    renderInputList: function(inputArray){
      if(inputArray.length > 0){
        for(var i = 0; i < inputArray.length; i++){
          instance.result.formattedAddressContainer.appendChild(inputArray[i]);
        }
      }
    },
    // Hide the initial country and address search inputs
    hideSearchInputs: function(){
      instance.toggleVisibility(instance.input.parentNode);
    }
  };

  // Toggle the visibility of elements
  instance.toggleVisibility = function(scope){
    scope = scope || document;
    var elements = scope.querySelectorAll(".toggle");
    for (var i = 0; i < elements.length; i++) {
      if(elements[i].classList.contains("hidden")){
        elements[i].classList.remove("hidden");
      } else {
        elements[i].classList.add("hidden");
      }
    }
  };

  instance.searchSpinner = {
    show: function(){
      // Return if we're not displaying a spinner
      if(!instance.useSpinner){
        return;
      }
      // Create the spinner container
        var spinnerContainer = document.createElement("div");
        spinnerContainer.classList.add("loader");
        spinnerContainer.classList.add("loader-inline");

        // Create the spinner
      var spinner = document.createElement("div");
      spinner.classList.add("spinner");
      spinnerContainer.appendChild(spinner);

      // Insert the spinner after the field
      instance.input.parentNode.insertBefore(spinnerContainer, instance.input.nextSibling);
    },

    hide: function(){
      // Return if we're not displaying a spinner
      if(!instance.useSpinner){
        return;
      }
      var spinner = instance.input.parentNode.querySelector(".loader-inline");
      if(spinner){
        instance.input.parentNode.removeChild(spinner);
      }
    }
  };

  // Reset the search
  instance.reset = function(event){
    if(event){
      event.preventDefault();
    }
    // Enable searching
    instance.enabled = true;
    // Hide formatted address
    instance.result.hide();
    // Clear the input field
    instance.input.value = "";
    // Reset search input back
    instance.hasSearchInputBeenReset = true;
    // Show search input
    instance.toggleVisibility(instance.input.parentNode);
    // Apply focus to input
    instance.input.focus();

    // Fire an event after a reset
    instance.events.trigger("post-reset");
  };

  // How to handle request errors
  instance.handleError = {
    // How to handle 400 Bad Request
    badRequest: function(xhr){
      instance.enabled = false;

      // As searching is disabled, show button to render final address instead
      instance.handleError.showSubmitButton();

      // Fire an event to notify users of the error
      instance.events.trigger("request-error-400", xhr);
    },

    // How to handle 401 Unauthorized (invalid token?) requests
    unauthorized: function(xhr){
      instance.enabled = false;

      // As searching is disabled, show button to render final address instead
      instance.handleError.showSubmitButton();

      // Fire an event to notify users of the error
      instance.events.trigger("request-error-401", xhr);
    },

    // How to handle 403 Forbidden requests
    forbidden: function(xhr){
      instance.enabled = false;

      // As searching is disabled, show button to render final address instead
      instance.handleError.showSubmitButton();

      // Fire an event to notify users of the error
      instance.events.trigger("request-error-403", xhr);
    },

    // How to handle 404 Not Found requests
    notFound: function(xhr){
      instance.enabled = false;

      // As searching is disabled, show button to render final address instead
      instance.handleError.showSubmitButton();

      // Fire an event to notify users of the error
      instance.events.trigger("request-error-404", xhr);
    },

    // As searching is disabled, show button to render final address instead
    showSubmitButton: function(){
      var button = document.createElement("button");
      button.innerText = "Submit";
      instance.input.parentNode.insertBefore(button, instance.input.nextSibling);
      button.addEventListener("click", function(){
        // Simulate a manual "use address entered" entry
        instance.picklist.useAddressEntered.click();
        // Remove the button
        instance.input.parentNode.removeChild(button);
      });
    }
  };

  // Use this to initiate and track XMLHttpRequests
  instance.request = {
    currentRequest: null,
    get: function(url, callback){
      instance.request.currentRequest = new XMLHttpRequest();
      instance.request.currentRequest.open('GET', url, true);
      instance.request.currentRequest.timeout = 5000; // 5 seconds

      instance.request.currentRequest.onload = function(xhr) {
        if (instance.request.currentRequest.status >= 200 && instance.request.currentRequest.status < 400) {
          // Success!
          var data = JSON.parse(instance.request.currentRequest.responseText);
          instance.request.latestResult = data;
          callback(data);
        } else {
          instance.request.latestResult = {};

          // We reached our target server, but it returned an error
          instance.searchSpinner.hide();

          // Fire an event to notify users of an error
          instance.events.trigger("request-error", xhr);

          // If the request is 400 Bad Request
          if (instance.request.currentRequest.status === 400){
            instance.handleError.badRequest(xhr);
          }
          // If the request is 401 Unauthorized (invalid token) we should probably disable future requests
          else if (instance.request.currentRequest.status === 401){
            instance.handleError.unauthorized(xhr);
          }
          // If the request is 403 Forbidden
          else if (instance.request.currentRequest.status === 403){
            instance.handleError.forbidden(xhr);
          }
          // If the request is 404 Not Found
          else if (instance.request.currentRequest.status === 404){
            instance.handleError.notFound(xhr);
          }
        }
      };

      instance.request.currentRequest.onerror = function(xhr) {
        // There was a connection error of some sort
        // Hide the inline search spinner
        instance.searchSpinner.hide();

        // Fire an event to notify users of an error
        instance.events.trigger("request-error", xhr);
      };

      instance.request.currentRequest.ontimeout = function(xhr) {
        // There was a connection timeout
        // Hide the inline search spinner
        instance.searchSpinner.hide();

        // Fire an event to notify users of the timeout
        instance.events.trigger("request-timeout", xhr);
      };

      instance.request.currentRequest.send();
    }
  };

  // Initialise this instance of ContactDataServices
  instance.init();

  // Return the instance object to the invoker
  return instance;
};
})(window, window.document);