/*
 * Copyright 2018 Google LLC. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.ar.sceneform.samples.hellosceneform;

import android.os.StrictMode;
import android.support.annotation.GuardedBy;
import android.support.annotation.Nullable;
import android.util.Log;

import android.app.Activity;
import android.app.ActivityManager;
import android.content.Context;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.Gravity;
import android.view.MotionEvent;
import android.widget.Button;
import android.widget.Toast;

import com.google.ar.core.Anchor;
import com.google.ar.core.Camera;
import com.google.ar.core.CameraConfig;
import com.google.ar.core.CameraIntrinsics;
import com.google.ar.core.Config;
import com.google.ar.core.Frame;
import com.google.ar.core.HitResult;
import com.google.ar.core.Plane;
import com.google.ar.core.Pose;
import com.google.ar.core.Session;
import com.google.ar.core.TrackingState;
import com.google.ar.core.exceptions.CameraNotAvailableException;
import com.google.ar.core.exceptions.UnavailableApkTooOldException;
import com.google.ar.core.exceptions.UnavailableArcoreNotInstalledException;
import com.google.ar.core.exceptions.UnavailableDeviceNotCompatibleException;
import com.google.ar.core.exceptions.UnavailableSdkTooOldException;
import com.google.ar.sceneform.AnchorNode;
import com.google.ar.sceneform.rendering.ModelRenderable;
import com.google.ar.sceneform.ux.ArFragment;
import com.google.ar.sceneform.ux.TransformableNode;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

import io.socket.client.IO;
import io.socket.client.Socket;

/**
 * This is an example activity that uses the Sceneform UX package to make common AR tasks easier.
 */
public class HelloSceneformActivity extends AppCompatActivity {
    private static final String TAG = HelloSceneformActivity.class.getSimpleName();
    private static final double MIN_OPENGL_VERSION = 3.0;
    private int hashCode;

    private ArFragment arFragment;
    private ModelRenderable andyRenderable;
    private Session session;

    private final SnackbarHelper snackbarHelper = new SnackbarHelper();
    private final StorageManager storageManager = new StorageManager();

    private Map<Integer, Pose> poseMatrices = new HashMap<>();
    private ArrayList<AnchorNode> mAnchors = new ArrayList<>();
    private Pose lastFramePose = Pose.IDENTITY;
    // AM = O; M = A(^-1)O; A = OM^(-1)
    // transformMatrix = M
    // serverMatrix = O
    // clientMatrix = A
    private Pose transformPose = null;

    private enum AppAnchorState {
        NONE,
        HOSTING,
        HOSTED,
        RESOLVING,
        RESOLVED
    }

    // Lock needed for synchronization.
    private final Object singleTapAnchorLock = new Object();

    @GuardedBy("singleTapAnchorLock")
    private AppAnchorState appAnchorState = AppAnchorState.NONE;

    @Nullable
    @GuardedBy("singleTapAnchorLock")
    private Anchor anchor;

    private Socket mSocket;

    {
        try {
            System.out.println("socket connecting");
            mSocket = IO.socket("http://192.168.0.198:80");
        } catch (URISyntaxException e) {
            e.printStackTrace();
        }
    }


    private void addAnchor(final Anchor anchor) {
        runOnUiThread(new Runnable() {
            public void run() {
                AnchorNode anchorNode = new AnchorNode(anchor);
                anchorNode.setParent(arFragment.getArSceneView().getScene());
                mAnchors.add(anchorNode);

                // Create the transformable andy and add it to the anchor.
                TransformableNode andy = new TransformableNode(arFragment.getTransformationSystem());
                andy.setParent(anchorNode);
                andy.setRenderable(andyRenderable);
                andy.select();
            }
        });
    }

    private void removeAnchor(final AnchorNode anchorNode) {
        runOnUiThread(new Runnable() {
            public void run() {
                anchorNode.getAnchor().detach();
                anchorNode.setParent(null);
            }
        });
    }

    @Override
    @SuppressWarnings({"AndroidApiCheckers", "FutureReturnValueIgnored"})
    // CompletableFuture requires api level 24
    // FutureReturnValueIgnored is not valid
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
        StrictMode.setThreadPolicy(policy);

        Random random = new Random();
        hashCode = random.nextInt();

        if (!checkIsSupportedDeviceOrFinish(this)) {
            return;
        }

        setContentView(R.layout.activity_ux);
        arFragment = (ArFragment) getSupportFragmentManager().findFragmentById(R.id.ux_fragment);

        arFragment.getPlaneDiscoveryController().hide();
        arFragment.getPlaneDiscoveryController().setInstructionView(null);

        Button clearButton = findViewById(R.id.clear_button);
        clearButton.setOnClickListener(
                (unusedView) -> {
                    synchronized (singleTapAnchorLock) {
                        setNewAnchor(null);
                    }
                });

        Button resolveButton = findViewById(R.id.resolve_button);
        resolveButton.setOnClickListener(
                (unusedView) -> {
//                    Camera camera = arFragment.getArSceneView().getArFrame().getCamera();
//                    TrackingState currentTrackingState = camera.getTrackingState();
//                    if(transformPose != null) {
//                        try {
//                            JSONObject addAndroidData = new JSONObject();
//                            // AM = O
////                            Pose androidServer = hitResult.getHitPose().compose(transformPose);
////                            Pose androidServer = hitResult.getHitPose().compose(transformPose);
//                            Pose androidServer = lastFramePose.compose(transformPose);
//                            float[] androidServerMatrix = new float[16];
//                            androidServer.toMatrix(androidServerMatrix, 0);
//                            JSONArray matrixJSON = new JSONArray(androidServerMatrix);
//                            addAndroidData.put("matrix", matrixJSON);
//                            mSocket.emit("add_android", addAndroidData);
//                        } catch (JSONException e) {
//                            e.printStackTrace();
//                        }
//                    }
                    synchronized (singleTapAnchorLock) {
                        if (anchor != null) {
                            snackbarHelper.showMessageWithDismiss(this, "Please clear anchor first.");
                            return;
                        }
                    }
                    ResolveDialogFragment dialog = new ResolveDialogFragment();
                    dialog.setOkListener(this::onResolveOkPressed);
                    dialog.show(getSupportFragmentManager(), "Resolve");
                });

        // Receiving an object
        mSocket.on("localization_data", args -> {
            JSONObject obj = (JSONObject)args[0];
            System.out.println("out " + obj.toString());
            try {
                JSONArray matrixJSON = obj.getJSONArray("matrix");
                int currHashCode = obj.getInt("hashCode");
                if(hashCode != currHashCode) return;
                int requestId = obj.getInt("requestId");
                float[] matrix = Helper.JSONArrayToFloatArray(matrixJSON);

                Pose poseServer = Helper.matrixToPose(matrix);
                Pose poseClient = poseMatrices.get(requestId);
                //M = A(^-1)O;
                transformPose = poseClient.inverse().compose(poseServer);
                //A = OM^(-1)
                JSONArray androidPosesJSON = obj.getJSONArray("androidPoses");
                ArrayList<Pose> androidPoses = Helper.getAndroidPoses(androidPosesJSON);
//                mAnchors.add(anchor);
                for (AnchorNode anchorNode: mAnchors){
                    removeAnchor(anchorNode);
                }
                mAnchors.clear();
//                if(mAnchors.size() > 0) return;
                for (Pose androidPose: androidPoses){
//                    float[] pos = { 0,0,-1 };
//                    float[] rotation = {0,0,0,1};
//                    Anchor anchor = session.createAnchor(new Pose(pos, rotation));
                    Camera camera = arFragment.getArSceneView().getArFrame().getCamera();
                    TrackingState currentTrackingState = camera.getTrackingState();
                    if(currentTrackingState == TrackingState.TRACKING){
//                    Anchor anchor = session.createAnchor(androidPose);
//                    mAnchors.add(anchor);
//                        Anchor anchor = session.createAnchor(
//                        lastFramePose
//                                .compose(Pose.makeTranslation(0, 0, -2f))
//                                .extractTranslation());
                        Anchor anchor = session.createAnchor(androidPose.compose(transformPose.inverse()));
                      addAnchor(anchor);
//                    AnchorNode anchorNode = new AnchorNode(anchor);
//                    anchorNode.setRenderable(andyRenderable);
//                    anchorNode.setParent(arFragment.getArSceneView().getScene());
                    }
                }
            } catch (JSONException e) {
                e.printStackTrace();
            }
        });

        final float[] lastSecs = {-1};
        arFragment.getArSceneView().getScene().addOnUpdateListener(FrameTime -> {
            System.out.println("update fram");
            System.out.println(Float.toString(lastSecs[0]));
            checkUpdatedAnchor();
            CameraIntrinsics imgIntrinsic = arFragment.getArSceneView().getArFrame().getCamera().getImageIntrinsics();
            Frame frame = arFragment.getArSceneView().getArFrame();
            lastFramePose = frame.getCamera().getPose();
            mSocket.connect();
            if(transformPose == null){
                System.out.println("anchor is not set yet");
//                return;
            } else {
//                Pose anchorPos = anchor.getPose().inverse();
//                Pose pos = anchorPos.compose(frame.getCamera().getPose());
                Pose pos = frame.getCamera().getPose().compose(transformPose);
                float[] matrix = new float[16];
                pos.toMatrix(matrix, 0);
                System.out.println(pos.toString());
                System.out.println(Arrays.toString(matrix));
                float[] projectionMatrix = new float[16];
                float nearClipPlane = arFragment.getArSceneView().getScene().getCamera().getNearClipPlane();
//                float farClipPlane = arFragment.getArSceneView().getScene().getCamera().getFarClipPlane();
                float farClipPlane = 0.5f;
                frame.getCamera().getProjectionMatrix(projectionMatrix, 0, nearClipPlane, farClipPlane);
                mSocket.connect();
                System.out.println("connected " + Boolean.toString(mSocket.connected()));
                JSONObject data = Helper.getPoseDataFromFrame(matrix, projectionMatrix, hashCode);
                mSocket.emit("cam_pos_phone", data);
                // Get tracked points/pointcloud
//                JSONObject pointsJson = Helper.getPointCloudFromFrame(frame, hashCode, anchorPos);
//                mSocket.emit("cam_pointcloud_phone", pointsJson);
            }
            float secs = FrameTime.getStartSeconds();
            if (lastSecs[0] + 4 > secs) {
                return;
            }
            lastSecs[0] = secs;
            // Get 2D Frame
            int requestId = random.nextInt();
            poseMatrices.put(requestId, frame.getCamera().getPose());
            JSONObject imgData = Helper.get2DImageFromFrame(frame, imgIntrinsic, this, hashCode, requestId);
            mSocket.emit("cam_img_phone", imgData);

            System.out.println("cam_img_phone");
        });

        // When you build a Renderable, Sceneform loads its resources in the background while returning
        // a CompletableFuture. Call thenAccept(), handle(), or check isDone() before calling get().
        ModelRenderable.builder()
                .setSource(this, R.raw.andy)
//                .setSource(this, R.raw.emoji)
//                .setSource(this, RenderableSource.builder().setSource(
//                        this,
//                        Uri.parse("https://poly.googleusercontent.com/downloads/0BnDT3T1wTE/85QOHCZOvov/Mesh_Beagle.gltf"),
//                        Uri.parse("https://poly.googleusercontent.com/downloads/c/fp/1556879418888625/dzbwWVcvygi/b-Qvn9R0BzA/model.gltf"),
//                        RenderableSource.SourceType.GLTF2)
//                        .setScale(0.1f)
//                        .build())
                .build()
                .thenAccept(renderable -> andyRenderable = renderable)
                .exceptionally(
                        throwable -> {
                            Toast toast =
                                    Toast.makeText(this, "Unable to load andy renderable", Toast.LENGTH_LONG);
                            toast.setGravity(Gravity.CENTER, 0, 0);
                            toast.show();
                            return null;
                        });

        arFragment.setOnTapArPlaneListener(
                (HitResult hitResult, Plane plane, MotionEvent motionEvent) -> {
                    if (andyRenderable == null) {
                        return;
                    }
                    Camera camera = arFragment.getArSceneView().getArFrame().getCamera();
                    TrackingState currentTrackingState = camera.getTrackingState();
                    if(transformPose != null) {
                        try {
                            JSONObject addAndroidData = new JSONObject();
                            // AM = O
                            Pose androidServer = hitResult.getHitPose().compose(transformPose);
                            float[] androidServerMatrix = new float[16];
                            androidServer.toMatrix(androidServerMatrix, 0);
                            JSONArray matrixJSON = new JSONArray(androidServerMatrix);
                            addAndroidData.put("matrix", matrixJSON);
                            mSocket.emit("add_android", addAndroidData);
                        } catch (JSONException e) {
                            e.printStackTrace();
                        }
                    }
//                    synchronized (singleTapAnchorLock) {
//                        if (anchor == null
//                                && currentTrackingState == TrackingState.TRACKING
//                                && appAnchorState == AppAnchorState.NONE) {
//                            // Create the Anchor.
//                            Anchor anch = hitResult.createAnchor();
//                            Anchor newAnchor = session.hostCloudAnchor(anch);
//                            setNewAnchor(newAnchor);
//                            appAnchorState = AppAnchorState.HOSTING;
//                            snackbarHelper.showMessage(this, "Now hosting anchor...");
//                        }
//                    }
                });
    }

    /**
     * Returns false and displays an error message if Sceneform can not run, true if Sceneform can run
     * on this device.
     *
     * <p>Sceneform requires Android N on the device as well as OpenGL 3.0 capabilities.
     *
     * <p>Finishes the activity if Sceneform can not run
     */
    public static boolean checkIsSupportedDeviceOrFinish(final Activity activity) {
        if (Build.VERSION.SDK_INT < VERSION_CODES.N) {
            Log.e(TAG, "Sceneform requires Android N or later");
            Toast.makeText(activity, "Sceneform requires Android N or later", Toast.LENGTH_LONG).show();
            activity.finish();
            return false;
        }
        String openGlVersionString =
                ((ActivityManager) activity.getSystemService(Context.ACTIVITY_SERVICE))
                        .getDeviceConfigurationInfo()
                        .getGlEsVersion();
        if (Double.parseDouble(openGlVersionString) < MIN_OPENGL_VERSION) {
            Log.e(TAG, "Sceneform requires OpenGL ES 3.0 later");
            Toast.makeText(activity, "Sceneform requires OpenGL ES 3.0 or later", Toast.LENGTH_LONG)
                    .show();
            activity.finish();
            return false;
        }
        return true;
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (session == null) {
            try {
                session = new Session(/* context= */ this);
                // Create default config and check if supported.
                Config config = new Config(session);
                config.setCloudAnchorMode(Config.CloudAnchorMode.ENABLED);
                config.setUpdateMode(Config.UpdateMode.LATEST_CAMERA_IMAGE);
                session.configure(config);
                arFragment.getArSceneView().setupSession(session);
                List<CameraConfig> configs = session.getSupportedCameraConfigs();
                session.setCameraConfig(configs.get(2));
            } catch (UnavailableArcoreNotInstalledException e) {
                e.printStackTrace();
            } catch (UnavailableApkTooOldException e) {
                e.printStackTrace();
            } catch (UnavailableSdkTooOldException e) {
                e.printStackTrace();
            } catch (UnavailableDeviceNotCompatibleException e) {
                e.printStackTrace();
            }
        }
        try {
            session.resume();
        } catch (CameraNotAvailableException e) {
            e.printStackTrace();
        } catch(Exception e){
            e.printStackTrace();
        }
    }

    @Override
    public void onPause() {
        super.onPause();
        if (session != null) {
            session.pause();
        }
    }

    /** Checks the anchor after an update. */
    private void checkUpdatedAnchor() {
        synchronized (singleTapAnchorLock) {
            if (appAnchorState != AppAnchorState.HOSTING && appAnchorState != AppAnchorState.RESOLVING) {
                // Do nothing if the app is not waiting for a hosting or resolving action to complete.
                return;
            }
            Anchor.CloudAnchorState cloudState = anchor.getCloudAnchorState();
            if (appAnchorState == AppAnchorState.HOSTING) {
                // If the app is waiting for a hosting action to complete.
                if (cloudState.isError()) {
                    snackbarHelper.showMessageWithDismiss(this, "Error hosting anchor: " + cloudState);
                    appAnchorState = AppAnchorState.NONE;
                } else if (cloudState == Anchor.CloudAnchorState.SUCCESS) {
                    int shortCode = storageManager.nextShortCode(this);
                    storageManager.storeUsingShortCode(this, shortCode, anchor.getCloudAnchorId());
                    snackbarHelper.showMessageWithDismiss(
                            this, "Anchor hosted successfully! Cloud Short Code: " + shortCode);
                    appAnchorState = AppAnchorState.HOSTED;
                }
            } else if (appAnchorState == AppAnchorState.RESOLVING) {
                // If the app is waiting for a resolving action to complete.
                if (cloudState.isError()) {
                    snackbarHelper.showMessageWithDismiss(this, "Error resolving anchor: " + cloudState);
                    appAnchorState = AppAnchorState.NONE;
                } else if (cloudState == Anchor.CloudAnchorState.SUCCESS) {
                    snackbarHelper.showMessageWithDismiss(this, "Anchor resolved successfully!");
                    appAnchorState = AppAnchorState.RESOLVED;
                }
            }
        }
    }

    /** Sets the new anchor in the scene. */
    @GuardedBy("singleTapAnchorLock")
    private void setNewAnchor(@Nullable Anchor newAnchor) {
        if (anchor != null) {
            anchor.detach();
        }
        anchor = newAnchor;
        appAnchorState = AppAnchorState.NONE;
        snackbarHelper.hide(this);

        AnchorNode anchorNode = new AnchorNode(anchor);
        anchorNode.setParent(arFragment.getArSceneView().getScene());

        // Create the transformable andy and add it to the anchor.
        TransformableNode andy = new TransformableNode(arFragment.getTransformationSystem());
        andy.setParent(anchorNode);
        andy.setRenderable(andyRenderable);
        andy.select();
    }

    private void onResolveOkPressed(String dialogValue) {
        int shortCode = Integer.parseInt(dialogValue);
        String cloudAnchorId = storageManager.getCloudAnchorId(this, shortCode);
        synchronized (singleTapAnchorLock) {
            Anchor resolvedAnchor = session.resolveCloudAnchor(cloudAnchorId);
            setNewAnchor(resolvedAnchor);
            snackbarHelper.showMessage(this, "Now resolving anchor...");
            appAnchorState = AppAnchorState.RESOLVING;
        }
    }
}
