;(function (exports) {
  'use strict';

  var OAUTH3 = exports.OAUTH3;
  var OAUTH3_CORE = exports.OAUTH3_CORE;

  function getDefaultAppUrl() {
    console.warn('[deprecated] using window.location.{protocol, host, pathname} when opts.client_id should be used');
    return window.location.protocol
      + '//' + window.location.host
      + (window.location.pathname).replace(/\/?$/, '')
      ;
  }

  var browser = exports.OAUTH3_BROWSER = {
    window: window
  , clientUri: function (location) {
      return OAUTH3_CORE.normalizeUri(location.host + location.pathname);
    }
  , discover: function (providerUri, opts) {
      if (!providerUri) {
        throw new Error('oauth3.discover(providerUri, opts) received providerUri as ' + providerUri);
      }
      var directives = OAUTH3.hooks.getDirectives(providerUri);
      if (directives && directives.issuer) {
        return OAUTH3.PromiseA.resolve(directives);
      }
      return browser._discoverHelper(providerUri, opts).then(function (directives) {
        directives.issuer = directives.issuer || OAUTH3_CORE.normalizeUrl(providerUri);
        console.log('discoverHelper', directives);
        return OAUTH3.hooks.setDirectives(providerUri, directives);
      });
    }
  , _discoverHelper: function (providerUri, opts) {
      opts = opts || {};
      //opts.debug = true;
      providerUri = OAUTH3_CORE.normalizeUrl(providerUri);
      if (window.location.hostname.match(providerUri)) {
        console.warn("It looks like you're a provider checking for your own directive,"
          + " so we we're just gonna use OAUTH3.request({ method: 'GET', url: '.well-known/oauth3/directive.json' })");
        return OAUTH3.request({
          method: 'GET'
        , url: OAUTH3.core.normalizeUrl(providerUri) + '/.well-known/oauth3/directives.json'
        });
      }

      if (!window.location.hostname.match(opts.client_id || opts.client_uri)) {
        console.warn("It looks like your client_id doesn't match your current window... this probably won't end well");
        console.warn(opts.client_id || opts.client_uri, window.location.hostname);
      }
      var discObj = OAUTH3_CORE.urls.discover(providerUri, { client_id: (opts.client_id || opts.client_uri || getDefaultAppUrl()), debug: opts.debug });

      // TODO ability to reuse iframe instead of closing
      return browser.insertIframe(discObj.url, discObj.state, opts).then(function (params) {
        if (params.error) {
          return OAUTH3_CORE.formatError(providerUri, params.error);
        }
        var directives = JSON.parse(atob(OAUTH3_CORE.utils.urlSafeBase64ToBase64(params.result || params.directives)));
        return directives;
      }, function (err) {
        return OAUTH3.PromiseA.reject(err);
      });
    }

  , discoverAuthorizationDialog: function(providerUri, opts) {
      var discObj = OAUTH3.core.discover(providerUri, opts);

      // hmm... we're gonna need a broker for this since switching windows is distracting,
      // popups are obnoxious, iframes are sometimes blocked, and most servers don't implement CORS
      // eventually it should be the browser (and postMessage may be a viable option now), but whatever...

      // TODO allow postMessage from providerUri in addition to callback
      var discWin = OAUTH3.openWindow(discObj.url, discObj.state, { reuseWindow: 'conquerer' });
      return discWin.then(function (params) {
        console.log('discwin params');
        console.log(params);
        // discWin.child
        // TODO params should have response_type indicating json, binary, etc
        var directives = JSON.parse(atob(OAUTH3.core.utils.urlSafeBase64ToBase64(params.result || params.directives)));
        console.log('directives');
        console.log(directives);

        // Do some stuff
        var authObj = OAUTH3.core.implicitGrant(
          directives
        , { redirect_uri: opts.redirect_uri
          , debug: opts.debug
          , client_id: opts.client_id || opts.client_uri
          , client_uri: opts.client_uri || opts.client_id
          }
        );

        if (params.debug) {
          window.alert("DEBUG MODE: Pausing so you can look at logs and whatnot :) Fire at will!");
        }

        return new OAUTH3.PromiseA(function (resolve, reject) {
          // TODO check if authObj.url is relative or full
          discWin.child.location = OAUTH3.core.urls.resolve(providerUri, authObj.url);

          if (params.debug) {
            discWin.child.focus();
          }

          window['--oauth3-callback-' + authObj.state] = function (tokens) {
            if (tokens.error) {
              return reject(OAUTH3.core.formatError(tokens.error));
            }

            if (params.debug || tokens.debug) {
              if (window.confirm("DEBUG MODE: okay to close oauth3 window?")) {
                discWin.child.close();
              }
            }
            else {
              discWin.child.close();
            }

            resolve(tokens);
          };
        });
      }).then(function (tokens) {
        return OAUTH3.hooks.refreshSession(
          opts.session || {
            provider_uri: providerUri
          , client_id: opts.client_id
          , client_uri: opts.client_uri || opts.clientUri
          }
        , tokens
        );
      });
    }

  , frameRequest: function (url, state, opts) {
      var promise;

      if (!opts.windowType) {
        opts.windowType = 'popup';
      }

      if ('background' === opts.windowType) {
        promise = browser.insertIframe(url, state, opts);
      } else if ('popup' === opts.windowType) {
        promise = browser.openWindow(url, state, opts);
      } else if ('inline' === opts.windowType) {
        // callback function will never execute and would need to redirect back to current page
        // rather than the callback.html
        url += '&original_url=' + browser.window.location.href;
        promise = browser.window.location = url;
      } else {
        throw new Error("login framing method options.windowType not specified or not type yet implemented");
      }

      return promise.then(function (params) {
        var err;

        if (params.error || params.error_description) {
          err = new Error(params.error_description || "Unknown response error");
          err.code = params.error || "E_UKNOWN_ERROR";
          err.params = params;
          return OAUTH3.PromiseA.reject(err);
        }

        return params;
      });
    }

  , insertIframe: function (url, state, opts) {
      opts = opts || {};
      if (opts.debug) {
        opts.timeout = opts.timeout || 15 * 60 * 1000;
      }
      var promise = new OAUTH3.PromiseA(function (resolve, reject) {
        var tok;
        var iframeDiv;

        function cleanup() {
          delete window['--oauth3-callback-' + state];
          iframeDiv.remove();
          clearTimeout(tok);
          tok = null;
        }

        window['--oauth3-callback-' + state] = function (params) {
          resolve(params);
          cleanup();
        };

        tok = setTimeout(function () {
          var err = new Error("the iframe request did not complete within 15 seconds");
          err.code = "E_TIMEOUT";
          reject(err);
          cleanup();
        }, opts.timeout || 15 * 1000);

        // TODO hidden / non-hidden (via directive even)
        var framesrc = '<iframe class="js-oauth3-iframe" src="' + url + '" ';
        if (opts.debug) {
          framesrc += ' width="800px" height="800px" style="opacity: 0.8;" frameborder="1"';
        }
        else {
          framesrc += ' width="1px" height="1px" frameborder="0"';
        }
        framesrc += '></iframe>';

        iframeDiv = window.document.createElement('div');
        iframeDiv.innerHTML = framesrc;
        window.document.body.appendChild(iframeDiv);
      });

      // TODO periodically garbage collect expired handlers from window object
      return promise;
    }

  , openWindow: function (url, state, opts) {
      if (opts.debug) {
        opts.timeout = opts.timeout || 15 * 60 * 1000;
      }
      var promise = new OAUTH3.PromiseA(function (resolve, reject) {
        var tok;

        function cleanup() {
          clearTimeout(tok);
          tok = null;
          delete window['--oauth3-callback-' + state];
          // this is last in case the window self-closes synchronously
          // (should never happen, but that's a negotiable implementation detail)
          if (!opts.reuseWindow) {
            promise.child.close();
          }
        }

        window['--oauth3-callback-' + state] = function (params) {
          console.log('YOLO!!');
          resolve(params);
          cleanup();
        };

        tok = setTimeout(function () {
          var err = new Error("the windowed request did not complete within 3 minutes");
          err.code = "E_TIMEOUT";
          reject(err);
          cleanup();
        }, opts.timeout || 3 * 60 * 1000);

        setTimeout(function () {
          if (!promise.child) {
            reject("TODO: open the iframe first and discover oauth3 directives before popup");
            cleanup();
          }
        }, 0);
      });

      // TODO allow size changes (via directive even)
      promise.child = window.open(
        url
      , 'oauth3-login-' + (opts.reuseWindow || state)
      , 'height=' + (opts.height || 720) + ',width=' + (opts.width || 620)
      );
      // TODO periodically garbage collect expired handlers from window object
      return promise;
    }

    //
    // Logins
    //
  , authn: {
      authorizationRedirect: function (providerUri, opts) {
        // TODO get own directives

        return OAUTH3.discover(providerUri, opts).then(function (directive) {
          var prequest = OAUTH3_CORE.urls.authorizationRedirect(
            directive
          , opts
          );

          if (!prequest.state) {
            throw new Error("[Devolper Error] [authorization redirect] prequest.state is empty");
          }

          return browser.frameRequest(prequest.url, prequest.state, opts);
        }).then(function (tokens) {
          return OAUTH3.hooks.refreshSession(
            opts.session || {
              provider_uri: providerUri
            , client_id: opts.client_id
            , client_uri: opts.client_uri || opts.clientUri
            }
          , tokens
          );
        });
      }
    , implicitGrant: function (providerUri, opts) {
        // TODO let broker=true change behavior to open discover inline with frameRequest
        // TODO OAuth3 provider should use the redirect URI as the appId?
        return OAUTH3.discover(providerUri, opts).then(function (directive) {
          var prequest = OAUTH3_CORE.urls.implicitGrant(
            directive
            // TODO OAuth3 provider should referrer / referer / origin as the appId?
          , opts
          );

          if (!prequest.state) {
            throw new Error("[Devolper Error] [implicit grant] prequest.state is empty");
          }

          return browser.frameRequest(prequest.url, prequest.state, opts);
        }).then(function (tokens) {
          return OAUTH3.hooks.refreshSession(
            opts.session || {
              provider_uri: providerUri
            , client_id: opts.client_id
            , client_uri: opts.client_uri || opts.clientUri
            }
          , tokens
          );
        });
      }
    , logout: function (providerUri, opts) {
        opts = opts || {};

        return OAUTH3.discover(providerUri, opts).then(function (directive) {
          var prequest = OAUTH3_CORE.urls.logout(
            directive
          , opts
          );
          // Oauth3.init({ logout: function () {} });
          //return Oauth3.logout();

          var redirectUri = opts.redirect_uri || opts.redirectUri
            || (window.location.protocol + '//' + (window.location.host + window.location.pathname) + 'oauth3.html')
            ;
          var params = {
            // logout=true for all logins/accounts
            // logout=app-scoped-login-id for a single login
            action: 'logout'
            // TODO specify specific accounts / logins to delete from session
          , accounts: true
          , logins: true
          , redirect_uri: redirectUri
          , state: prequest.state
          , debug: opts.debug
          };

          if (prequest.url === params.redirect_uri) {
            return OAUTH3.PromiseA.resolve();
          }

          prequest.url += '#' + OAUTH3_CORE.querystringify(params);

          return OAUTH3.insertIframe(prequest.url, prequest.state, opts);
        });
      }
    }
  , isIframe: function isIframe () {
      try {
        return window.self !== window.top;
      } catch (e) {
        return true;
      }
    }
  , parseUrl: function (url) {
      var parser = document.createElement('a');
      parser.href = url;
      return parser;
    }
  , isRedirectHostSafe: function (referrerUrl, redirectUrl) {
      var src = browser.parseUrl(referrerUrl);
      var dst = browser.parseUrl(redirectUrl);

      // TODO how should we handle subdomains?
      // It should be safe for api.example.com to redirect to example.com
      // But it may not be safe for to example.com to redirect to aj.example.com
      // It is also probably not safe for sally.example.com to redirect to john.example.com
      // The client should have a list of allowed URLs to choose from and perhaps a wildcard will do
      //
      // api.example.com.evil.com SHOULD NOT match example.com
      return dst.hostname === src.hostname;
    }
  , checkRedirect: function (client, query) {
      console.warn("[security] URL path checking not yet implemented");

      var clientUrl = OAUTH3.core.normalizeUrl(client.url);
      var redirectUrl = OAUTH3.core.normalizeUrl(query.redirect_uri);

      // General rule:
      // I can callback to a shorter domain (fewer subs) or a shorter path (on the same domain)
      // but not a longer (more subs) or different domain or a longer path (on the same domain)


      // We can callback to an explicitly listed domain (TODO and path)
      if (browser.isRedirectHostSafe(clientUrl, redirectUrl)) {
        return true;
      }

      return false;
    }
  /*
  , redirect: function (redirect) {
      if (parser.search) {
        parser.search += '&';
      } else {
        parser.search += '?';
      }

      parser.search += 'error=E_NO_SESSION';
      redirectUri = parser.href;

      window.location.href = redirectUri;
    }
  */

  , hackFormSubmit: function (opts) {
      opts = opts || {};
      scope.authorizationDecisionUri = DaplieApiConfig.providerUri + '/api/org.oauth3.provider/authorization_decision';
      scope.updateScope();

      var redirectUri = scope.appQuery.redirect_uri.replace(/^https?:\/\//i, 'https://');
      var separator;

      // TODO check that we appropriately use '#' for implicit and '?' for code
      // (server-side) in an OAuth2 backwards-compatible way
      if ('token' === scope.appQuery.response_type) {
        separator = '#';
      }
      else if ('code' === scope.appQuery.response_type) {
        separator = '?';
      }
      else {
        separator = '#';
      }

      if (scope.pendingScope.length && !opts.allow) {
        redirectUri += separator + Oauth3.querystringify({
          error: 'access_denied'
          , error_description: 'None of the permissions were accepted'
          , error_uri: 'https://oauth3.org/docs/errors#access_denied'
          , state: scope.appQuery.state
        });
        window.location.href = redirectUri;
        return;
      }

      // TODO move to Oauth3? or not?
      // this could be implementation-specific,
      // but it may still be nice to provide it as de-facto
      var url = DaplieApiConfig.apiBaseUri + '/api/org.oauth3.provider/grants/:client_id/:account_id'
        .replace(/:client_id/g, scope.appQuery.client_id || scope.appQuery.client_uri)
        .replace(/:account_id/g, scope.selectedAccountId)
        ;

      var account = scope.sessionAccount;
      var session = { accessToken: account.token, refreshToken: account.refreshToken };
      var preq = {
        url: url
      , method: 'POST'
      , data: {
          scope: updateAccepted()
        , response_type: scope.appQuery.response_type
        , referrer: document.referrer || document.referer || ''
        , referer: document.referrer || document.referer || ''
        , tenant_id: scope.appQuery.tenant_id
        , client_id: scope.appQuery.client_id
        , client_uri: scope.appQuery.client_uri
        }
      , session: session
      };
      preq.clientId = preq.appId = DaplieApiConfig.appId || DaplieApiConfig.clientId;
      preq.clientUri = preq.appUri = DaplieApiConfig.appUri || DaplieApiConfig.clientUri;
      // TODO need a way to have middleware in Oauth3.request for TherapySession et al

      return Oauth3.request(preq).then(function (resp) {
        var err;
        var data = resp.data || {};

        if (data.error) {
          err = new Error(data.error.message || data.errorDescription);
          err.message = data.error.message || data.errorDescription;
          err.code = resp.data.error.code || resp.data.error;
          err.uri = 'https://oauth3.org/docs/errors#' + (resp.data.error.code || resp.data.error);
          return $q.reject(err);
        }

        if (!(data.code || data.accessToken)) {
          err = new Error("No grant code");
          return $q.reject(err);
        }

        return data;
      }).then(function (data) {
        redirectUri += separator + Oauth3.querystringify({
          state: scope.appQuery.state

        , code: data.code

        , access_token: data.access_token
        , expires_at: data.expires_at
        , expires_in: data.expires_in
        , scope: data.scope

        , refresh_token: data.refresh_token
        , refresh_expires_at: data.refresh_expires_at
        , refresh_expires_in: data.refresh_expires_in
        });

        if ('token' === scope.appQuery.response_type) {
          window.location.href = redirectUri;
          return;
        }
        else if ('code' === scope.appQuery.response_type) {
          scope.hackFormSubmitHelper(redirectUri);
          return;
        }
        else {
          console.warn("Grant Code NOT IMPLEMENTED for '" + scope.appQuery.response_type + "'");
          console.warn(redirectUri);
          throw new Error("Grant Code NOT IMPLEMENTED for '" + scope.appQuery.response_type + "'");
        }
      }, function (err) {
        redirectUri += separator + Oauth3.querystringify({
          error: err.code || 'server_error'
        , error_description: err.message || "Server Error: It's not your fault"
        , error_uri: err.uri || 'https://oauth3.org/docs/errors#server_error'
        , state: scope.appQuery.state
        });

        console.error('Grant Code Error NOT IMPLEMENTED');
        console.error(err);
        console.error(redirectUri);
        //window.location.href = redirectUri;
      });
    }

  , hackFormSubmitHelper: function (uri) {
      // TODO de-jQuerify
      //window.location.href = redirectUri;
      //return;

      // the only way to do a POST that redirects the current window
      window.jQuery('form.js-hack-hidden-form').attr('action', uri);

      // give time for the apply to take place
      window.setTimeout(function () {
        window.jQuery('form.js-hack-hidden-form').submit();
      }, 50);
    }
  };
  browser.requests = browser.authn;

  Object.keys(browser).forEach(function (key) {
    if ('requests' === key) {
      Object.keys(browser.requests).forEach(function (key) {
        OAUTH3.requests[key] = browser.requests[key];
      });
      return;
    }
    OAUTH3[key] = browser[key];
  });

}('undefined' !== typeof exports ? exports : window));