/* Copyright 2015 Esri
 *
 * 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.
 *
 * A copy of the license is available in the repository's
 * https://github.com/Esri/arcgis-runtime-demos-android/blob/master/license.txt
 *
 * For information about licensing your deployed app, see
 * https://developers.arcgis.com/android/guide/license-your-app.htm
 *
 */

package main.java.com.esri.test.glasssample;

import android.app.Activity;
import android.widget.Toast;

import com.esri.android.map.MapView;
import com.esri.core.geometry.Point;
import com.thalmic.myo.AbstractDeviceListener;
import com.thalmic.myo.Arm;
import com.thalmic.myo.Myo;
import com.thalmic.myo.Pose;
import com.thalmic.myo.Quaternion;
import com.thalmic.myo.XDirection;

import java.util.Timer;
import java.util.TimerTask;

/**
 * Created by mich6984 on 2/23/15.
 */
public class MyoMapListener extends AbstractDeviceListener {

  // The arm that Myo is on is unknown until the arm recognized event is received.
  private Arm mArm = Arm.UNKNOWN;

  private Activity mActivity;
  private MapView mMapView;

  private double mLastYawDelta;
  private double mLastRollDelta;
  private boolean mPanningEnabled = false;
  private Pose mCurrentPose;
  private XDirection mXDirection;
  private double mCurrentRoll;
  private double mCurrentPitch;
  private double mCurrentYaw;
  private double mLastYaw = Double.NaN;
  private double mLastRoll = Double.NaN;
  private double mStartingRoll;
  private double mStartingPitch;
  private double mStartingYaw;
  private final double DIV = 100.0;
  private final double THRESHOLD = 5.0 / DIV;
  private final double DELTA_TRIGGER = 5;
  private double factorBase = 0.0;
  private double xFactor = factorBase;
  private double yFactor = factorBase;
  // Base zoom factor, so that worst case, no zoom occurs
  private float mZoomoutFactor = 0.98f;
  private float mZoominFactor = 1.02f;
  private boolean mZoomingEnabled = false;

  private Timer mPanTimer;
  private Timer mZoomTimer;

  private class PanTimerTask extends TimerTask {
    private final double PAN_AMOUNT = 100.0;
    private final double SCALE_DIV = 10000;
    @Override
    public void run() {
      // zoom the map
      Point center = mMapView.getCenter();
      double newX = center.getX() + (PAN_AMOUNT * (mMapView.getScale()/SCALE_DIV) * xFactor);
      double newY = center.getY() + (PAN_AMOUNT * (mMapView.getScale()/SCALE_DIV) * yFactor);
      mMapView.centerAt(new Point(newX, newY), false);
    }
  }

  private class ZoomTimerTask extends TimerTask {
    private float mZoomFactor;

    public ZoomTimerTask(float zoomFactor) {
      mZoomFactor = zoomFactor;
    }

    @Override
    public void run() {
      // zoom the map
      mMapView.doubleTapZoom(mZoomFactor);
    }
  }

  public MyoMapListener(Activity activity, MapView mapView) {
    mActivity = activity;
    mMapView = mapView;
  }

  // onArmRecognized() is called whenever Myo has recognized a setup gesture after someone has put it on their
  // arm. This lets Myo know which arm it's on and which way it's facing.
  @Override
  public void onArmSync(Myo myo, long timestamp, Arm arm, XDirection xDirection) {
    // Save the arm the Myo is on so that we can use it in the pose events.
    mArm = arm;
    mXDirection = xDirection;
  }

  // onArmLost() is called whenever Myo has detected that it was moved from a stable position on a person's arm after
  // it recognized the arm. Typically this happens when someone takes Myo off of their arm, but it can also happen
  // when Myo is moved around on the arm.
  @Override
  public void onArmUnsync(Myo myo, long timestamp) {
    mArm = Arm.UNKNOWN;
  }

  @Override
  public void onOrientationData(Myo myo, long timestamp, Quaternion rotation) {
    updateValues(rotation);
    if(mPanningEnabled) {
      //X Factor
      normalizeYaw();
      double yawDiff = ((mCurrentYaw - mStartingYaw) * -1.0) / DIV;
      if(Math.abs(yawDiff) >= THRESHOLD) {
        yawDiff = (yawDiff < 0) ? yawDiff*yawDiff*-1.0 : yawDiff*yawDiff;
        xFactor = factorBase + yawDiff;
      } else {
        xFactor = factorBase;
      }

      //Y Factor
      double pitchDiff = ((mCurrentPitch - mStartingPitch) * -1.0) / DIV;
      if(Math.abs(pitchDiff) >= THRESHOLD) {
        pitchDiff = (pitchDiff < 0) ? pitchDiff*pitchDiff*-1.0 : pitchDiff*pitchDiff;
        yFactor = factorBase + pitchDiff;
      } else {
        yFactor = factorBase;
      }
      if(mPanTimer == null) {
        mPanTimer = new Timer();
        mPanTimer.schedule(new PanTimerTask(), 0, 16 /*16ms ensure 60Hz refresh rate*/);
      }
    }
    if(myo.isUnlocked() && mCurrentPose == Pose.FIST) {
      normalizeRoll();
      double rollDiff = mCurrentRoll - mStartingRoll;
      if(Math.abs(rollDiff) >= 5.0) {
        double newRoll = mMapView.getRotationAngle() + rollDiff;
        mMapView.setRotationAngle(newRoll);
      }
    }
  }

  private void normalizeYaw() {
    if(mLastYaw != Double.NaN) {
      double delta = mCurrentYaw - mLastYaw;
      if (Math.abs(delta) >= DELTA_TRIGGER) {
        if (mLastYawDelta > 0 && delta < 0) {
          mStartingYaw -= 360;
        } else if (mLastYawDelta < 0 && delta > 0) {
          mStartingYaw += 360;
        }
      }
      mLastYawDelta = delta;
    }
    mLastYaw = mCurrentYaw;
  }

  private void normalizeRoll() {
    if(mLastRoll != Double.NaN) {
      double delta = mCurrentRoll - mLastRoll;
      if (Math.abs(delta) >= DELTA_TRIGGER) {
        if (mLastRollDelta > 0 && delta < 0) {
          mStartingRoll -= 360;
        } else if (mLastRollDelta < 0 && delta > 0) {
          mStartingRoll += 360;
        }
      }
      mLastRollDelta = delta;
    }
    mLastRoll = mCurrentRoll;
  }

  private void updateValues(Quaternion rotation) {
    // Calculate Euler angles (roll, pitch, and yaw) from the quaternion.
    float roll = (float) Math.toDegrees(Quaternion.roll(rotation));
    float pitch = (float) Math.toDegrees(Quaternion.pitch(rotation));
    float yaw = (float) Math.toDegrees(Quaternion.yaw(rotation));
    // Adjust roll and pitch for the orientation of the Myo on the arm.
    if (mXDirection == XDirection.TOWARD_ELBOW) {
      roll *= -1;
      pitch *= -1;
    }
    roll += 180;
    yaw += 180;
    mCurrentRoll = roll;
    mCurrentPitch = pitch;
    mCurrentYaw = yaw;
  }

  // onPose() is called whenever a Myo provides a new pose.
  @Override
  public void onPose(Myo myo, long timestamp, Pose pose) {
    if(!myo.isUnlocked() && pose != Pose.DOUBLE_TAP) { return; }
    // Swap wave poses if the Myo is on the left arm. Allows user to "wave" right or left
    // regardless of the Myo arm and have the swipes be in the appropriate direction.
    if (mArm == Arm.LEFT) {
      if (pose == Pose.WAVE_IN) {
        pose = Pose.WAVE_OUT;
      } else if (pose == Pose.WAVE_OUT) {
        pose = Pose.WAVE_IN;
      }
    }

    // Dispatch touch pad events for the standard navigation controls based on the
    // current pose.
    switch (pose) {
      case DOUBLE_TAP:
        if(myo.isUnlocked()) {
          lock(myo);
        } else {
          unlock(myo);
        }
        break;
      case FIST:
        mStartingRoll = mCurrentRoll;
        break;
      case FINGERS_SPREAD:
        if(mPanningEnabled) {
          stopPanning(myo);
        } else {
          startPanning(myo);
        }
        break;
      case WAVE_IN:
        if(!mZoomingEnabled) {
          startZooming(mZoomoutFactor);
        }
        break;
      case WAVE_OUT:
        if(!mZoomingEnabled) {
          startZooming(mZoominFactor);
        }
        break;
      case REST:
        switch(mCurrentPose) {
          case WAVE_IN:
          case WAVE_OUT:
            stopZooming();
            break;
        }
        break;
    }
    mCurrentPose = pose;
  }

  private void lock(final Myo myo) {
    if (mPanningEnabled) {
      stopPanning(myo);
    }
    showToast("Control locked!");
    myo.lock();
  }

  private void unlock(final Myo myo) {
    showToast("Control unlocked!");
    myo.unlock(Myo.UnlockType.HOLD);
  }

  private void stopPanning(Myo myo) {
    mPanningEnabled = false;
    stopPanTimer();
    resetPanValues();
    showToast("Panning disabled!");
    myo.vibrate(Myo.VibrationType.MEDIUM);
  }

  private void startPanning(Myo myo) {
    mPanningEnabled = true;
    mStartingPitch = mCurrentPitch;
    mStartingYaw = mCurrentYaw;
    showToast("Panning enabled!");
    myo.vibrate(Myo.VibrationType.MEDIUM);
  }

  private void stopPanTimer() {
    if (mPanTimer != null) {
      mPanTimer.cancel();
      mPanTimer = null;
    }
  }

  private void resetPanValues() {
    xFactor = factorBase;
    yFactor = factorBase;
    mLastYaw = Double.NaN;
    mLastYawDelta = 0.0;
    mLastRoll = Double.NaN;
    mLastRollDelta = 0.0;
  }

  private void startZooming(float zoomFactor) {
    mZoomingEnabled = true;
    if(mZoomTimer == null) {
      mZoomTimer = new Timer();
      mZoomTimer.schedule(new ZoomTimerTask(zoomFactor), 0, 16);
    }
  }

  private void stopZooming() {
    mZoomingEnabled = false;
    if (mZoomTimer != null) {
      mZoomTimer.cancel();
      mZoomTimer = null;
    }
  }

  private void showToast(final String message) {
    mActivity.runOnUiThread(new Runnable() {
      public void run() {
        Toast.makeText(mActivity, message, Toast.LENGTH_SHORT).show();
      }
    });
  }
}