package xyz.luan.games.play.playgames;

import java.io.ByteArrayOutputStream;
import java.util.Map;
import java.util.HashMap;

import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.PluginRegistry.Registrar;
import io.flutter.plugin.common.PluginRegistry.ActivityResultListener;

import android.content.Context;
import android.net.Uri;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.Log;

import androidx.annotation.NonNull;

import com.google.android.gms.common.images.ImageManager;
import com.google.android.gms.games.AnnotatedData;

import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
import com.google.android.gms.games.SnapshotsClient;
import com.google.android.gms.games.snapshot.Snapshot;
import com.google.android.gms.games.snapshot.SnapshotMetadata;
import com.google.android.gms.games.snapshot.SnapshotMetadataChange;
import com.google.android.gms.games.LeaderboardsClient;
import com.google.android.gms.games.leaderboard.LeaderboardVariant;
import com.google.android.gms.games.leaderboard.ScoreSubmissionData;
import com.google.android.gms.games.leaderboard.LeaderboardScore;

import com.google.android.gms.games.Games;
import com.google.android.gms.games.GamesClient;
import com.google.android.gms.games.Player;
import com.google.android.gms.games.PlayersClient;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;

public class Request {

    private static final String TAG = Request.class.getCanonicalName();

    private GoogleSignInAccount currentAccount;
    private Registrar registrar;
    private MethodCall call;
    private Result result;
    private PlayGamesPlugin pluginRef;

    public Request(PlayGamesPlugin pluginRef, GoogleSignInAccount currentAccount, Registrar registrar, MethodCall call, Result result) {
        this.pluginRef = pluginRef;
        this.currentAccount = currentAccount;
        this.registrar = registrar;
        this.call = call;
        this.result = result;
    }

    public void handle() {
        if (currentAccount == null) {
            error("ERROR_NOT_SIGNED_IN", "You must be signed in to perform any other action.");
            return;
        }
        if (call.method.equals("getHiResImage")) {
            this.getHiResImage();
        } else if (call.method.equals("getIconImage")) {
            this.getIconImage();
        } else if (call.method.equals("unlockAchievementById")) {
            String id = call.argument("id");
            this.unlockAchievement(id);
        } else if (call.method.equals("unlockAchievementByName")) {
            String name = call.argument("name");
            this.unlockAchievement(getIdByName(name));
        } else if (call.method.equals("incrementAchievementById")) {
            String id = call.argument("id");
            int amount = call.argument("amount");
            this.incrementAchievement(id, amount);
        } else if (call.method.equals("incrementAchievementByName")) {
            String name = call.argument("name");
            int amount = call.argument("amount");
            this.incrementAchievement(getIdByName(name), amount);
        } else if (call.method.equals("setPopupOptions")) {
            boolean show = call.argument("show");
            int gravity = call.argument("gravity");
            this.setPopupOptions(show, gravity);
        } else if (call.method.equals("openSnapshot")) {
            String snapshotName = call.argument("snapshotName");
            this.openSnapshot(snapshotName);
        } else if (call.method.equals("saveSnapshot")) {
            String snapshotName = call.argument("snapshotName");
            String content = call.argument("content");
            Map<String, String> metadata = call.argument("metadata");
            this.saveSnapshot(snapshotName, content, metadata);
        } else if (call.method.equals("resolveSnapshotConflict")) {
            String snapshotName = call.argument("snapshotName");
            String conflictId = call.argument("conflictId");
            String content = call.argument("content");
            Map<String, String> metadata = call.argument("metadata");
            this.resolveSnapshotConflict(snapshotName, conflictId, content, metadata);
        } else if (call.method.equals("submitScoreByName")) {
            String leaderboardName = call.argument("leaderboardName");
            Long score = ((Number) call.argument("score")).longValue();
            this.submitScore(this.getIdByName(leaderboardName), score);
        } else if (call.method.equals("submitScoreById")) {
            String leaderboardId = call.argument("leaderboardId");
            Long score = ((Number) call.argument("score")).longValue();
            this.submitScore(leaderboardId, score);
        } else if (call.method.equals("loadPlayerCenteredScoresByName")) {
            String leaderboardName = call.argument("leaderboardName");
            String timeSpan = call.argument("timeSpan");
            String collectionType = call.argument("collectionType");
            int maxResults = call.argument("maxResults");
            boolean forceReload = call.argument("forceReload");
            this.loadPlayerCenteredScores(this.getIdByName(leaderboardName), timeSpan, collectionType, maxResults, forceReload);
        } else if (call.method.equals("loadPlayerCenteredScoresById")) {
            String leaderboardId = call.argument("leaderboardId");
            String timeSpan = call.argument("timeSpan");
            String collectionType = call.argument("collectionType");
            int maxResults = call.argument("maxResults");
            boolean forceReload = call.argument("forceReload");
            this.loadPlayerCenteredScores(leaderboardId, timeSpan, collectionType, maxResults, forceReload);
        } else if (call.method.equals("loadTopScoresByName")) {
            String leaderboardName = call.argument("leaderboardName");
            String timeSpan = call.argument("timeSpan");
            String collectionType = call.argument("collectionType");
            int maxResults = call.argument("maxResults");
            boolean forceReload = call.argument("forceReload");
            this.loadTopScores(this.getIdByName(leaderboardName), timeSpan, collectionType, maxResults, forceReload);
        } else if (call.method.equals("loadTopScoresById")) {
            String leaderboardId = call.argument("leaderboardId");
            String timeSpan = call.argument("timeSpan");
            String collectionType = call.argument("collectionType");
            int maxResults = call.argument("maxResults");
            boolean forceReload = call.argument("forceReload");
            this.loadTopScores(leaderboardId, timeSpan, collectionType, maxResults, forceReload);
        } else {
            result.notImplemented();
        }
    }

    private String getIdByName(String name) {
        Context ctx = registrar.context();
        int resId = ctx.getResources().getIdentifier(name, "string", ctx.getPackageName());
        return ctx.getString(resId);
    }

    public void unlockAchievement(String id) {
        Games.getAchievementsClient(registrar.activity(), currentAccount).unlockImmediate(id)
                .addOnSuccessListener(new OnSuccessListener<Void>() {
                    @Override
                    public void onSuccess(Void a) {
                        result(true);
                    }
                }).addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                Log.e(TAG, "Could not unlock achievement", e);
                result(false);
            }
        });
    }

    public void incrementAchievement(String id, int amount) {
        Games.getAchievementsClient(registrar.activity(), currentAccount).incrementImmediate(id, amount)
                .addOnSuccessListener(new OnSuccessListener<Boolean>() {
                    @Override
                    public void onSuccess(Boolean b) {
                        result(true);
                    }
                }).addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                Log.e(TAG, "Could not increment achievement", e);
                result(false);
            }
        });
    }

    public void setPopupOptions(boolean show, int gravity) {
        GamesClient gamesClient = Games.getGamesClient(registrar.activity(), currentAccount);
        if (show) {
            gamesClient.setViewForPopups(registrar.view());
            gamesClient.setGravityForPopups(gravity);
        } else {
            gamesClient.setViewForPopups(null);
        }
        result(true);
    }

    private void result(Object response) {
        result.success(response);
    }

    private void error(String type, Throwable e) {
        Log.e(TAG, "Unexpected error on " + type, e);
        error(type, e.getMessage());
    }

    private void error(String type, String message) {
        Map<String, Object> errorMap = new HashMap<>();
        errorMap.put("type", type);
        errorMap.put("message", message);
        result(errorMap);
    }

    public void getHiResImage() {
        PlayersClient playersClient = Games.getPlayersClient(registrar.activity(), currentAccount);
        playersClient.getCurrentPlayer().addOnSuccessListener(new OnSuccessListener<Player>() {
            @Override
            public void onSuccess(Player player) {
                readImage(player.getHiResImageUri());
            }
        });
    }

    public void getIconImage() {
        PlayersClient playersClient = Games.getPlayersClient(registrar.activity(), currentAccount);
        playersClient.getCurrentPlayer().addOnSuccessListener(new OnSuccessListener<Player>() {
            @Override
            public void onSuccess(Player player) {
                readImage(player.getHiResImageUri());
            }
        });
    }

    private void readImage(final Uri uri) {
        final Map<String, Object> data = new HashMap<>();
        if (uri == null) {
            data.put("bytes", null);
            result(data);
        }
        ImageManager manager = ImageManager.create(registrar.context());
        manager.loadImage(new ImageManager.OnImageLoadedListener() {
            @Override
            public void onImageLoaded(Uri uri1, Drawable drawable, boolean isRequestedDrawable) {
                Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
                ByteArrayOutputStream stream = new ByteArrayOutputStream();
                bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream);
                byte[] bytes = stream.toByteArray();
                data.put("bytes", bytes);
                result(data);
            }
        }, uri);
    }

    private OnSuccessListener<SnapshotsClient.DataOrConflict<Snapshot>> generateCallback(final String snapshotName) {
        return new OnSuccessListener<SnapshotsClient.DataOrConflict<Snapshot>>() {
            @Override
            public void onSuccess(SnapshotsClient.DataOrConflict<Snapshot> i) {
                if (i.isConflict()) {
                    try {
                        String conflictId = i.getConflict().getConflictId();
                        Map<String, Object> errorMap = new HashMap<>();
                        errorMap.put("type", "SNAPSHOT_CONFLICT");
                        errorMap.put("message", "There was a conflict while opening snapshot, call resolveConflict to solve it.");
                        errorMap.put("conflictId", conflictId);
                        Snapshot snapshot = i.getConflict().getSnapshot();
                        pluginRef.getLoadedSnapshot().put(snapshotName, snapshot);
                        errorMap.put("local", snapshotToMap(i.getConflict().getConflictingSnapshot()));
                        errorMap.put("server", snapshotToMap(snapshot));
                        result(errorMap);
                    } catch (IOException e) {
                        error("SNAPSHOT_CONTENT_READ_ERROR", e);
                    }
                } else {
                    pluginRef.getLoadedSnapshot().put(snapshotName, i.getData());
                    try {
                        result(snapshotToMap(i.getData()));
                    } catch (IOException e) {
                        error("SNAPSHOT_CONTENT_READ_ERROR", e);
                    }
                }
            }
        };
    }

    public void openSnapshot(String snapshotName) {
        SnapshotsClient snapshotsClient = Games.getSnapshotsClient(this.registrar.activity(), currentAccount);
        snapshotsClient.open(snapshotName, true).addOnSuccessListener(generateCallback(snapshotName)).addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                Log.e(TAG, "Could not open snapshot", e);
                error("SNAPSHOT_FAILURE", e);
            }
        });
    }

    private boolean hasOnlyAllowedKeys(Map<String, ?> map, String... keys) {
        Set<String> _keys = new HashSet<>(Arrays.asList(keys));
        for (String key : map.keySet()) {
            if (!_keys.contains(key)) {
                error("INVALID_METADATA_SET", "You can not set the metadata " + key + "; only " + _keys + " are allowed.");
                return false;
            }
        }
        return true;
    }

    public void saveSnapshot(String snapshotName, String content, Map<String, String> metadata) {
        if (!hasOnlyAllowedKeys(metadata, "description")) {
            return;
        }
        SnapshotsClient snapshotsClient = Games.getSnapshotsClient(this.registrar.activity(), currentAccount);
        Snapshot snapshot = pluginRef.getLoadedSnapshot().get(snapshotName);
        if (snapshot == null) {
            error("SNAPSHOT_NOT_OPENED", "The snapshot with name " + snapshotName + " was not opened before.");
            return;
        }
        snapshot.getSnapshotContents().writeBytes(content.getBytes());
        SnapshotMetadataChange metadataChange = new SnapshotMetadataChange.Builder()
                .setDescription(metadata.get("description"))
                .build();
        snapshotsClient.commitAndClose(snapshot, metadataChange).addOnSuccessListener(new OnSuccessListener<SnapshotMetadata>() {
            @Override
            public void onSuccess(SnapshotMetadata snapshotMetadata) {
                Map<String, Object> result = new HashMap<>();
                result.put("status", true);
                result(result);
            }
        }).addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                error("SAVE_SNAPSHOT_ERROR", e);
            }
        });
    }

    public void resolveSnapshotConflict(String snapshotName, String conflictId, String content, Map<String, String> metadata) {
        if (!hasOnlyAllowedKeys(metadata, "description")) {
            return;
        }
        SnapshotsClient snapshotsClient = Games.getSnapshotsClient(this.registrar.activity(), currentAccount);
        Snapshot snapshot = pluginRef.getLoadedSnapshot().get(snapshotName);
        if (snapshot == null) {
            error("SNAPSHOT_NOT_OPENED", "The snapshot with name " + snapshotName + " was not opened before.");
            return;
        }
        snapshot.getSnapshotContents().writeBytes(content.getBytes());
        SnapshotMetadataChange metadataChange = new SnapshotMetadataChange.Builder()
                .setDescription(metadata.get("description"))
                .build();

        snapshotsClient.resolveConflict(conflictId, snapshot.getMetadata().getSnapshotId(), metadataChange, snapshot.getSnapshotContents()).addOnSuccessListener(generateCallback(snapshotName)).addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                error("RESOLVE_SNAPSHOT_CONFLICT_ERROR", e);
            }
        });
    }

    private Map<String, Object> snapshotToMap(Snapshot snapshot) throws IOException {
        String blob = new String(snapshot.getSnapshotContents().readFully());
        Map<String, Object> metadata = new HashMap<>();
        metadata.put("title", snapshot.getMetadata().getTitle());
        metadata.put("description", snapshot.getMetadata().getDescription());
        metadata.put("deviceName", snapshot.getMetadata().getDeviceName());
        metadata.put("snapshotId", snapshot.getMetadata().getSnapshotId());
        metadata.put("uniqueName", snapshot.getMetadata().getUniqueName());
        metadata.put("coverImageAspectRatio", snapshot.getMetadata().getCoverImageAspectRatio());
        metadata.put("coverImageUri", snapshot.getMetadata().getCoverImageUri() == null ? null : snapshot.getMetadata().getCoverImageUri().toString());
        metadata.put("lastModifiedTimestamp", snapshot.getMetadata().getLastModifiedTimestamp());

        Map<String, Object> data = new HashMap<>();
        data.put("content", blob);
        data.put("metadata", metadata);

        return data;
    }

    public void submitScore(String leaderboardId, Long score) {
        Log.i(TAG, "Submitting leaderboard, id: " + leaderboardId + "; score: " + score);
        LeaderboardsClient leaderboardsClient = Games.getLeaderboardsClient(this.registrar.activity(), currentAccount);
        leaderboardsClient.submitScoreImmediate(leaderboardId, score).addOnSuccessListener(new OnSuccessListener<ScoreSubmissionData>() {
            @Override
            public void onSuccess(ScoreSubmissionData data) {
                Map<String, Object> successMap = new HashMap<>();
                successMap.put("type", "SUCCESS");
                successMap.put("leaderboardId", data.getLeaderboardId());
                successMap.put("playerId", data.getPlayerId());
                successMap.put("scoreResultDaily", resultToMap(data.getScoreResult(LeaderboardVariant.TIME_SPAN_DAILY)));
                successMap.put("scoreResultWeekly", resultToMap(data.getScoreResult(LeaderboardVariant.TIME_SPAN_WEEKLY)));
                successMap.put("scoreResultAllTime", resultToMap(data.getScoreResult(LeaderboardVariant.TIME_SPAN_ALL_TIME)));
                result(successMap);
            }
        }).addOnFailureListener(new OnFailureListener() {
            @Override
            public void onFailure(@NonNull Exception e) {
                Log.e(TAG, "Could not submit leadderboard", e);
                error("LEADERBOARD_SUBMIT_FAILURE", e);
            }
        });
    }

    public void loadPlayerCenteredScores(String leaderboardId, String timeSpan, String collectionType, int maxResults, boolean forceReload) {
        LeaderboardsClient leaderboardsClient = Games.getLeaderboardsClient(this.registrar.activity(), currentAccount);
        leaderboardsClient.loadPlayerCenteredScores(leaderboardId, convertTimeSpan(timeSpan), convertCollection(collectionType), maxResults, forceReload)
                .addOnSuccessListener(scoreSuccessHandler())
                .addOnFailureListener(new OnFailureListener() {
                                          @Override
                                          public void onFailure(@NonNull Exception e) {
                                              Log.e(TAG, "Could not fetch leaderboard player centered (retrieve failure)", e);
                                              error("LEADERBOARD_PLAYER_CENTERED_FAILURE", e);
                                          }
                                      }
                );
    }

    public void loadTopScores(String leaderboardId, String timeSpan, String collectionType, int maxResults, boolean forceReload) {
        LeaderboardsClient leaderboardsClient = Games.getLeaderboardsClient(this.registrar.activity(), currentAccount);
        leaderboardsClient.loadTopScores(leaderboardId, convertTimeSpan(timeSpan), convertCollection(collectionType), maxResults, forceReload)
                .addOnSuccessListener(scoreSuccessHandler())
                .addOnFailureListener(new OnFailureListener() {
                                          @Override
                                          public void onFailure(@NonNull Exception e) {
                                              Log.e(TAG, "Could not fetch leaderboard top scores (retrieve failure)", e);
                                              error("LEADERBOARD_TOP_SCORES_FAILURE", e);
                                          }
                                      }
                );
    }

    private OnSuccessListener<AnnotatedData<LeaderboardsClient.LeaderboardScores>> scoreSuccessHandler() {
        return new OnSuccessListener<AnnotatedData<LeaderboardsClient.LeaderboardScores>>() {
            @Override
            public void onSuccess(AnnotatedData<LeaderboardsClient.LeaderboardScores> data) {
                LeaderboardsClient.LeaderboardScores scores = data.get();
                Map<String, Object> successMap = new HashMap<>();
                successMap.put("type", "SUCCESS");
                successMap.put("leaderboardDisplayName", scores.getLeaderboard().getDisplayName());
                List<Map<String, Object>> list = new ArrayList<>();
                Iterator<LeaderboardScore> it = scores.getScores().iterator();
                while (it.hasNext()) {
                    list.add(scoreToMap(it.next()));
                }
                successMap.put("scores", list);

                scores.release();
                result(successMap);
            }
        };
    }

    private int convertTimeSpan(String timeSpan) {
        switch (timeSpan) {
            case "TimeSpan.TIME_SPAN_DAILY":
                return LeaderboardVariant.TIME_SPAN_DAILY;
            case "TimeSpan.TIME_SPAN_WEEKLY":
                return LeaderboardVariant.TIME_SPAN_WEEKLY;
            case "TimeSpan.TIME_SPAN_ALL_TIME":
                return LeaderboardVariant.TIME_SPAN_ALL_TIME;
        }
        throw new RuntimeException("Unknown time span...");
    }

    private int convertCollection(String collection) {
        if ("CollectionType.COLLECTION_PUBLIC".equals(collection)) {
            return LeaderboardVariant.COLLECTION_PUBLIC;
        }
        throw new RuntimeException("Unknown time span...");
    }

    private Map<String, Object> resultToMap(ScoreSubmissionData.Result result) {
        Map<String, Object> data = new HashMap<>();
        data.put("formattedScore", result.formattedScore);
        data.put("newBest", result.newBest);
        data.put("rawScore", result.rawScore);
        data.put("scoreTag", result.scoreTag);
        return data;
    }

    private Map<String, Object> scoreToMap(LeaderboardScore score) {
        Map<String, Object> data = new HashMap<>();

        data.put("displayRank", score.getDisplayRank());
        data.put("displayScore", score.getDisplayScore());
        data.put("rank", score.getRank());
        data.put("rawScore", score.getRawScore());
        data.put("scoreTag", score.getScoreTag());
        data.put("timestampMillis", score.getTimestampMillis());
        data.put("scoreHolderDisplayName", score.getScoreHolderDisplayName());

        return data;
    }
}