package net.steppschuh.datalogger.messaging;

import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.PendingResult;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.wearable.MessageApi;
import com.google.android.gms.wearable.Node;
import com.google.android.gms.wearable.NodeApi;
import com.google.android.gms.wearable.Wearable;

import android.os.Build;
import android.os.Bundle;
import android.os.Message;
import android.support.annotation.NonNull;
import android.util.Log;

import net.steppschuh.datalogger.MobileApp;
import net.steppschuh.datalogger.messaging.handler.MessageHandler;
import net.steppschuh.datalogger.status.GoogleApiStatus;
import net.steppschuh.datalogger.status.Status;
import net.steppschuh.datalogger.status.StatusUpdateEmitter;
import net.steppschuh.datalogger.status.StatusUpdateHandler;
import net.steppschuh.datalogger.status.StatusUpdateReceiver;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

public class GoogleApiMessenger implements GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener, StatusUpdateEmitter {

    private static final String TAG = GoogleApiMessenger.class.getSimpleName();
    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
    public static final String DEFAULT_NODE_ID = "LOCAL_NODE";

    private GoogleApiStatus status = new GoogleApiStatus();
    private StatusUpdateHandler statusUpdateHandler;

    private MobileApp app;
    private GoogleApiClient googleApiClient;
    private boolean wearableApiAvailable;

    public GoogleApiMessenger(MobileApp app) {
        this.app = app;
        initialize(app);
    }

    private void initialize(MobileApp app) {
        Log.d(TAG, "Initializing Google API client");
        googleApiClient = new GoogleApiClient.Builder(app)
                .addConnectionCallbacks(this)
                .addApi(Wearable.API)
                .build();
        updateLocalNode();
        setupStatusUpdates();
    }

    public void setupStatusUpdates() {
        statusUpdateHandler = new StatusUpdateHandler();
        statusUpdateHandler.registerStatusUpdateReceiver(new StatusUpdateReceiver() {
            @Override
            public void onStatusUpdated(Status status) {
                app.getStatus().setGoogleApiStatus((GoogleApiStatus) status);
                app.getStatus().updated(app.getStatusUpdateHandler());

                // update node reachabilities
                app.getReachabilityChecker().checkReachabilities(null);
            }
        });
    }

    public boolean connect() {
        Log.d(TAG, "Connecting Google API client");
        if (googleApiClient != null && !googleApiClient.isConnected()) {
            googleApiClient.connect();
            return true;
        }
        return false;
    }

    public boolean disconnect() {
        Log.d(TAG, "Disconnecting Google API client");
        status.setLastConnectedNodes(new ArrayList<Node>());
        if (googleApiClient != null && googleApiClient.isConnected()) {
            googleApiClient.disconnect();
            return true;
        }
        return false;
    }

    @Override
    public void onConnected(Bundle bundle) {
        Log.d(TAG, "Connected");
        updateLastConnectedNodes();
        status.setConnected(true);
        status.updated(statusUpdateHandler);
    }

    @Override
    public void onConnectionSuspended(int i) {
        Log.d(TAG, "Connection suspended");
        updateLastConnectedNodes();
        status.setConnected(false);
        status.updated(statusUpdateHandler);
    }

    @Override
    public void onConnectionFailed(ConnectionResult connectionResult) {
        if (connectionResult.getErrorCode() == ConnectionResult.API_UNAVAILABLE) {
            wearableApiAvailable = false;
        }
        status.setConnected(false);
        status.updated(statusUpdateHandler);
        Log.e(TAG, "Google API client connection failed: " + connectionResult.getErrorMessage());
    }

    public void updateLocalNode() {
        PendingResult<NodeApi.GetLocalNodeResult> pendingResult = Wearable.NodeApi.getLocalNode(googleApiClient);
        pendingResult.setResultCallback(new ResultCallback<NodeApi.GetLocalNodeResult>() {
            @Override
            public void onResult(@NonNull NodeApi.GetLocalNodeResult getLocalNodeResult) {
                status.setLocalNode(getLocalNodeResult.getNode());
                status.updated(statusUpdateHandler);
                wearableApiAvailable = getLocalNodeResult.getNode() != null;
            }
        });
    }

    public void updateLastConnectedNodes() {
        getConnectedNodes(new ResultCallback<NodeApi.GetConnectedNodesResult>() {
            @Override
            public void onResult(@NonNull NodeApi.GetConnectedNodesResult getConnectedNodesResult) {
                status.setLastConnectedNodes(getConnectedNodesResult.getNodes());
                status.setLastConnectedNodesUpdateTimestamp(System.currentTimeMillis());
                status.updated(statusUpdateHandler);

                StringBuilder sb = new StringBuilder();
                sb.append("Connected Nodes:");
                for (Node connectedNode : status.getLastConnectedNodes()) {
                    sb.append("\n - ").append(connectedNode.getDisplayName());
                    sb.append(": ").append(connectedNode.getId());
                    if (connectedNode.isNearby()) {
                        sb.append(" (nearby)");
                    }
                }
                Log.v(TAG, sb.toString());
            }
        });
    }

    public PendingResult<NodeApi.GetConnectedNodesResult> getConnectedNodes(ResultCallback<NodeApi.GetConnectedNodesResult> callback) {
        PendingResult<NodeApi.GetConnectedNodesResult> pendingResult = Wearable.NodeApi.getConnectedNodes(googleApiClient);
        pendingResult.setResultCallback(callback);
        return pendingResult;
    }

    public String getNodeName(String id) {
        Node node = getLastConnectedNodeById(id);
        if (node != null) {
            return node.getDisplayName();
        } else {
            if (id == null || id.equals(DEFAULT_NODE_ID)) {
                return Build.MODEL;
            } else {
                return id;
            }
        }
    }

    public Node getLastConnectedNodeById(String id) {
        if (id == null || id.equals(DEFAULT_NODE_ID)) {
            return null;
        }
        String localNodeId = getLocalNodeId();
        if (localNodeId != null && localNodeId.equals(id)) {
            return status.getLocalNode();
        }
        return getNodeById(id, status.getLastConnectedNodes());
    }

    public List<Node> getLastConnectedNodes() {
        if (status != null) {
            return status.getLastConnectedNodes();
        }
        return new ArrayList<>();
    }

    public List<Node> getLastConnectedNearbyNodes() {
        if (status != null) {
            return getNearbyNodes(status.getLastConnectedNodes());
        }
        return new ArrayList<>();
    }

    public static Node getNodeById(String id, List<Node> nodes) {
        for (Node recentNode : nodes) {
            if (recentNode.getId().equals(id)) {
                return recentNode;
            }
        }
        return null;
    }

    public void sendMessageToNearbyNodes(final String path, final String data) {
        sendMessageToNearbyNodes(path, data.getBytes(DEFAULT_CHARSET));
    }

    public void sendMessageToNearbyNodes(final String path, final byte[] data) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                NodeApi.GetConnectedNodesResult getConnectedNodesResult = Wearable.NodeApi.getConnectedNodes(googleApiClient).await();
                status.setLastConnectedNodes(getConnectedNodesResult.getNodes());
                status.setLastConnectedNodesUpdateTimestamp(System.currentTimeMillis());
                for (Node node : status.getLastConnectedNodes()) {
                    try {
                        if (!node.isNearby()) {
                            continue;
                        }
                        sendMessageToNodeWithResult(path, data, node.getId());
                    } catch (Exception ex) {
                        Log.w(TAG, "Unable to send message to node: " + ex.getMessage());
                    }
                }
            }
        }).start();
    }

    public void sendMessageToNode(final String path, final String data, final String nodeId) {
        sendMessageToNode(path, data.getBytes(DEFAULT_CHARSET), nodeId);
    }

    public void sendMessageToNode(final String path, final byte[] data, final String nodeId) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                if (nodeId == null || nodeId.equals(DEFAULT_NODE_ID) || nodeId.equals(getLocalNodeId())) {
                    sendMessageToLocalNode(path, data, nodeId);
                } else {
                    try {
                        sendMessageToNodeWithResult(path, data, nodeId);
                    } catch (Exception ex) {
                        Log.w(TAG, "Unable to send message to node: " + nodeId + ": " + ex.getMessage());
                        ex.printStackTrace();
                    }
                }
            }
        }).start();
    }

    public MessageApi.SendMessageResult sendMessageToNodeWithResult(final String path, final byte[] data, String nodeId) throws Exception {
        if (!googleApiClient.isConnected()) {
            throw new Exception("Google API client is not connected");
        }
        if (nodeId == null) {
            throw new Exception("Node Id is not set");
        }
        return Wearable.MessageApi.sendMessage(googleApiClient, nodeId, path, data).await();
    }

    public void sendMessageToLocalNode(final String path, final byte[] data, String nodeId) {
        try {
            if (nodeId == null) {
                nodeId = DEFAULT_NODE_ID;
            }
            Bundle bundle = new Bundle();
            bundle.putString(MessageHandler.KEY_PATH, path);
            bundle.putString(MessageHandler.KEY_SOURCE_NODE_ID, nodeId);
            bundle.putByteArray(MessageHandler.KEY_DATA, data);

            Message message = new Message();
            message.setData(bundle);

            app.notifyMessageHandlers(message);
        } catch (Exception ex) {
            Log.w(TAG, "Unable to send message to local node: " + ex.getMessage());
            ex.printStackTrace();
        }
    }

    public GoogleApiClient getGoogleApiClient() {
        return googleApiClient;
    }

    public void setGoogleApiClient(GoogleApiClient googleApiClient) {
        this.googleApiClient = googleApiClient;
    }

    public String getLocalNodeId() {
        if (status != null && status.getLocalNode() != null) {
            return status.getLocalNode().getId();
        }
        return DEFAULT_NODE_ID;
    }

    public static List<Node> getNearbyNodes(List<Node> nodes) {
        List<Node> nearbyNodes = new ArrayList<>();
        for (Node node : nodes) {
            if (node.isNearby()) {
                nearbyNodes.add(node);
            }
        }
        return nearbyNodes;
    }

    @Override
    public Status getStatus() {
        return status;
    }

    @Override
    public StatusUpdateHandler getStatusUpdateHandler() {
        return statusUpdateHandler;
    }

    public boolean isWearableApiAvailable() {
        return wearableApiAvailable;
    }
}