package de.golfgl.gdxgamesvcs;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Net;
import com.badlogic.gdx.net.HttpParametersUtils;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Base64Coder;
import com.badlogic.gdx.utils.JsonReader;
import com.badlogic.gdx.utils.JsonValue;
import com.badlogic.gdx.utils.Timer;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;

import de.golfgl.gdxgamesvcs.achievement.IAchievement;
import de.golfgl.gdxgamesvcs.achievement.IFetchAchievementsResponseListener;
import de.golfgl.gdxgamesvcs.gamestate.IFetchGameStatesListResponseListener;
import de.golfgl.gdxgamesvcs.gamestate.ILoadGameStateResponseListener;
import de.golfgl.gdxgamesvcs.gamestate.ISaveGameStateResponseListener;
import de.golfgl.gdxgamesvcs.leaderboard.IFetchLeaderBoardEntriesResponseListener;
import de.golfgl.gdxgamesvcs.leaderboard.ILeaderBoardEntry;

/**
 * GameServiceClient for GameJolt API
 * <p>
 * See https://github.com/MrStahlfelge/gdx-gamesvcs/wiki/GameJolt for documentation of this implementation.
 * <p>
 * See http://gamejolt.com/api/doc/game for GameJolt API documentation
 * <p>
 * Some code taken from smelc/gdx-gamejolt - thanks for providing it!
 * https://github.com/smelc/gdx-gamejolt &amp; http://www.schplaf.org/hgames/
 * <p>
 * Created by Benjamin Schulte on 17.06.2017.
 */

public class GameJoltClient implements IGameServiceClient {
    public static final String GAMESERVICE_ID = IGameServiceClient.GS_GAMEJOLT_ID;
    public static final String GJ_USERNAME_PARAM = "gjapi_username";
    public static final String GJ_USERTOKEN_PARAM = "gjapi_token";
    protected static final int GJ_PING_INTERVAL = 30;

    // This is not static and not final for overriding reasons
    public String GJ_GATEWAY = "https://gamejolt.com/api/game/v1/";
    protected IGameServiceListener gsListener;
    protected String userName;
    protected String userToken;
    protected String gjAppId;
    protected String gjAppPrivateKey;
    protected boolean connected;
    protected boolean connectionPending;
    protected boolean initialized;
    protected IGameServiceIdMapper<Integer> scoreTableMapper;
    protected IGameServiceIdMapper<Integer> trophyMapper;
    protected Timer.Task pingTask;
    private String eventKeyPrefix;
    private String guestName;

    /**
     * You need to call this basic initialization in order to call GameJolts
     *
     * @param gjAppId         your GameJolt App Id
     * @param gjAppPrivateKey your apps private key
     * @return this for method chaining
     */
    public GameJoltClient initialize(String gjAppId, String gjAppPrivateKey) {
        this.gjAppId = gjAppId;
        this.gjAppPrivateKey = gjAppPrivateKey;
        initialized = true;

        return this;
    }

    /**
     * sets up the mapper for score table calls
     *
     * @param scoreTableMapper
     * @return this for method chaining
     */
    public GameJoltClient setGjScoreTableMapper(IGameServiceIdMapper<Integer> scoreTableMapper) {
        this.scoreTableMapper = scoreTableMapper;
        return this;
    }

    /**
     * sets up the mapper for trophy calls
     *
     * @param trophyMapper
     * @return this for method chaining
     */
    public GameJoltClient setGjTrophyMapper(IGameServiceIdMapper<Integer> trophyMapper) {
        this.trophyMapper = trophyMapper;
        return this;
    }


    public String getUserToken() {
        return userToken;
    }

    /**
     * Sets the GameJolt user token. Not possible when connected!
     *
     * @param userToken
     * @return
     */
    public GameJoltClient setUserToken(String userToken) {
        if (isSessionActive())
            throw new IllegalStateException();

        this.userToken = userToken;
        return this;
    }

    /**
     * Sets the GameJolt user name. Not possible when connected!
     *
     * @param userName
     * @return
     */
    public GameJoltClient setUserName(String userName) {
        if (isSessionActive())
            throw new IllegalStateException();

        this.userName = userName;
        return this;
    }

    /**
     * see {@link #setGuestName(String)}
     *
     * @return
     */
    public String getGuestName() {
        return guestName;
    }

    /**
     * GameJolt can post scores to scoreboards without an authenticated user. Set a guest name to enable this featuee.
     *
     * @param guestName
     */
    public GameJoltClient setGuestName(String guestName) {
        this.guestName = guestName;

        return this;
    }

    @Override
    public String getGameServiceId() {
        return GAMESERVICE_ID;
    }

    @Override
    public void setListener(IGameServiceListener gsListener) {
        this.gsListener = gsListener;
    }

    @Override
    public boolean resumeSession() {
        return connect(true);
    }

    @Override
    public boolean logIn() {
        return connect(false);
    }

    public boolean connect(final boolean silent) {
        if (!initialized) {
            Gdx.app.error(GAMESERVICE_ID, "Cannot connect before app ID is set via initialize()");
            return false;
        }

        if (connected)
            return true;

        if (userName == null || userToken == null) {
            //show UI via Gdx.input.getTextInput not possible in GWT w/o gdx-dialog.
            //to avoid a dependency and keep this simple, nothing is done here but
            //see the sample apps extension at https://github.com/MrStahlfelge/gdx-gamesvcs-app
            //GameJolt branch
            Gdx.app.log(GAMESERVICE_ID, "Cannot connect without user name and user's token.");

            return false;
        }

        Map<String, String> params = new HashMap<String, String>();
        addGameIDUserNameUserToken(params);

        final Net.HttpRequest http = buildJsonRequest("users/auth/", params);
        if (http == null)
            return false;

        connectionPending = true;

        Gdx.net.sendHttpRequest(http, new Net.HttpResponseListener() {
            @Override
            public void handleHttpResponse(Net.HttpResponse httpResponse) {
                connectionPending = false;
                JsonValue response = null;
                String json = httpResponse.getResultAsString();
                try {
                    response = new JsonReader().parse(json).get("response");
                } catch (Throwable t) {
                    Gdx.app.error(GAMESERVICE_ID, "Could not parse answer from GameJolt", t);
                }

                if (response == null) {
                    Gdx.app.error(GAMESERVICE_ID, "Could not parse answer from GameJolt: " + json);
                    authenticationFailed(silent, "Cannot authenticate. Response not in right format.");
                } else {
                    connected = response.getBoolean("success");

                    if (connected) {
                        // Open a session
                        sendOpenSessionEvent();

                        if (gsListener != null)
                            gsListener.gsOnSessionActive();
                    } else {
                        Gdx.app.log(GAMESERVICE_ID, "Authentification from GameJolt failed. Check username, token, " +
                                "app id and private key.");
                        authenticationFailed(silent, "GameJolt authentication failed.");
                    }
                }
            }

            @Override
            public void failed(Throwable t) {
                Gdx.app.log(GAMESERVICE_ID, "Auth HTTP Request failed");
                authenticationFailed(silent, "Cannot connect to GameJolt due to network problems.");
            }

            @Override
            public void cancelled() {
                Gdx.app.log(GAMESERVICE_ID, "Auth HTTP Request cancelled.");
                authenticationFailed(silent, "Cannot connect to GameJolt. Request cancelled.");
            }
        });

        return true;
    }

    protected void sendOpenSessionEvent() {
        if (!isSessionActive())
            return;

        Map<String, String> params = new HashMap<String, String>();
        addGameIDUserNameUserToken(params);

        final Net.HttpRequest http = buildJsonRequest("sessions/open/", params);

        if (http != null)
            Gdx.net.sendHttpRequest(http, new NoOpResponseListener());

        pingTask = Timer.schedule(new Timer.Task() {
            @Override
            public void run() {
                sendKeepSessionOpenEvent();
            }
        }, GJ_PING_INTERVAL, GJ_PING_INTERVAL);

    }

    protected void sendKeepSessionOpenEvent() {
        if (!isSessionActive())
            return;

        Map<String, String> params = new HashMap<String, String>();
        addGameIDUserNameUserToken(params);

        final Net.HttpRequest http = buildJsonRequest("sessions/ping/", params);

        if (http != null)
            Gdx.net.sendHttpRequest(http, new NoOpResponseListener());

    }

    protected void authenticationFailed(boolean silent, String msg) {
        connected = false;
        connectionPending = false;

        if (gsListener != null) {
            gsListener.gsOnSessionInactive();

            if (!silent)
                gsListener.gsShowErrorToUser(IGameServiceListener.GsErrorType.errorLoginFailed, msg, null);
        }
    }

    @Override
    public void pauseSession() {
        if (pingTask != null)
            pingTask.cancel();

        sendCloseSessionEvent();

        connected = false;

        if (gsListener != null)
            gsListener.gsOnSessionInactive();
    }

    protected void sendCloseSessionEvent() {
        if (!isSessionActive())
            return;

        Map<String, String> params = new HashMap<String, String>();
        addGameIDUserNameUserToken(params);

        final Net.HttpRequest http = buildJsonRequest("sessions/close/", params);

        if (http != null)
            Gdx.net.sendHttpRequest(http, new NoOpResponseListener());

    }


    @Override
    public void logOff() {
        pauseSession();
        userName = null;
        userToken = null;
    }

    @Override
    public String getPlayerDisplayName() {
        return (connected ? userName : null);
    }

    @Override
    public boolean isSessionActive() {
        return connected;
    }

    @Override
    public boolean isConnectionPending() {
        return connectionPending && !connected;
    }

    @Override
    public void showLeaderboards(String leaderBoardId) throws GameServiceException {
        throw new GameServiceException.NotSupportedException();
    }

    @Override
    public void showAchievements() throws GameServiceException {
        throw new GameServiceException.NotSupportedException();
    }

    @Override
    public boolean fetchAchievements(final IFetchAchievementsResponseListener callback) {
        if (!isSessionActive())
            return false;

        Map<String, String> params = new HashMap<String, String>();
        addGameIDUserNameUserToken(params);

        final Net.HttpRequest http = buildJsonRequest("trophies/", params);

        if (http == null)
            return false;

        Gdx.net.sendHttpRequest(http, new Net.HttpResponseListener() {
            @Override
            public void handleHttpResponse(Net.HttpResponse httpResponse) {

                JsonValue response = null;
                String json = httpResponse.getResultAsString();
                try {
                    response = new JsonReader().parse(json).get("response");

                    if (response == null || !response.getBoolean("success")) {
                        Gdx.app.error(GAMESERVICE_ID, "Could not parse answer from GameJolt: " + json);
                        callback.onFetchAchievementsResponse(null);
                    } else {

                        JsonValue trophies = response.get("trophies");
                        Array<IAchievement> achs = new Array<IAchievement>();
                        for (JsonValue trophy = trophies.child; trophy != null; trophy = trophy.next) {
                            IAchievement ach = achievementJsonToObject(trophy);
                            if (ach != null)
                                achs.add(ach);
                        }
                        callback.onFetchAchievementsResponse(achs);
                    }
                } catch (Throwable t) {
                    Gdx.app.error(GAMESERVICE_ID, "Could not parse answer from GameJolt " + json, t);
                    callback.onFetchAchievementsResponse(null);
                }
            }

            @Override
            public void failed(Throwable t) {
                callback.onFetchAchievementsResponse(null);
            }

            @Override
            public void cancelled() {
                callback.onFetchAchievementsResponse(null);
            }
        });

        return true;
    }

    protected GjTrophy achievementJsonToObject(JsonValue trophy) {
        GjTrophy ach = GjTrophy.fromJson(trophy);
        ach.setTrophyMapper(trophyMapper);
        return ach;
    }

    @Override
    public boolean submitToLeaderboard(String leaderboardId, long score, String tag) {
        //GameJolt allows submitting scores without an open session.
        //Enable it by setting guest name.
        //see http://gamejolt.com/api/doc/game/scores/add

        if (!initialized) {
            Gdx.app.error(GAMESERVICE_ID, "Cannot post score: set app ID via initialize()");
            return false;
        }
        if (scoreTableMapper == null) {
            Gdx.app.log(GAMESERVICE_ID, "Cannot post score: No mapper for score table ids provided.");
            return false;
        }

        Integer boardId = scoreTableMapper.mapToGsId(leaderboardId);

        // no board available
        if (boardId == null)
            return false;

        Map<String, String> params = new HashMap<String, String>();

        if (isSessionActive())
            addGameIDUserNameUserToken(params);
        else if (guestName != null) {
            params.put("game_id", gjAppId);
            params.put("guest", guestName);
        } else {
            Gdx.app.log(GAMESERVICE_ID, "Cannot post to scoreboard. No guest name and no user given.");
            return false;
        }
        params.put("score", String.valueOf(score));
        params.put("sort", String.valueOf(score));
        if (tag != null)
            params.put("extra_data", tag);
        params.put("table_id", boardId.toString());

        final Net.HttpRequest http = buildJsonRequest("scores/add/", params);
        if (http == null)
            return false;

        Gdx.net.sendHttpRequest(http, new NoOpResponseListener());

        return true;
    }

    @Override
    public boolean fetchLeaderboardEntries(String leaderBoardId, int limit, boolean relatedToPlayer,
                                           final IFetchLeaderBoardEntriesResponseListener callback) {
        if (!initialized) {
            Gdx.app.error(GAMESERVICE_ID, "Cannot fetch leaderboard: set app ID via initialize() first");
            return false;
        }

        Map<String, String> params = new HashMap<String, String>();
        // http://gamejolt.com/api/doc/game/scores/fetch
        if (relatedToPlayer && isSessionActive())
            addGameIDUserNameUserToken(params);
        else
            params.put("game_id", gjAppId);

        params.put("limit", String.valueOf(limit));

        if (leaderBoardId != null) {
            Integer boardId = scoreTableMapper.mapToGsId(leaderBoardId);
            if (boardId != null)
                params.put("table_id", String.valueOf(boardId));
        }

        final Net.HttpRequest http = buildJsonRequest("scores/", params);
        if (http == null)
            return false;

        Gdx.net.sendHttpRequest(http, new Net.HttpResponseListener() {
            @Override
            public void handleHttpResponse(Net.HttpResponse httpResponse) {

                JsonValue response = null;
                String json = httpResponse.getResultAsString();
                try {
                    response = new JsonReader().parse(json).get("response");

                    if (response == null || !response.getBoolean("success")) {
                        Gdx.app.error(GAMESERVICE_ID, "Could not parse answer from GameJolt: " + json);
                        callback.onLeaderBoardResponse(null);
                    } else {
                        JsonValue scores = response.get("scores");
                        int rank = 0;
                        Array<ILeaderBoardEntry> les = new Array<ILeaderBoardEntry>();
                        for (JsonValue score = scores.child; score != null; score = score.next) {
                            rank++;
                            ILeaderBoardEntry gje = scoreJsonToObject(rank, score);
                            if (gje != null)
                                les.add(gje);
                        }
                        callback.onLeaderBoardResponse(les);
                    }
                } catch (Throwable t) {
                    Gdx.app.error(GAMESERVICE_ID, "Could not parse answer from GameJolt: " + json, t);
                    callback.onLeaderBoardResponse(null);
                }
            }

            @Override
            public void failed(Throwable t) {
                callback.onLeaderBoardResponse(null);
            }

            @Override
            public void cancelled() {
                callback.onLeaderBoardResponse(null);
            }
        });

        return true;
    }

    /**
     * converts GameJolt's scoreboard return json to our own data type. This method is for overriding purposes
     */
    protected ILeaderBoardEntry scoreJsonToObject(int rank, JsonValue score) {
        return GjScoreboardEntry.fromJson(score, rank, getPlayerDisplayName());
    }

    /**
     * see {@link #getEventKeyPrefix()}
     *
     * @return
     */
    public String getEventKeyPrefix() {
        return eventKeyPrefix;
    }


    /**
     * GameJolt does not have a dedicated API for events, but it is encouraged by documentation to use the
     * global data store. Use this method to set a prefix to use for event keys for no conflicts with other
     * keys you are using.
     * <p>
     * Please note: GameJolt does not provide a user interface for reading the event stats. You have to write
     * your own program to read the event stats regularly.
     * <p>
     * You have to set the key yourself the first time. submitEvents performs an add operation on the key, which
     * fails when the key is not already created.
     *
     * @param eventKeyPrefix Your prefix for event keys, or null to deactivate using global data storage for events.
     *                       Default is null.
     */
    public GameJoltClient setEventKeyPrefix(String eventKeyPrefix) {
        this.eventKeyPrefix = eventKeyPrefix;
        return this;
    }

    @Override
    public boolean submitEvent(String eventId, int increment) {

        if (!initialized) {
            Gdx.app.error(GAMESERVICE_ID, "Cannot submit event: set app ID via initialize() first");
            return false;
        }
        if (eventKeyPrefix == null) {
            Gdx.app.log(GAMESERVICE_ID, "No event logged - no event key prefix provided.");
            return false;
        }

        Map<String, String> params = new HashMap<String, String>();
        // no user name or token added! We want to use the global storage.
        // http://gamejolt.com/api/doc/game/data-store/update
        params.put("game_id", gjAppId);
        params.put("key", eventKeyPrefix + eventId);
        params.put("value", Integer.toString(increment));
        params.put("operation", "add");

        final Net.HttpRequest http = buildJsonRequest("data-store/update/", params);
        if (http == null)
            return false;

        Gdx.net.sendHttpRequest(http, new NoOpResponseListener());

        return true;
    }

    /**
     * Use careful! It resets your event to 0. Needed for first time initialization.
     *
     * @param eventId
     */
    public void initializeOrResetEventKey(String eventId) {
        if (!initialized) {
            Gdx.app.error(GAMESERVICE_ID, "Cannot submit event: set app ID via initialize() first");
            return;
        }
        if (eventKeyPrefix == null) {
            Gdx.app.log(GAMESERVICE_ID, "No event key prefix provided.");
            return;
        }

        // no user name or token added! We want to use the global storage.
        // http://gamejolt.com/api/doc/game/data-store/set
        Net.HttpRequest http = buildStoreDataRequest(eventKeyPrefix + eventId, true, "0");

        if (http != null)
            Gdx.net.sendHttpRequest(http, new NoOpResponseListener());

    }

    @Override
    public boolean unlockAchievement(String achievementId) {
        if (trophyMapper == null) {
            Gdx.app.log(GAMESERVICE_ID, "Cannot unlock achievement: No mapper for trophy ids provided.");
            return false;
        }

        if (!isSessionActive())
            return false;

        Integer trophyId = trophyMapper.mapToGsId(achievementId);

        // no board available or not connected
        if (trophyId == null)
            return false;

        Map<String, String> params = new HashMap<String, String>();
        addGameIDUserNameUserToken(params);
        params.put("trophy_id", String.valueOf(trophyId));

        final Net.HttpRequest http = buildJsonRequest("trophies/add-achieved/", params);
        if (http == null)
            return false;

        Gdx.net.sendHttpRequest(http, new NoOpResponseListener());

        return true;
    }

    @Override
    public boolean incrementAchievement(String achievementId, int incNum, float completionPercentage) {
        // not supported - fall back
        if (completionPercentage >= 1f)
            return unlockAchievement(achievementId);
        else
            return true;
    }

    @Override
    public void saveGameState(String fileId, byte[] gameState, long progressValue,
                              final ISaveGameStateResponseListener listener) {
        if (!isSessionActive()) {
            if (listener != null)
                listener.onGameStateSaved(false, "NOT_CONNECTED");
            return;
        }

        //TODO progressValue is saved for future use, but should be checked before overwriting existing values

        Net.HttpRequest http = buildStoreDataRequest(fileId, false,
                Long.toString(progressValue) + "\n" + new String(Base64Coder.encode(gameState)));

        Gdx.net.sendHttpRequest(http, new Net.HttpResponseListener() {
            @Override
            public void handleHttpResponse(Net.HttpResponse httpResponse) {
                String json = httpResponse.getResultAsString();
                boolean success = parseSuccessFromResponse(json);

                if (!success)
                    Gdx.app.error(GAMESERVICE_ID, "Error saving gamestate: " + json);

                if (listener != null)
                    listener.onGameStateSaved(success, null);
            }

            @Override
            public void failed(Throwable t) {
                Gdx.app.error(GAMESERVICE_ID, "Error saving gamestate", t);
                if (listener != null)
                    listener.onGameStateSaved(false, null);
            }

            @Override
            public void cancelled() {
                Gdx.app.error(GAMESERVICE_ID, "Error saving gamestate: Cancelled");
                if (listener != null)
                    listener.onGameStateSaved(false, null);
            }
        });
    }

    /**
     * Helper method when just interested if GameJolt request was successful
     */
    protected boolean parseSuccessFromResponse(String json) {
        JsonValue response = null;
        boolean success;
        try {
            response = new JsonReader().parse(json).get("response");
            success = response != null && response.getBoolean("success");
        } catch (Throwable t) {
            Gdx.app.error(GAMESERVICE_ID, "Cannot parse GameJolt response: " + json, t);
            success = false;
        }
        return success;
    }

    @Override
    public boolean deleteGameState(String fileId, final ISaveGameStateResponseListener successListener) {
        if (!isSessionActive())
            return false;

        Map<String, String> params = new HashMap<String, String>();

        addGameIDUserNameUserToken(params);
        params.put("key", fileId);

        final Net.HttpRequest http = buildJsonRequest("data-store/remove/", params);
        if (http == null)
            return false;

        Gdx.net.sendHttpRequest(http, new Net.HttpResponseListener() {
            @Override
            public void handleHttpResponse(Net.HttpResponse httpResponse) {
                String json = httpResponse.getResultAsString();
                boolean success = parseSuccessFromResponse(json);

                // just log, no error because deleting a nonexistant gamestate fails but is no error
                if (!success)
                    Gdx.app.log(GAMESERVICE_ID, "Failed to delete gamestate: " + json);

                if (successListener != null)
                    successListener.onGameStateSaved(success, null);
            }

            @Override
            public void failed(Throwable t) {
                Gdx.app.error(GAMESERVICE_ID, "Error deleting gamestate", t);
                if (successListener != null)
                    successListener.onGameStateSaved(false, null);
            }

            @Override
            public void cancelled() {
                Gdx.app.error(GAMESERVICE_ID, "Error deleting gamestate: Cancelled");
                if (successListener != null)
                    successListener.onGameStateSaved(false, null);
            }
        });

        return true;
    }

    @Override
    public boolean fetchGameStates(final IFetchGameStatesListResponseListener callback) {
        if (!isSessionActive())
            return false;

        Map<String, String> params = new HashMap<String, String>();

        addGameIDUserNameUserToken(params);

        final Net.HttpRequest http = buildJsonRequest("data-store/get-keys/", params);
        if (http == null)
            return false;

        Gdx.net.sendHttpRequest(http, new Net.HttpResponseListener() {
            @Override
            public void handleHttpResponse(Net.HttpResponse httpResponse) {
                JsonValue response = null;
                String json = httpResponse.getResultAsString();
                try {
                    response = new JsonReader().parse(json).get("response");

                    if (response == null || !response.getBoolean("success")) {
                        Gdx.app.error(GAMESERVICE_ID, "Could not parse answer from GameJolt: " + json);
                        callback.onFetchGameStatesListResponse(null);
                    } else {

                        JsonValue keysObj = response.get("keys");
                        Array<String> keysArr = new Array<String>();
                        for (JsonValue keyObj = keysObj.child; keyObj != null; keyObj = keyObj.next) {
                            keysArr.add(keyObj.getString("key"));
                        }
                        callback.onFetchGameStatesListResponse(keysArr);
                    }
                } catch (Throwable t) {
                    Gdx.app.error(GAMESERVICE_ID, "Could not parse answer from GameJolt: " + json, t);
                    callback.onFetchGameStatesListResponse(null);
                }
            }

            @Override
            public void failed(Throwable t) {
                Gdx.app.error(GAMESERVICE_ID, "Error fetching gamestates", t);
                callback.onFetchGameStatesListResponse(null);
            }

            @Override
            public void cancelled() {
                Gdx.app.error(GAMESERVICE_ID, "Error fetching gamestates: Cancelled");
                callback.onFetchGameStatesListResponse(null);
            }
        });

        return true;
    }

    @Override
    public boolean isFeatureSupported(GameServiceFeature feature) {
        switch (feature) {
            case GameStateStorage:
            case GameStateMultipleFiles:
            case SubmitEvents:
            case FetchLeaderBoardEntries:
            case FetchAchievements:
            case GameStateDelete:
            case FetchGameStates:
            case PlayerLogOut:
                return true;
            default:
                return false;
        }
    }

    /**
     * content must be without special chars ampersand or question mark - use Base64 when not sure!
     */
    protected Net.HttpRequest buildStoreDataRequest(String dataKey, boolean globalKey, String content) {
        Map<String, String> params = new HashMap<String, String>();

        if (globalKey)
            params.put("game_id", gjAppId);
        else
            addGameIDUserNameUserToken(params);
        params.put("key", dataKey);

        final Net.HttpRequest http = buildJsonRequest("data-store/set/", params);
        if (http == null)
            return null;

        http.setMethod(Net.HttpMethods.POST);
        http.setHeader("Content-Type", "application/x-www-form-urlencoded");
        http.setContent("data=" + content);

        return http;
    }

    @Override
    public void loadGameState(String fileId, final ILoadGameStateResponseListener listener) {
        if (!isSessionActive()) {
            listener.gsGameStateLoaded(null);
            return;
        }

        Net.HttpRequest http = buildLoadDataRequest(fileId, false);

        Gdx.net.sendHttpRequest(http, new Net.HttpResponseListener() {
            @Override
            public void handleHttpResponse(Net.HttpResponse httpResponse) {
                String response = httpResponse.getResultAsString();

                if (response == null || !response.startsWith("SUCCESS")) {
                    // just log, no error because loading a nonexistant gamestate fails but is no error
                    Gdx.app.log(GAMESERVICE_ID, "Gamestate load failed: " + response);
                    listener.gsGameStateLoaded(null);
                } else {
                    // indexOf is twice to cut first two lines. First one is success message,
                    // second one is progressValue
                    byte[] gs = Base64Coder.decode(response.substring(
                            response.indexOf('\n', response.indexOf('\n') + 1) + 1));
                    listener.gsGameStateLoaded(gs);
                }
            }

            @Override
            public void failed(Throwable t) {
                Gdx.app.error(GAMESERVICE_ID, "Gamestate load failed", t);
                listener.gsGameStateLoaded(null);
            }

            @Override
            public void cancelled() {
                Gdx.app.error(GAMESERVICE_ID, "Gamestate load cancelled");

                listener.gsGameStateLoaded(null);
            }
        });
    }

    /**
     * Load data is done with dump format
     */
    protected Net.HttpRequest buildLoadDataRequest(String dataKey, boolean globalKey) {
        Map<String, String> params = new HashMap<String, String>();

        if (globalKey)
            params.put("game_id", gjAppId);
        else
            addGameIDUserNameUserToken(params);
        params.put("key", dataKey);

        final Net.HttpRequest http = buildRequest("data-store/?format=dump&", params);

        return http;

    }

    protected void addGameIDUserNameUserToken(Map<String, String> params) {
        params.put("game_id", String.valueOf(gjAppId));
        params.put("username", userName);
        params.put("user_token", userToken);
    }

    protected /* @Nullable */ Net.HttpRequest buildJsonRequest(String component, Map<String, String> params) {
        component = component + "?format=json&";
        return buildRequest(component, params);
    }

    protected Net.HttpRequest buildRequest(String component, Map<String, String> params) {
        String request = GJ_GATEWAY + component;
        request += HttpParametersUtils.convertHttpParameters(params);

        /* Generate signature */
        final String signature;
        try {
            signature = md5(request + gjAppPrivateKey);
        } catch (Exception e) {
            /* Do not leak 'gamePrivateKey' in log */
            Gdx.app.error(GAMESERVICE_ID, "Cannot honor request: " + request, e);
            return null;
        }
        /* Append signature */
        String complete = request;
        complete += "&";
        complete += "signature";
        complete += "=";
        complete += signature;

        final Net.HttpRequest http = new Net.HttpRequest();
        http.setMethod(Net.HttpMethods.GET);
        http.setUrl(complete);

        return http;
    }

    protected String md5(String s) throws UnsupportedEncodingException, NoSuchAlgorithmException {
        final MessageDigest md = MessageDigest.getInstance("MD5");
        final byte[] bytes = s.getBytes("UTF-8");
        final byte[] digest = md.digest(bytes);
        /**
         * Magic to convert it into an String: an answer at the bottom of:
         *
         * <pre>
         * http://stackoverflow.com/questions/415953/how-can-i-generate-an-md5-hash
         * </pre>
         */
        final StringBuffer sb = new StringBuffer();
        for (int i = 0; i < digest.length; ++i) {
            sb.append(Integer.toHexString((digest[i] & 0xFF) | 0x100).substring(1, 3));
        }
        return sb.toString();
    }

    protected static class NoOpResponseListener implements Net.HttpResponseListener {
        @Override
        public void handleHttpResponse(Net.HttpResponse httpResponse) {
            Gdx.app.debug(GAMESERVICE_ID, httpResponse.getResultAsString());
        }

        @Override
        public void failed(Throwable t) {
            Gdx.app.log(GAMESERVICE_ID, t.getMessage(), t);
        }

        @Override
        public void cancelled() {

        }
    }
}