const crypto = require("crypto");
var XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest;
const chalk = require("chalk");
const requesty = require('request');
const mime = require('mime-types');
const fs = require('fs')

/**
Class containing the static methods used by Darwin
*/
export default class Darwin {

  /*
  * Returns the current Access Token.
  */
  static getAccessToken() {
    return Darwin.getCookie('darwin-access')
  }

  /*
  * Returns the current CSRF Token.
  */
  static getCSRFToken() {
    return Darwin.getCookie('darwin-csrf')
  }

  /**
   * Determines if the user is authenticated by checking the csrf cookie.
   * Also, checks to see if the csrf token is set in the url and if so transfers
   * that parameter to a cookie for storage.
   * If the user isn't authenticated, then we set the url of the page to the passed in authURL.
   * As an example, the authURL could be:
   * "https://darwin.app.darwincloud.com/darwin/v1-4/authorize/3-Website-2?redirectURL=https://darwin.app.darwincloud.com/darwin/v1-4/authorized"
   */
  static checkAuthentication(authURL, authType, tradeURL, clientID, redirectURL, callback) {
    //1) See if there is an auth code and state in the url params.
    let urlParams = Darwin.getAllUrlParams();
    if ('error' in urlParams) {
      //There was an error in the authentication process.
      console.log(urlParams['error'])
    } else if ('state' in urlParams && 'code' in urlParams) {
      //1.11) Make sure the state matches.
      let requestState = Darwin.getCookie("darwin-state")
      Darwin.deleteCookie("darwin-state");
      if (urlParams["state"] === requestState) {
        //1.12) Get the PKCE Verification.
        let authCode = urlParams["code"]
        let requestPKCE = Darwin.getCookie("darwin-pkce")
        Darwin.deleteCookie("darwin-pkce");
        //1.13) Send the request to /access_token to trade for access_tokens.
        Darwin.callUnsecuredAPI("POST", tradeURL, {
          "grant_type": "authorization_code",
          "client_id": clientID,
          "redirect_uri": redirectURL,
          "code_verifier": requestPKCE,
          "code": authCode
        }, (result) => {
          //Check the tokens and make sure they are set.
          if (result === undefined || result["csrf_token"] === undefined || result["access_token"] === undefined || result["refresh_token"] === undefined) {
            console.log("Error Trading Auth Code For Tokens:", result)
            Darwin.redirectToAuthURL(authURL)
            return
          }
          Darwin.setCookie("darwin-csrf", result["csrf_token"], result["expires_in"]);
          Darwin.setCookie("darwin-access", result["access_token"], result["expires_in"]);
          Darwin.setCookie("darwin-refresh", result["refresh_token"], result["expires_in"]);
          //If not, then we need to make sure that they are accurate.
          callback(true, "traded")
        })
        return true;
      } else {
        console.log("States don't match")
        Darwin.redirectToAuthURL(authURL)
      }
    }
    if (Darwin.getCookie('darwin-csrf') !== "") {
      //2) The tokens have been set so we can continue to use the app and make API requests.
      callback(true)
      return true;
    }
    //The user is not logged in.
    Darwin.logout(authURL, authType)
    return false;
  }

  /**
   * If authType is 'password', then just redirects to the authURL.
   * Else:
   * Takes the passed in url, adds state and PKCE query parameters,
   * stores the state and PKCE query parameters, and then redirects
   * the browser to the auth url for logging in.
   */
  static redirectToAuthURL(authURL, authType) {
    if (authType === "password") {
      //Redirect the browser to the url for logging in.
      window.location.href = authURL
      return
    }

    //1) Create the state parameter, store it, and add it to the authURL.
    let state = Darwin.randomString(16)
    Darwin.setCookie("darwin-state", state, 300)
    authURL = authURL + "&state=" + state
    //2) Create the PKCE parameter, store it, and add it to the authURL.
    let pkce = Darwin.randomString(82)
    Darwin.setCookie("darwin-pkce", pkce, 300)
    let code_challenge = Darwin.sha256PKCE(pkce)
    authURL = authURL + "&code_challenge=" + encodeURIComponent(code_challenge)
    //3) Redirect the browser to the url for logging in.
    window.location.href = authURL
  }

  /**
   * Call this to log the user out of the website.
   * This will delete the csrf token.
   * Then, the api will be called to delete it's cookies.
   * Then, the user will be redirected to the login page.
   */
  static logout(authURL, authType, redirect = true) {
    //1) Delete the cookies.
    Darwin.deleteCookie("darwin-csrf")
    Darwin.deleteCookie("darwin-access")
    Darwin.deleteCookie("darwin-refresh")
    //2) Redirect to the login endpoint.
    if (redirect) {
      Darwin.redirectToAuthURL(authURL, authType)
    }
  }

  /**
   * Generates a random alphanumeric string and returns it.
   */
  static randomString(length) {
    let allowedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
		let allowedCharsCount = allowedChars.length
		let randomString = ""

    for (let i = 0; i < length; i += 1) {
      let randomNum = Math.floor(Math.random() * Math.floor(allowedCharsCount))
      let newCharacter = allowedChars[randomNum]
      randomString = randomString + newCharacter
    }
		return randomString
  }

  /**
   * sha256 hashes the provided string and returns the result after performing
   * some string manipulation to put the hash in a usable format for PKCE.
   */
  static sha256PKCE(str) {
    //1) Hash the string.
    let hash = crypto.createHash("sha256").update(str, "binary").digest("base64");
    //2) Format for a PKCE.
    hash = hash.trim().replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_")
    return hash
  }

  static getXMLHttpRequest() {
    /*var xmlhttp;
    if (window.XMLHttpRequest){// code for IE7+, Firefox, Chrome, Opera, Safari
      xmlhttp=new XMLHttpRequest();
    }*/
    var xmlhttp = new XMLHttpRequest();
    return xmlhttp
  }

  /**
   * Call this to asynchronously call an unsecured API. Pass in the API url to call along with the data.
   * If this is a GET request then the data will be appended to the apiURL.
   */
  static callUnsecuredAPI(method, apiURL, data, callback) {
    var xmlhttp = Darwin.getXMLHttpRequest()
    xmlhttp.onreadystatechange = function() {
      if (xmlhttp.readyState === 4) {
        //if there is an authentication error we should try to refresh the token
        //if that works then we need to call this apiURL again, otherwise logout the user
        if ((xmlhttp.status >= 400 && xmlhttp.status <= 499)) {
          if (xmlhttp.status === 404) {
            //404 Not Found Error. The API Method doesn't exist or the wrong HTTP Method (GET, POST, PUT, ...) was used.
            console.log(xmlhttp.status);
            console.log(xmlhttp.responseText);
          } else {
            //print out the error if the callback is undefined or call the callback
            if (callback === undefined) {
              //just print out the response
              console.log(xmlhttp.status);
              console.log(xmlhttp.responseText);
            } else {
              //call the function
              let returnData = xmlhttp.responseText
              try {
                returnData = JSON.parse(returnData)
              } catch(e) {
                console.log("Couldn't parse response with exception: ", e)
              }
              //print out the error
              console.log("Error on API ", method, apiURL, returnData)
              //call the function
              callback(returnData);
            }
          }
        } else {
          if (callback === undefined) {
            //just print out the response
            console.log(xmlhttp.status);
            console.log(xmlhttp.responseText);
          } else {
            //first parse the response if possible
            let returnData = xmlhttp.responseText
            try {
              returnData = JSON.parse(returnData)
            } catch(e) {
              console.log("Couldn't parse response with exception: ", e)
              console.log("ERROR: ", xmlhttp.status, xmlhttp.responseText, xmlhttp)
            }
            //call the function
            callback(returnData);
          }
        }
      }
    }
    if (method === "GET") {
      xmlhttp.open(method, apiURL + Darwin.objectToGETURL(data), true);
    } else {
      xmlhttp.open(method, apiURL, true);
    }
    xmlhttp.withCredentials = true;
    xmlhttp.setRequestHeader("content-type", "application/x-www-form-urlencoded");
    if (method === "POST" || method === "PUT" || method === "DELETE") {
      xmlhttp.send(Darwin.objectToGETURL(data, true));
    } else if (method === "GET") {
      xmlhttp.send();//the data must be set in the apiURL for GET functions
    }
  }

  /*
   * Uploads a file to S3 with a presigned url.
   */
  static uploadFileToS3(formAttributes, formInputs, key, file, callback, progress = false, cacheSeconds = 0) {

  	let body = formInputs
  	body.key = key

  	//determine the mime type
  	let m = mime.lookup(file)
  	if (m == false) {
  		m = "binary/octet-stream"
  	}
  	body["Content-Type"] = m
  	body["Cache-Control"] = "max-age=" + cacheSeconds

  	//determine the file's size
  	let stats = fs.lstatSync(file)

  	//the file has to be the last thing set in the formData
  	body.file = fs.createReadStream(file)
  	if (progress != false) {
  		body.file.on('data', function(data) {
  			let percent = Math.floor((body.file.bytesRead / stats.size) * 100)
  			progress(percent)
  		})
  	}

  	let options = {
  		url: formAttributes.action,
  		method: formAttributes.method,
  		headers: {
  			'Content-Type': 'text/plain;charset=UTF-8'
  		},
  		formData: body
  	}
  	requesty(options, function (error, response, body) {
  		if (!error && response.statusCode >= 200 && response.statusCode <= 299) {
  			//we got a valid response
  			let result = body
  			callback(true, result)
  		} else if (response == undefined) {
        callback(false, error)
  		} else {
  			callback(false, response)
  		}
  	})
  }

  /**
   * Call this to asynchronously call an unsecured API. Pass in the API url to call along with the data.
   * If this is a GET request then the data will be appended to the apiURL.
   */
  static uploadFileToS3Old(formAttributes, formInputs, key, filePath, callback, cacheSeconds = 0) {
    var xmlhttp = Darwin.getXMLHttpRequest()
    xmlhttp.onreadystatechange = function() {
      if (xmlhttp.readyState === 4) {
        //if there is an authentication error we should try to refresh the token
        //if that works then we need to call this apiURL again, otherwise logout the user
        if ((xmlhttp.status >= 400 && xmlhttp.status <= 599)) {
          if (xmlhttp.status === 404) {
            //404 Not Found Error. The API Method doesn't exist or the wrong HTTP Method (GET, POST, PUT, ...) was used.
            console.log(xmlhttp.status);
            console.log(xmlhttp.responseText);
            callback(false, xmlhttp.responseText)
          } else {
            //print out the error if the callback is undefined or call the callback
            if (callback === undefined) {
              //just print out the response
              console.log(xmlhttp.status);
              console.log(xmlhttp.responseText);
            } else {
              //call the function
              let returnData = xmlhttp.responseText
              try {
                returnData = JSON.parse(returnData)
              } catch(e) {
                returnData = xmlhttp.responseText
              }
              //call the function
              callback(false, returnData);
            }
          }
        } else if (xmlhttp.status >= 200 && xmlhttp.status <= 299) {
          callback(true, null)
        } else {
          if (callback === undefined) {
            //just print out the response
            console.log(xmlhttp.status);
            console.log(xmlhttp.responseText);
          } else {
            //first parse the response if possible
            let returnData = xmlhttp.responseText
            try {
              returnData = JSON.parse(returnData)
            } catch(e) {
              returnData = xmlhttp.responseText
            }
            callback(true, returnData);
          }
        }
      }
    }
    let method = formAttributes.method
    let apiURL = formAttributes.action
    xmlhttp.open(method, apiURL, true);

    //Create the form to send
    var f = document.createElement("form");
    f.setAttribute('method', method);
    f.setAttribute('action', apiURL);
    f.setAttribute('enctype', formAttributes.enctype)

    let formData = new FormData(f)
    //set the content-type and cache-control
    let m = mime.lookup(filePath)
  	if (m == false) {
  		m = "binary/octet-stream"
  	}
    formData.set("Content-Type", mimeType)
    formData.set("Cache-Control", "max-age=" + cacheSeconds)
    //set all other formInputs
    for (let key in formInputs) {
      formData.set(key, formInputs[key])
    }
    //set the key and file data
    formData.set("key", key)
    let data = "slfkldsf"
    formData.set("file", data)
    xmlhttp.send(formData);
  }

  /**
   * Are we currently refreshing tokens
   */
  static refreshing = false
  /**
   * The refresh functions to call after we are done refreshing
   */
  static refreshFunctions = []

  /**
   * Attempts to refresh the tokens and then calls the callback with the response.
   */
  static refreshTokens(callback, authURL, clientID, refreshURL, authType) {
    var xmlhttpRefresh = Darwin.getXMLHttpRequest()
    xmlhttpRefresh.onreadystatechange=function(){
      if (xmlhttpRefresh.readyState === 4) {
        //if the refresh was successful, then we need to set the new csrf token. Otherwise, we
        //need to delete the cookie and tell the user they are logged out.
        var refreshFailed = false
        if ((xmlhttpRefresh.status >= 400 && xmlhttpRefresh.status <= 499)) {
          refreshFailed = true
        } else if (xmlhttpRefresh.status === 200) {
          console.log("REFRESH RESULT");
          console.log(xmlhttpRefresh.status);
          console.log(xmlhttpRefresh.responseText);

          //set the new csrf token
          var result = JSON.parse(xmlhttpRefresh.responseText);
          //check for an error
          if (result["error"] !== undefined) {
            refreshFailed = true
          } else {
            if (result["csrf_token"] !== undefined) {
              //we found the csrf token
              Darwin.setCookie("darwin-csrf", result["csrf_token"], result["expires_in"]);
            }
            if (result["access_token"] !== undefined) {
              //we found the access token
              Darwin.setCookie("darwin-access", result["access_token"], result["expires_in"]);
            }
            if (result["refresh_token"] !== undefined) {
              //we found the refresh token
              Darwin.setCookie("darwin-refresh", result["refresh_token"], result["expires_in"]);
            }
            //now call all of the refresh functions
            Darwin.refreshFunctions.forEach((func) => {
              func()
            })
            Darwin.refreshFunctions = []
          }
        } else {
          console.log("Unknown Refresh Response")
          //we couldn't refresh the tokens.
          //unknown error or something unexpected occurred.
          console.log(xmlhttpRefresh.status);
          console.log(xmlhttpRefresh.responseText);
        }

        if (refreshFailed === true) {
          console.log("REFRESH FAILED");
          console.log(xmlhttpRefresh.status);
          console.log(xmlhttpRefresh.responseText);

          //logout the user as our refresh has failed.
          Darwin.logout(authURL, authType)
        }
        Darwin.refreshing = false
      }
    }
    if (Darwin.refreshing) {
      Darwin.refreshFunctions.push(callback)
      return
    }
    console.log("Refreshing Tokens:", refreshURL)
    Darwin.refreshing = true
    Darwin.refreshFunctions = []
    Darwin.refreshFunctions.push(callback)
    xmlhttpRefresh.open("POST", refreshURL, true);
    xmlhttpRefresh.withCredentials = true;
    xmlhttpRefresh.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    xmlhttpRefresh.setRequestHeader("x-csrf-token", Darwin.getCookie("darwin-csrf"));
    let dat2 = {
      "grant_type": "refresh_token",
      "client_id": clientID,
      "refresh_token": Darwin.getCookie("darwin-refresh")
    }
    xmlhttpRefresh.send(Darwin.objectToGETURL(dat2, true));
  }

  /*
  Manually logs into the app using username and password.
  If success, the tokens are stored.
  */
  static manualLogin(clientID, baseURL, username, password, callback) {
    Darwin.callUnsecuredAPI("POST", baseURL + "access_token", {
      grant_type: "password",
      client_id: clientID,
      username: username,
      password: password
    }, (result) => {
      if ("access_token" in result) {
        //success, now store the tokens
        if (result["csrf_token"] !== undefined) {
          //we found the csrf token
          Darwin.setCookie("darwin-csrf", result["csrf_token"], result["expires_in"]);
        }
        if (result["access_token"] !== undefined) {
          //we found the access token
          Darwin.setCookie("darwin-access", result["access_token"], result["expires_in"]);
        }
        if (result["refresh_token"] !== undefined) {
          //we found the refresh token
          Darwin.setCookie("darwin-refresh", result["refresh_token"], result["expires_in"]);
        }
      }
      callback(result)
    })
  }

  /**
   * Call this to asynchronously call an API using user authentication, The result will be passed to the callback.
   * If we are unauthorized, then a request will be made to refresh the token if possible.
   * For authURL, pass the URL used to login the user. The page will redirect to authURL if the user isn't logged in
   * and we couldn't refresh the tokens.
   * For clientID, pass in the clientID of this app that is making API requests.
   * For refreshURL, use the URL used for Darwin Auth to submit a refresh_tokens request.
   * isRefresh should be false for your API call, this method will be re-run if the tokens are invalid
   * with isRefresh set to true.
   */
  static callSecuredAPI(method, apiURL, data, headers, callback, authURL, authType, clientID, refreshURL, isRefresh = false) {
    //console.log("calling API: ", method, apiURL, data)
    if (["GET", "POST", "PUT", "DELETE", "HEAD", "CONNECT", "OPTIONS", "TRACE", "PATCH"].indexOf(method) === -1) {
      console.error("Darwin API - method parameter is not valid. You supplied " + method);
      return
    }
    var xmlhttp = Darwin.getXMLHttpRequest()
    xmlhttp.onreadystatechange = function() {
      if (xmlhttp.readyState === 4) {
        //if there is an authentication error we should try to refresh the token
        //if that works then we need to call this apiURL again, otherwise logout the user
        if ((xmlhttp.status >= 400 && xmlhttp.status <= 499)) {
          if (xmlhttp.status === 401 && isRefresh === false) {
            //authentication error, try to refresh the token
            Darwin.refreshTokens(() => {
              Darwin.callSecuredAPI(method, apiURL, data, headers, callback, authURL, clientID, refreshURL, true)
            }, authURL, clientID, refreshURL, authType)
            /*var xmlhttpRefresh = Darwin.getXMLHttpRequest()
            xmlhttpRefresh.onreadystatechange=function(){
              if (xmlhttpRefresh.readyState === 4) {
                //if the refresh was successful, then we need to set the new csrf token. Otherwise, we
                //need to delete the cookie and tell the user they are logged out.
                var refreshFailed = false
                if ((xmlhttpRefresh.status >= 400 && xmlhttpRefresh.status <= 499)) {
                  refreshFailed = true
                } else if (xmlhttpRefresh.status === 200) {
                  console.log("REFRESH RESULT");
                  console.log(xmlhttpRefresh.status);
                  console.log(xmlhttpRefresh.responseText);

                  //set the new csrf token
                  var result = JSON.parse(xmlhttpRefresh.responseText);
                  //check for an error
                  if (result["error"] !== undefined) {
                    refreshFailed = true
                  } else {
                    if (result["csrf_token"] !== undefined) {
                      //we found the csrf token
                      Darwin.setCookie("darwin-csrf", result["csrf_token"], result["expires_in"]);
                    }
                    if (result["access_token"] !== undefined) {
                      //we found the access token
                      Darwin.setCookie("darwin-access", result["access_token"], result["expires_in"]);
                    }
                    if (result["refresh_token"] !== undefined) {
                      //we found the refresh token
                      Darwin.setCookie("darwin-refresh", result["refresh_token"], result["expires_in"]);
                    }
                    //now call all of the refresh functions
                    Darwin.refreshFunctions.forEach((func) => {
                      func()
                    })
                    Darwin.refreshFunctions = []
                  }
                } else {
                  console.log("Unknown Refresh Response")
                  //we couldn't refresh the tokens.
                  //unknown error or something unexpected occurred.
                  console.log(xmlhttpRefresh.status);
                  console.log(xmlhttpRefresh.responseText);
                }

                if (refreshFailed === true) {
                  console.log("REFRESH FAILED");
                  console.log(xmlhttpRefresh.status);
                  console.log(xmlhttpRefresh.responseText);

                  //logout the user as our refresh has failed.
                  Darwin.logout(authURL, authType)
                }
                Darwin.refreshing = false
              }
            }
            if (Darwin.refreshing) {
              Darwin.refreshFunctions.push(() => {Darwin.callSecuredAPI(method, apiURL, data, headers, callback, authURL, authType, clientID, refreshURL, true)})
              return
            }
            console.log("Refreshing Tokens:", refreshURL)
            Darwin.refreshing = true
            Darwin.refreshFunctions = []
            Darwin.refreshFunctions.push(() => {Darwin.callSecuredAPI(method, apiURL, data, headers, callback, authURL, authType, clientID, refreshURL, true)})
            xmlhttpRefresh.open("POST", refreshURL, true);
            xmlhttpRefresh.withCredentials = true;
            xmlhttpRefresh.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
            xmlhttpRefresh.setRequestHeader("x-csrf-token", Darwin.getCookie("darwin-csrf"));
            let dat2 = {
              "grant_type": "refresh_token",
              "client_id": clientID,
              "refresh_token": Darwin.getCookie("darwin-refresh")
            }
            xmlhttpRefresh.send(Darwin.objectToGETURL(dat2, true));*/
          } else if (xmlhttp.status === 404) {
            //404 Not Found Error. The API Method doesn't exist or the wrong HTTP Method (GET, POST, PUT, ...) was used.
            console.log(xmlhttp.status);
            console.log(xmlhttp.responseText);
          } else {
            //print out the error if the callback is undefined or call the callback
            if (callback === undefined) {
              //just print out the response
              console.log(xmlhttp.status);
              console.log(xmlhttp.responseText);
            } else {
              //call the function
              let returnData = xmlhttp.responseText
              try {
                returnData = JSON.parse(returnData)
              } catch(e) {
                console.log("Couldn't parse response with exception: ", e)
                console.log("Response: ", xmlhttp.responseText, xmlhttp.status)
              }
              //print out the error
              console.log("Error on API ", method, apiURL, returnData)
              //call the function
              callback(returnData);
            }
          }
        } else if (xmlhttp.status === 0 || xmlhttp.status === 500) {
          //There was a server error. Most likely an error with an API.
          console.log("Internal Server Error");
          callback({
            "error": "Internal Server Error"
          })
        } else {
          if (callback === undefined) {
            //just print out the response
            console.log(xmlhttp.status);
            console.log(xmlhttp.responseText);
          } else {
            //first parse the response if possible
            let returnData = xmlhttp.responseText
            try {
              returnData = JSON.parse(returnData)
            } catch(e) {
              console.log("Couldn't parse response with exception: ", e)
              console.log("Response: ", xmlhttp.responseText, xmlhttp.status)
            }
            //call the function
            callback(returnData);
          }
        }
      }
    }
    if (method === "GET") {
      xmlhttp.open(method, apiURL + Darwin.objectToGETURL(data), true);
    } else {
      xmlhttp.open(method, apiURL, true);
    }
    xmlhttp.withCredentials = true;
    xmlhttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    xmlhttp.setRequestHeader("x-csrf-token", Darwin.getCookie("darwin-csrf"));
    xmlhttp.setRequestHeader("authorization", "Bearer " + Darwin.getCookie("darwin-access"));
    for (var hkey in headers) {
      xmlhttp.setRequestHeader(hkey, headers[hkey])
    }
    if (method === "POST" || method === "PUT" || method === "DELETE") {
      xmlhttp.send(Darwin.objectToGETURL(data, true));
    } else if (method === "GET") {
      xmlhttp.send();//the data must be set in the apiURL for GET functions
    }
  }

  /**
   * Call this to asynchronously call an API using client authentication, and then call the func callback with
   * the results when finished. If we are unauthorized, then a request will be made
   * to trade client_credentials for tokens if possible. Provide the client_id and client_secret as well as the url
   * used to grant an access token.
   */
  static clientCallDarwinAPI(method, apiURL, data, headers, callback, isRefresh, client_id, client_secret, accessTokenURL) {
    if (isRefresh === undefined) {
      isRefresh = false;
    }
    var xmlhttp = Darwin.getXMLHttpRequest()
    xmlhttp.onreadystatechange = function() {
      if (xmlhttp.readyState === 4) {
        //if there is an authentication error we should try to trade client_credentials for tokens.
        //if that works then we need to call this apiURL again, otherwise fail.
        if ((xmlhttp.status >= 400 && xmlhttp.status <= 499)) {
          if (xmlhttp.status === 401 && isRefresh === false) {
            //authentication error, try to refresh the token
            var xmlhttpRefresh = Darwin.getXMLHttpRequest()
            xmlhttpRefresh.onreadystatechange = function() {
              if (xmlhttpRefresh.readyState === 4) {
                //if the refresh was successful, then we need to set the new csrf and access tokens.
                //otherwise, we have failed and something went wrong with the auth mechanism (bad credentials, ...).
                if ((xmlhttpRefresh.status >= 400 && xmlhttpRefresh.status <= 499)) {
                  console.log(chalk.bold.red("Failed to trade client credentials for tokens."));
                  try {
                    let json = JSON.parse(xmlhttpRefresh.responseText);
                    if (json.error != undefined) {
                      console.log(chalk.bold.red(json.error));
                    }
                  } catch(e) {
                    console.log(chalk.bold.red(xmlhttpRefresh.responseText))
                  }
                  console.log(chalk.bold.red("Check your DARWIN_CLIENT_ID & DARWIN_CLIENT_SECRET environment variables."))

                  Darwin.deleteCookie("darwin-csrf");
                  Darwin.deleteCookie("darwin-access");
                  callback(false, xmlhttpRefresh.responseText)
                } else if (xmlhttpRefresh.status === 200) {
                  //console.log("Client Credentials Traded for Tokens");

                  //set the new tokens
                  var result = JSON.parse(xmlhttpRefresh.responseText);
                  if (result.token_type !== undefined) {
                    //we found the csrf token
                    Darwin.setCookie("darwin-csrf", result.csrf_token, result.expires_in);
                    Darwin.setCookie("darwin-access", result.access_token, result.expires_in);
                  }
                  //now call the function again
                  Darwin.clientCallDarwinAPI(method, apiURL, data, headers, callback, true, client_id, client_secret, accessTokenURL);
                } else {
                  console.log("Unknown Error")
                  //unknown error or something unexpected occurred.
                  console.log(xmlhttpRefresh.status);
                  console.log(xmlhttpRefresh.responseText);
                  callback(false, xmlhttpRefresh.responseText)
                }
              }
            }
            //console.log("Trading Credentials for Tokens...")
            xmlhttpRefresh.open("POST", accessTokenURL, true);
            xmlhttpRefresh.withCredentials = true;
            xmlhttpRefresh.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
            let pd = {
              "grant_type": "client_credentials",
              "client_id": client_id,
              "client_secret": client_secret,
              "scope": ""
            }
            xmlhttpRefresh.send(Darwin.objectToGETURL(pd, true));
          } else if (xmlhttp.status === 404) {
            //404 Not Found Error. The API Method doesn't exist or the wrong HTTP Method (GET, POST, PUT, ...) was used.
            console.log(xmlhttp.status);
            console.log(xmlhttp.responseText);
            callback(false, xmlhttp.responseText)
          } else if (xmlhttp.status === 403) {
            //Forbidden. You don't have access.
            console.log(chalk.bold.red("You don't have access to this resource: " + apiURL))
            callback(false, xmlhttp.responseText)
          } else {
            //print out the error if the callback is undefined or call the callback
            if (callback === undefined) {
              //just print out the response
              console.log(xmlhttp.status);
              console.log(xmlhttp.responseText);
            } else {
              //call the function
              let json = xmlhttp.responseText
              try {
                json = JSON.parse(json);
                if (json.data != undefined) {
                  json = json.data
                }
              } catch(e) {
                json = xmlhttp.responseText
              }
              callback(false, json);
            }
          }
        } else if (xmlhttp.status == 0) {
          //no internet connection
          console.log(chalk.bold.red("You are not connected to the internet! Check your internet settings and try again."))
          callback(false, xmlhttp.responseText)
        } else {
          if (callback === undefined) {
            //just print out the response
            console.log(xmlhttp.status);
            console.log(xmlhttp.responseText);
          } else {
            //success, try to parse responseText
            let json = xmlhttp.responseText
            try {
              json = JSON.parse(json);
              if (json.data != undefined) {
                json = json.data
              }
            } catch(e) {
              json = xmlhttp.responseText
            }
            callback(true, json);
          }
        }
      }
    }
    if (method === "GET") {
      xmlhttp.open(method, apiURL + Darwin.objectToGETURL(data), true);
    } else {
      xmlhttp.open(method, apiURL, true);
    }
    xmlhttp.withCredentials = true;
    xmlhttp.setRequestHeader("Content-Type", "application/json");
    //xmlhttp.setRequestHeader("content-type", "application/x-www-form-urlencoded");
    xmlhttp.setRequestHeader("x-csrf-token", Darwin.getCookie("darwin-csrf"));
    xmlhttp.setRequestHeader("authorization", "Bearer " + Darwin.getCookie("darwin-access"));
    for (var hkey in headers) {
      xmlhttp.setRequestHeader(hkey, headers[hkey])
    }
    if (method === "POST" || method === "PUT" || method === "DELETE") {
      xmlhttp.send(JSON.stringify(data))
      /*let dd = Darwin.objectToGETURL(data, true)
      console.log(dd)
      xmlhttp.send(dd);*/
    } else if (method === "GET") {
      xmlhttp.send();//the data must be set in the apiURL for GET functions
    }
  }

  /**
  * Returns the url parameters as an object that you can access.
  * Code taken from https://www.sitepoint.com/get-url-parameters-with-javascript/
  */
  static getAllUrlParams(url) {
    // get query string from url (optional) or window
    var queryString = url ? url.split('?')[1] : window.location.search.slice(1);

    // we'll store the parameters here
    var obj = {};

    // if query string exists
    if (queryString) {

      // stuff after # is not part of query string, so get rid of it
      queryString = queryString.split('#')[0];

      // split our query string into its component parts
      var arr = queryString.split('&');

      let funcv = (v) => {
        paramNum = v.slice(1,-1);
        return '';
      }

      for (var i=0; i<arr.length; i++) {
        // separate the keys and the values
        var a = arr[i].split('=');

        // in case params look like: list[]=thing1&list[]=thing2
        var paramNum = undefined;
        var paramName = a[0].replace(/\[\d*\]/, (v) => {
          return funcv(v)
        });

        // set parameter value (use 'true' if empty)
        var paramValue = typeof(a[1])==='undefined' ? true : a[1];

        // if parameter name already exists
        if (obj[paramName]) {
          // convert value to array (if still string)
          if (typeof obj[paramName] === 'string') {
            obj[paramName] = [obj[paramName]];
          }
          // if no array index number specified...
          if (typeof paramNum === 'undefined') {
            // put the value on the end of the array
            obj[paramName].push(paramValue);
          }
          // if array index number specified...
          else {
            // put the value at that index number
            obj[paramName][paramNum] = paramValue;
          }
        }
        // if param name doesn't exist yet, set it
        else {
          obj[paramName] = paramValue;
        }
      }
    }
    return obj;
  }

  /**
  * Sets a cookie with the provided name, value, and number of seconds until expiration
  */
  static setCookie(cname, cvalue, exseconds) {
    if (typeof(Storage) !== "undefined") {
      // Code for localStorage/sessionStorage.
      localStorage.setItem(cname, cvalue)
    } else if (typeof localStorage === "undefined" || localStorage === null) {
      var LocalStorage = require('node-localstorage').LocalStorage;
      let localStorage = new LocalStorage('./scratch');
      localStorage.setItem(cname, cvalue)
    } else {
      // Sorry! No Web Storage support... so use cookies
      var d = new Date();
      d.setTime(d.getTime() + (exseconds*1000));
      var expires = "expires="+d.toUTCString();
      document.cookie = cname + "=" + cvalue + "; " + expires + ";secure";
    }
  }

  /**
  * Gets a cookie if it exists or returns "";
  */
  static getCookie(cname) {
    if (typeof(Storage) !== "undefined") {
      // Code for localStorage/sessionStorage.
      let val = localStorage.getItem(cname)
      if (val === null) {
        return ""
      } else {
        return val
      }
    } else if (typeof localStorage === "undefined" || localStorage === null) {
      var LocalStorage = require('node-localstorage').LocalStorage;
      let localStorage = new LocalStorage('./scratch');
      let val = localStorage.getItem(cname)
      if (val === null) {
        return ""
      } else {
        return val
      }
    } else {
      // Sorry! No Web Storage support... so use cookies
      var name = cname + "=";
      var ca = document.cookie.split(';');
      for(var i = 0; i < ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0) === ' ') {
          c = c.substring(1);
        }
        if (c.indexOf(name) === 0) {
          return c.substring(name.length, c.length);
        }
      }
      return "";
    }
  }

  /**
  * Deletes the cookie from the browser
  */
  static deleteCookie(cname) {
    if (typeof(Storage) !== "undefined") {
      // Code for localStorage/sessionStorage.
      localStorage.removeItem(cname)
    } else if (typeof localStorage === "undefined" || localStorage === null) {
      var LocalStorage = require('node-localstorage').LocalStorage;
      let localStorage = new LocalStorage('./scratch');
      localStorage.removeItem(cname)
    } else {
      // Sorry! No Web Storage support... so use cookies
      Darwin.setCookie(cname, "", -100);
    }
  }

  /**
  Converts a dictionary into a url encoded array for use in calling APIs.
  */
  static objectToGETURL(dict, post = false) {
    var res = "?"
    if (post === true) {
      res = ""
    }
    for (var k in dict) {
      if (Array.isArray(dict[k])) {
        //this is an array so add each element of the array to the url
        for (var l in dict[k]) {
          res += encodeURIComponent(k) + "=" + encodeURIComponent(dict[k][l]) + "&"
        }
      } else if (typeof dict[k] === 'object') {
        //json encode the object
        res += encodeURIComponent(k) + "=" + encodeURIComponent(JSON.stringify(dict[k])) + "&"
      } else {
        res += encodeURIComponent(k) + "=" + encodeURIComponent(dict[k]) + "&"
      }
    }
    //remove the last & if we have set some value
    if (res.length > 1) {
      return res.substring(0, res.length - 1)
    } else {
      return ""
    }
  }

  /**
  * Validates the provided input based on if it is required, its type, its value
  * the name of the form element (string), the provided form (as a dom element), and an optional min and max value
  */
  static validateInput(required, type, val, name, form, min = -1, max = -1) {
    let retVal = "yes";
  	let regex = /^((\s|.)*?)$/;
    let numeric = false
    switch (type) {
      case "choice":
        //find the element and construct the regex string from the choices
        let regexString = "^("
        for (var i = 0; i < form.elements.length; i++) {
          if (form.elements[i].name === name) {
            let e = form.elements[i];
            for (var j = 0; j < e.options.length; j++) {
              if (j === 0) {
                regexString += e.options[j].value
              } else {
                regexString += "|" + e.options[j].value
              }
            }
          }
        }
        regexString += ")$"
        regex = new RegExp(regexString, "g")
        break;
      case "text":
        regex = /^((\s|.)*?)$/;
        break;
      case "file":
        regex = /^((\s|.)*?)$/;
        break;
      case "folder":
        regex = /^((\s|.)*?)$/;
        break;
      case "number":
        regex = /^-?\d+$/;
        numeric = true
        break;
      case "positiveNumber":
        regex = /^\d+$/;
        numeric = true
        break;
      case "decimal":
        regex = /^-?\d+(?:\.\d*)?$/;
        numeric = true
        break;
      case "versionNumber":
        regex = /^[0-9]+([.][0-9]+)*$/;
        break;
      case "email":
        regex = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,63}$/;
        break;
      case "password":
        regex = /^(?=.*\d)(?=.*[A-Z])(?=.*[a-z])([^\s]){8,100}$/;
        break;
      case "date":
        regex = /^\d{1,2}-\d{1,2}-\d{4}$/;
        break;
      case "time":
        regex = /^\d{1,2}:\d{2}(AM|am|PM|pm)+$/;
        break;
      case "alphanumeric":
        regex = /^[A-Za-z0-9]+$/;
        break;
      case "alphanumeric+":
        regex = /^[A-Za-z0-9]+([A-Za-z0-9]|[-_ ][A-Za-z0-9])*$/;
        break;
      case "variableName":
        regex = /^[A-Za-z]{1}[A-Za-z0-9]{0,30}$/;
        break;
      case "hex":
        regex = /^[0-9A-Fa-f]+$/;
        break;
      case "color":
        regex = /^[0-9A-Fa-f]{6}$/;
        break;
      case "domain":
        regex = /^(?=^.{4,253}$)(^((?!(-|_))[a-zA-Z0-9-_]{0,62}[a-zA-Z0-9]\.){2,}[a-zA-Z]{2,63}$)$/;
        break;
      case "url":
        regex = /^https?:\/\/(?=.{4,253})(((?!(-|_))[a-zA-Z0-9-_]{0,62}[a-zA-Z0-9]\.){1,}[a-zA-Z]{2,63})(\/.*)?$/;
        break;
      case "bool":
				regex = /^(0|1)$/;
        numeric = true
				break;
      case "json":
        try {
          JSON.parse(val)
        } catch (err) {
          //the value is not valid json
          retVal = "failed"
        }
        break;
      default:
        regex = /^(.*?)$/;
        break;
    }
    if (val === null || val === "") {
  		if (required) {
        //required field not filled out
        retVal = "empty"
      }
  	} else if (type === "files") {
      //this is an array of files, just make sure there is at least one
      if (val.length === 0) {
        retVal = "failed";
      }
    } else if (!regex.test(val)) {
      //the regex failed
  		retVal = "failed";
  	} else if (retVal === "yes" && min !== -1) {
      //make sure the min is valid if necessary
      if (numeric) {
        if (val < min) {
          retVal = "failed";
        }
      } else if (val.length < min) {
        //string test
        retVal = "failed";
      }
    } else if (retVal === "yes" && max !== -1) {
      //make sure the min is valid if necessary
      if (numeric) {
        if (val > max) {
          retVal = "failed";
        }
      } else if (val.length > max) {
        //string test
        retVal = "failed";
      }
    }

    if (retVal === "yes") {
      return true
    } else {
      return false
    }
  }
}
