(function(global)
{
    Gitana.OAuth2Http = Gitana.Http.extend(
    /** @lends Gitana.OAuth2Http.prototype */
    {
        /**
         * @constructs
         *
         * @class Gitana.OAuth2Http
         */
        constructor: function(options, storage)
        {
            var self = this;

            // storage for OAuth credentials
            // this can either be a string ("local", "session", "memory") or a storage instance or empty
            // if empty, memory-based storage is assumed
            if (storage === null || typeof(storage) === "string")
            {
                storage = new Gitana.OAuth2Http.Storage(storage);
            }

            // cookie mode
            this.cookieMode = null;

            // ticket mode
            this.ticketMode = null;

            // preset the error state
            this.error = null;
            this.errorDescription = null;
            this.errorUri = null;

            // gitana urls
            var tokenURL = "/oauth/token";
            if (options.tokenURL)
            {
                tokenURL = options.tokenURL;
            }

            // base URL?
            var baseURL = null;
            if (options.baseURL)
            {
                baseURL = options.baseURL;
            }

            // client
            var clientKey = options.clientKey;
            var clientSecret = options.clientSecret;

            // authorization flow
            // if none specified, assume CODE
            this.authorizationFlow = options.authorizationFlow || Gitana.OAuth2Http.AUTHORIZATION_CODE;

            // optional
            if (options.requestedScope)
            {
                this.requestedScope = options.requestedScope;
            }

            if (this.authorizationFlow == Gitana.OAuth2Http.AUTHORIZATION_CODE)
            {
                this.code = options.code;
                this.redirectUri = options.redirectUri;
            }

            if (this.authorizationFlow == Gitana.OAuth2Http.PASSWORD)
            {
                this.username = options.username;

                if (options.password)
                {
                    this.password = options.password;
                }
                else
                {
                    this.password = "";
                }
            }

            if (this.authorizationFlow == Gitana.OAuth2Http.COOKIE)
            {
                this.cookieMode = true;
            }

            if (this.authorizationFlow == Gitana.OAuth2Http.TICKET)
            {
                this.ticketMode = options.ticket;
            }

            this.ticketMaxAge = options.ticketMaxAge;


            ////////////////////////////////////////////////////////////////////////////////////////////////
            //
            // ACCESSORS
            //
            ////////////////////////////////////////////////////////////////////////////////////////////////

            /**
             * Clears persisted storage of auth data
             */
            this.clearStorage = function()
            {
                storage.clear();
            };

            /**
             * Gets or saves the access token
             *
             * @param value [String] optional value
             */
            this.accessToken = function(value)
            {
                return storage.poke("accessToken", value);
            };

            /**
             * Gets or saves the refresh token
             *
             * @param value [String] optional value
             */
            this.refreshToken = function(value)
            {
                return storage.poke("refreshToken", value);
            };

            /**
             * Gets or saves the granted scope
             *
             * @param value [String] optional value
             */
            this.grantedScope = function(value)
            {
                return storage.poke("grantedScope", value);
            };

            /**
             * Gets or saves the expires in value
             *
             * @param value [String] optional value
             */
            this.expiresIn = function(value)
            {
                return storage.poke("expiresIn", value);
            };

            /**
             * Gets or saves the grant time
             *
             * @param value [String] optional value
             */
            this.grantTime = function(value)
            {
                return storage.poke("grantTime", value);
            };

            this.getClientAuthorizationHeader = function() {

                var basicString = clientKey + ":";
                if (clientSecret)
                {
                    basicString += clientSecret;
                }
                return "Basic " + Gitana.btoa(basicString);
            };

            this.getBearerAuthorizationHeader = function()
            {
                return "Bearer " + self.accessToken();
            };

            this.getPrefixedTokenURL = function()
            {
                return this.getPrefixedURL(tokenURL);
            };

            this.getPrefixedURL = function(url)
            {
                var rebasedURL = url;
                if (baseURL && Gitana.startsWith(url, "/"))
                {
                    rebasedURL = baseURL + url;
                }

                return rebasedURL;
            };


            // if they initiatialized with an access token, clear and write into persisted state
            // unless they're continuing an existing token
            if (this.authorizationFlow == Gitana.OAuth2Http.TOKEN)
            {
                var existingAccessToken = this.accessToken();
                if (existingAccessToken !== options.accessToken)
                {
                    storage.clear();
                }

                this.accessToken(existingAccessToken);
            }

            this.base();
        },

        /**
         * Performs an HTTP call using OAuth2.
         *
         * @param options
         */
        request: function(options)
        {
            var self = this;

            /**
             * Call over to Gitana and acquires an access token using flow authorization.
             *
             * @param success
             * @param failure
             */
            var doGetAccessToken = function(success, failure)
            {
                var onSuccess = function(response)
                {
                    var object = JSON.parse(response.text);
                    if (response["error"])
                    {
                        self.error = object["error"];
                        self.errorDescription = object["error_description"];
                        self.errorUri = object["error_uri"];
                    }
                    else
                    {
                        var _accessToken = object["access_token"];
                        var _refreshToken = object["refresh_token"];
                        var _expiresIn = object["expires_in"];
                        var _grantedScope = object["scope"];
                        var _grantTime = new Date().getTime();

                        // store into persistent storage
                        self.clearStorage();
                        self.accessToken(_accessToken);
                        self.refreshToken(_refreshToken);
                        self.expiresIn(_expiresIn);
                        self.grantedScope(_grantedScope);
                        self.grantTime(_grantTime);

                        // console.log("doGetAccessToken -> " + JSON.stringify(object));
                    }

                    success();
                };

                var onFailure = function(http, xhr) {
                    failure(http, xhr);
                };

                var o = {
                    success: onSuccess,
                    failure: onFailure,
                    headers: {
                        "Authorization": self.getClientAuthorizationHeader()
                    },
                    url: self.getPrefixedTokenURL(),
                    method: Gitana.OAuth2Http.TOKEN_METHOD
                };

                // query string
                var qs = {};

                // json payload
                qs["grant_type"] = self.authorizationFlow;
                if (self.requestedScope) {
                    qs["scope"] = self.requestedScope;
                }
                if (self.authorizationFlow === Gitana.OAuth2Http.AUTHORIZATION_CODE)
                {
                    qs["code"] = self.code;
                    if (self.redirectUri) {
                        qs["redirect_uri"] = self.redirectUri;
                    }
                }
                else if (self.authorizationFlow === Gitana.OAuth2Http.PASSWORD)
                {
                    qs["username"] = self.username;
                    qs["password"] = self.password;
                }

                // ticket max age
                if (self.ticketMaxAge)
                {
                    qs["ticketMaxAge"] = self.ticketMaxAge;
                }

                // if we're POSTing, do so as application/x-www-form-urlencoded to make secure over the wire
                if ("post" === Gitana.OAuth2Http.TOKEN_METHOD.toLowerCase())
                {
                    o.headers["Content-Type"] = "application/x-www-form-urlencoded";
                }

                // append into query string
                var queryString = Gitana.Http.toQueryString(qs);
                if (queryString)
                {
                    if (o.url.indexOf("?") > -1)
                    {
                        o.url = o.url + "&" + queryString;
                    }
                    else
                    {
                        o.url = o.url + "?" + queryString;
                    }
                }

                self.invoke(o);
            };

            /**
             * Calls over to Gitana and acquires an access token using an existing refresh token.
             *
             * @param success
             * @param failure
             */
            var doRefreshAccessToken = function(success, failure)
            {
                var onSuccess = function(response)
                {
                    var object = JSON.parse(response.text);
                    if (response["error"])
                    {
                        self.error = object["error"];
                        self.errorDescription = object["error_description"];
                        self.errorUri = object["error_uri"];
                    }
                    else
                    {
                        var _accessToken = object["access_token"];
                        var _refreshToken = object["refresh_token"];
                        var _expiresIn = object["expires_in"];
                        //self.grantedScope = object["scope"]; // this doesn't come back on refresh, assumed the same
                        var _grantTime = new Date().getTime();
                        var _grantedScope = self.grantedScope();

                        // store into persistent storage
                        self.clearStorage();
                        self.accessToken(_accessToken);
                        self.refreshToken(_refreshToken);
                        self.expiresIn(_expiresIn);
                        self.grantedScope(_grantedScope);
                        self.grantTime(_grantTime);

                        // console.log("doRefreshAccessToken -> " + JSON.stringify(object));
                    }

                    success();
                };

                var onFailure = function(http, xhr) {

                    Gitana.REFRESH_TOKEN_FAILURE_FN(self, http, xhr);

                    failure(http, xhr);
                };

                var o = {
                    success: onSuccess,
                    failure: onFailure,
                    headers: {
                        "Authorization": self.getClientAuthorizationHeader()
                    },
                    url: self.getPrefixedTokenURL(),
                    method: Gitana.OAuth2Http.TOKEN_METHOD
                };

                // query string
                var qs = {};

                // json payload
                qs["grant_type"] = "refresh_token";
                qs["refresh_token"] = self.refreshToken();
                if (self.requestedScope)
                {
                    qs["scope"] = self.requestedScope;
                }

                // ticket max age
                if (self.ticketMaxAge)
                {
                    qs["ticketMaxAge"] = self.ticketMaxAge;
                }

                // if we're POSTing, do so as application/x-www-form-urlencoded to make secure over the wire
                if ("post" === Gitana.OAuth2Http.TOKEN_METHOD.toLowerCase())
                {
                    o.headers["Content-Type"] = "application/x-www-form-urlencoded";
                }

                // append into query string
                var queryString = Gitana.Http.toQueryString(qs);
                if (queryString)
                {
                    if (o.url.indexOf("?") > -1)
                    {
                        o.url = o.url + "&" + queryString;
                    }
                    else
                    {
                        o.url = o.url + "?" + queryString;
                    }
                }

                self.invoke(o);
            };

            var doCall = function(autoAttemptRefresh)
            {
                var successHandler = function(response)
                {
                    options.success(response);
                };

                var failureHandler = function(http, xhr)
                {
                    if (autoAttemptRefresh)
                    {
                        // there are a few good reasons why this might naturally fail
                        //
                        // 1.  our access token is invalid, has expired or has been forcefully invalidated on the Cloud CMS server
                        //     in this case, we get back a 200 and something like http.text =
                        //     {"error":"invalid_token","error_description":"Invalid access token: blahblahblah"}
                        //
                        // 2.  the access token no longer permits access to the resource
                        //     in this case, we get back a 401
                        //     it might not make much sense to re-request a new access token, but we do just in case.

                        var notJson = false;
                        var isInvalidToken = false;
                        if (http.text)
                        {
                            var responseData = {};

                            // catch if http.text is not JSON
                            try
                            {
                                responseData = JSON.parse(http.text);
                            }
                            catch (e)
                            {
                                console.log("Error response is not json");
                                console.log(e);
                                notJson = true;
                            }

                            if (responseData.error)
                            {
                                if (responseData.error == "invalid_token")
                                {
                                    isInvalidToken = true;
                                }
                            }
                        }
                        var is401 = (http.code == 401);
                        var is400 = (http.code == 400);
                        var is403 = (http.code == 403);

                        // handle both cases
                        if (is401 || is400 || is403 || isInvalidToken || notJson)
                        {
                            if (self.refreshToken())
                            {
                                // use the refresh token to acquire a new access token
                                doRefreshAccessToken(function() {

                                    // success, got a new access token

                                    doCall(false);

                                }, function() {

                                    // failure, nothing else we can do
                                    // call into intended failure handler with the original failure http object
                                    options.failure(http, xhr);
                                });
                            }
                            else
                            {
                                // fail case - nothing we can do
                                options.failure(http, xhr);
                            }
                        }
                        else
                        {
                            // some other kind of error
                            options.failure(http, xhr);
                        }
                    }
                    else
                    {
                        // we aren't allowed to automatically attempt to get a new token via refresh token
                        options.failure(http, xhr);
                    }
                };

                // call through to the protected resource (with custom success/failure handling)
                var o = {};
                Gitana.copyInto(o, options);
                o.success = successHandler;
                o.failure = failureHandler;
                if (!o.headers)
                {
                    o.headers = {};
                }
                if (!self.cookieMode && !self.ticketMode)
                {
                    o.headers["Authorization"] = self.getBearerAuthorizationHeader();
                }
                if (self.ticketMode)
                {
                    o.headers["GITANA_TICKET"] = encodeURIComponent(self.ticketMode);
                }
                o.url = self.getPrefixedURL(o.url);

                // make the call
                self.invoke(o);
            };

            // if we have an access token and it's about to expire (within 20 seconds of it's expiry),
            // we force an early refresh of the access token so that concurrent requests don't get access problems
            // this is important for any browser-originated requests that rely on a persisted cookie (GITANA_TICKET)
            //
            // also provide some debugging if needed
            var forceRefresh = false;
            if (self.accessToken())
            {
                var grantTime = self.grantTime();
                if (grantTime)
                {
                    var expiresIn = self.expiresIn();
                    if (expiresIn)
                    {
                        // NOTE: expiresIn is in seconds
                        var expirationTimeMs = self.grantTime() + (self.expiresIn() * 1000);
                        var nowTimeMs = new Date().getTime();

                        var timeRemainingMs = expirationTimeMs - nowTimeMs;
                        if (timeRemainingMs <= 0)
                        {
                            // console.log("Access Token is expired, refresh will be attempted!");
                        }
                        else
                        {
                            // console.log("Access Token Time Remaining: " + timeRemainingMs);
                        }

                        if (timeRemainingMs <= 20000)
                        {
                            // console.log("Access Token only has 20 seconds left, forcing early refresh");
                            forceRefresh = true;
                        }
                    }
                }
            }

            // if no access token, request one
            if ((!self.accessToken() || forceRefresh) && !this.cookieMode && !this.ticketMode)
            {
                if (!self.refreshToken())
                {
                    // no refresh token, do an authorization call
                    doGetAccessToken(function() {

                        // got an access token, so proceed
                        doCall(true);

                    }, function(http, xhr) {

                        // access denied
                        options.failure(http, xhr);

                    });
                }
                else
                {
                    // we have a refresh token, so do a refresh call
                    doRefreshAccessToken(function() {

                        // got an access token, so proceed
                        doCall(true);

                    }, function(http, xhr) {

                        // unable to get an access token
                        options.failure(http, xhr);

                    });
                }
            }
            else
            {
                // we already have an access token
                doCall(true);
            }
        },

        /**
         * Refreshes the OAuth2 access token.
         */
        refresh: function(callback)
        {
            var self = this;

            var onSuccess = function(response)
            {
                var object = JSON.parse(response.text);
                if (response["error"])
                {
                    self.error = object["error"];
                    self.errorDescription = object["error_description"];
                    self.errorUri = object["error_uri"];

                    callback({
                        "error": self.error,
                        "message": self.errorDescription
                    });
                }
                else
                {
                    var _accessToken = object["access_token"];
                    var _refreshToken = object["refresh_token"];
                    var _expiresIn = object["expires_in"];
                    //self.grantedScope = object["scope"]; // this doesn't come back on refresh, assumed the same
                    var _grantTime = new Date().getTime();
                    var _grantedScope = self.grantedScope();

                    // store into persistent storage
                    self.clearStorage();
                    self.accessToken(_accessToken);
                    self.refreshToken(_refreshToken);
                    self.expiresIn(_expiresIn);
                    self.grantedScope(_grantedScope);
                    self.grantTime(_grantTime);

                    callback();
                }
            };

            var onFailure = function(http, xhr)
            {
                Gitana.REFRESH_TOKEN_FAILURE_FN(self, http, xhr);

                callback({
                    "message": "Unable to refresh access token"
                });
            };

            var o = {
                success: onSuccess,
                failure: onFailure,
                headers: {
                    "Authorization": self.getClientAuthorizationHeader()
                },
                url: self.getPrefixedTokenURL(),
                method: Gitana.OAuth2Http.TOKEN_METHOD
            };

            // query string
            var qs = {};

            // json payload
            qs["grant_type"] = "refresh_token";
            qs["refresh_token"] = self.refreshToken();
            if (self.requestedScope)
            {
                qs["scope"] = self.requestedScope;
            }


            // ticket max age
            if (self.ticketMaxAge)
            {
                qs["ticketMaxAge"] = self.ticketMaxAge;
            }

            // if we're POSTing, do so as application/x-www-form-urlencoded to make secure over the wire
            if ("post" === Gitana.OAuth2Http.TOKEN_METHOD.toLowerCase())
            {
                o.headers["Content-Type"] = "application/x-www-form-urlencoded";
            }

            // append into query string
            var queryString = Gitana.Http.toQueryString(qs);
            if (queryString)
            {
                if (o.url.indexOf("?") > -1)
                {
                    o.url = o.url + "&" + queryString;
                }
                else
                {
                    o.url = o.url + "?" + queryString;
                }
            }

            self.invoke(o);
        }
    });

    /**
     * Provides a storage location for OAuth2 credentials
     *
     * @param type
     * @param scope
     *
     * @return storage instance
     * @constructor
     */
    Gitana.OAuth2Http.Storage = function(scope)
    {
        // in-memory implementation of HTML5 storage interface
        var memoryStorage = function() {

            var memory = {};

            var m = {};
            m.removeItem = function(key)
            {
                delete memory[key];
            };

            m.getItem = function(key)
            {
                return memory[key];
            };

            m.setItem = function(key, value)
            {
                memory[key] = value;
            };

            return m;
        }();

        /**
         * Determines whether the current runtime environment supports HTML5 local storage
         *
         * @return {Boolean}
         */
        var supportsLocalStorage = function()
        {
            try {
                return 'localStorage' in window && window['localStorage'] !== null;
            } catch (e) {
                return false;
            }
        };

        /**
         * Determines whether the current runtime environment supports HTML5 session storage.
         *
         * @return {Boolean}
         */
        var supportsSessionStorage = function()
        {
            try {
                return 'sessionStorage' in window && window['sessionStorage'] !== null;
            } catch (e) {
                return false;
            }
        };

        var acquireStorage = function()
        {
            var storage = null;

            // store
            if (scope == "session" && supportsSessionStorage())
            {
                storage = sessionStorage;
            }
            else if (scope == "local" && supportsLocalStorage())
            {
                storage = localStorage;
            }
            else
            {
                // fall back to memory-based storage
                storage = memoryStorage;
            }

            return storage;
        };

        // result object
        var r = {};

        /**
         * Clears state.
         */
        r.clear = function()
        {
            acquireStorage().removeItem("gitanaAuthState");
        };

        /**
         * Pokes and peeks the value of a key in the state.
         *
         * @param key
         * @param value
         *
         * @return {*}
         */
        r.poke = function(key, value)
        {
            var state = {};

            var stateString = acquireStorage().getItem("gitanaAuthState");
            if (stateString) {
                state = JSON.parse(stateString);
            }

            var touch = false;
            if (typeof(value) !== "undefined" && value !== null)
            {
                state[key] = value;
                touch = true;
            }
            else if (value === null)
            {
                delete state[key];
                touch = true;
            }

            if (touch) {
                acquireStorage().setItem("gitanaAuthState", JSON.stringify(state));
            }

            return state[key];
        };

        return r;
    };

}(this));

// statics
Gitana.OAuth2Http.PASSWORD = "password";
Gitana.OAuth2Http.AUTHORIZATION_CODE = "authorization_code";
Gitana.OAuth2Http.TOKEN = "token";
Gitana.OAuth2Http.COOKIE = "cookie";
Gitana.OAuth2Http.TICKET = "ticket";

// method to use for retrieving access and refresh tokens
//Gitana.OAuth2Http.TOKEN_METHOD = "GET";
Gitana.OAuth2Http.TOKEN_METHOD = "POST";



