/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * 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.android.glass.sample.timer;

import com.google.android.glass.media.Sounds;
import com.google.android.glass.touchpad.Gesture;
import com.google.android.glass.touchpad.GestureDetector;
import com.google.android.glass.touchpad.GestureDetector.BaseListener;
import com.google.android.glass.touchpad.GestureDetector.FingerListener;
import com.google.android.glass.touchpad.GestureDetector.ScrollListener;

import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.media.AudioManager;
import android.os.Bundle;
import android.view.animation.DecelerateInterpolator;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;

import java.util.concurrent.TimeUnit;

/**
 * Activity to set the timer.
 */
public class SetTimerActivity extends Activity implements BaseListener, ScrollListener,
        FingerListener {

    public static final String EXTRA_DURATION_MILLIS = "extra_duration";

    public static final String EXTRA_START_TIMER = "extra_start_timer";

    /** Maximum velocity when dragging. */
    private static final float MAX_DRAG_VELOCITY = 1;

    /** Deceleration constant for physics simulation. */
    private static final float DECELERATION_CONSTANT = 0.2f;

    /** Minimum velocity to start the inertial scrolling. */
    private static final float FLING_VELOCITY_CUTOFF = 1;

    /** Exagerate the time it takes to slow down the inertial scrolling. */
    private static final float TIME_LENGTHENING = 12;

    /** Max timer value of 24:59:00. */
    private static final long MAX_TIME_SECONDS = TimeUnit.HOURS.toSeconds(24)
            + TimeUnit.MINUTES.toSeconds(59);

    /** Animator for inertial scroll. */
    private ValueAnimator mInertialScrollAnimator;
    private float mReleaseVelocity;

    private float mTimeSeconds = 0;

    private TextView mHoursView;
    private TextView mMinutesView;
    private TextView mSecondsView;
    private TextView mTipView;

    private AudioManager mAudioManager;
    private GestureDetector mDetector;

    // Options menu flags.
    private boolean mShouldFinish;
    private boolean mOptionMenuOpen;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mTimeSeconds =
                TimeUnit.MILLISECONDS.toSeconds(getIntent().getLongExtra(EXTRA_DURATION_MILLIS, 0));
        mDetector = new GestureDetector(this)
                .setBaseListener(this)
                .setFingerListener(this)
                .setScrollListener(this);

        mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);

        // Initialize the various views.
        setContentView(R.layout.card_timer);
        mHoursView = (TextView) findViewById(R.id.hours);
        mMinutesView = (TextView) findViewById(R.id.minutes);
        mSecondsView = (TextView) findViewById(R.id.seconds);
        mTipView = (TextView) findViewById(R.id.tip);

        mSecondsView.setTextColor(getResources().getColor(R.color.gray));
        mSecondsView.setText("00");
        mTipView.setText(getResources().getString(R.string.swipe_to_set_timer));
        updateText();

        // Initialize the animator use for the intertial scrolling.
        mInertialScrollAnimator = new ValueAnimator();
        mInertialScrollAnimator.setInterpolator(new DecelerateInterpolator());
        mInertialScrollAnimator.addUpdateListener(new AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float value = (Float) animation.getAnimatedValue();
                setTimeSeconds(value);
            }
        });
    }

    @Override
    public void onPause() {
        super.onPause();
        mInertialScrollAnimator.cancel();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.set_timer, menu);
        return true;
    }

    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        // The "set" menu item should only be visible when called from another Activity.
        menu.findItem(R.id.set).setVisible(getCallingActivity() != null);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        mShouldFinish = true;
        switch (item.getItemId()) {
            case R.id.start:
                if (getCallingActivity() == null) {
                    startTimer();
                } else {
                    setTimer(true);
                }
                return true;
            case R.id.set:
                setTimer(false);
                return true;
            default:
                mShouldFinish = false;
                return super.onOptionsItemSelected(item);
        }
    }

    @Override
    public void onOptionsMenuClosed(Menu menu) {
        if (mShouldFinish) {
            finish();
        }
        mOptionMenuOpen = false;
    }

    @Override
    public boolean onGenericMotionEvent(MotionEvent event) {
        return mDetector.onMotionEvent(event);
    }

    @Override
    public void onFingerCountChanged(int previousCount, int currentCount) {
        boolean wentDown = currentCount > previousCount;

        if (currentCount == 0 && !wentDown && !mOptionMenuOpen) {
            // Only fling if the velocity is greater than the cutoff
            if (Math.abs(mReleaseVelocity) > FLING_VELOCITY_CUTOFF) {
                // Deceleration always in the opposite direction of the velocity
                final float deceleration = Math.signum(mReleaseVelocity) * -DECELERATION_CONSTANT;
                final float flingTime = -mReleaseVelocity / deceleration * TIME_LENGTHENING;
                float totalDelta = mReleaseVelocity * mReleaseVelocity / 2f / -deceleration;

                // Start the animation
                mInertialScrollAnimator.cancel();
                mInertialScrollAnimator.setFloatValues(
                        mTimeSeconds, confineTimeSeconds(mTimeSeconds + totalDelta));
                mInertialScrollAnimator.setDuration((long) flingTime);
                mInertialScrollAnimator.start();
            }
        } else {
            mInertialScrollAnimator.cancel();
        }
    }

    @Override
    public boolean onScroll(float displacement, float delta, float velocity) {
        mReleaseVelocity = velocity;
        if (!mOptionMenuOpen) {
            addTimeSeconds(delta * Math.min(Math.abs(velocity), MAX_DRAG_VELOCITY));
        }
        return true;
    }

    @Override
    public boolean onGesture(Gesture gesture) {
        switch (gesture) {
            case TAP:
                long timeMinutes = TimeUnit.SECONDS.toMinutes((long) mTimeSeconds);

                if (timeMinutes > 0) {
                    playSoundEffect(Sounds.TAP);
                    openOptionsMenu();
                    mOptionMenuOpen = true;
                } else {
                    playSoundEffect(Sounds.DISALLOWED);
                }
                return true;
            case SWIPE_DOWN:
                setResultInternal(RESULT_CANCELED, null);
                playSoundEffect(Sounds.DISMISSED);
                finish();
                return true;
            default:
                return false;
        }
    }

    /** Starts a new Timer. */
    private void startTimer() {
        Intent timerIntent = new Intent(this, TimerService.class);
        long timeMinutes = TimeUnit.SECONDS.toMinutes((long) mTimeSeconds);

        timerIntent.setAction(TimerService.ACTION_START);
        timerIntent.putExtra(
            TimerService.EXTRA_DURATION_MILLIS, TimeUnit.MINUTES.toMillis(timeMinutes));
        startService(timerIntent);
    }

    /** Returns the new timer value to the calling Activity. */
    private void setTimer(boolean startTimer) {
        Intent resultIntent = new Intent();
        long timeMinutes = TimeUnit.SECONDS.toMinutes((long) mTimeSeconds);

        resultIntent.putExtra(EXTRA_DURATION_MILLIS, TimeUnit.MINUTES.toMillis(timeMinutes));
        resultIntent.putExtra(EXTRA_START_TIMER, startTimer);
        setResultInternal(RESULT_OK, resultIntent);
    }

    /** Adds {@code delta} seconds to the Timer.*/
    private void addTimeSeconds(float delta) {
        setTimeSeconds(mTimeSeconds + delta);
    }

    /** Sets the Timer value. */
    private void setTimeSeconds(float timeSeconds) {
        float previousTimeSeconds = mTimeSeconds;

        mTimeSeconds = confineTimeSeconds(timeSeconds);
        if (TimeUnit.SECONDS.toMinutes((int) previousTimeSeconds)
            != TimeUnit.SECONDS.toMinutes((int) mTimeSeconds)) {
            playSoundEffect(Sounds.TAP);
            updateText();
        }
    }

    /** Updates the various {@link TextView} with the current Timer value. */
    private void updateText() {
        long hours = TimeUnit.SECONDS.toHours((int) mTimeSeconds);
        long minutes = TimeUnit.SECONDS.toMinutes((int) mTimeSeconds % TimeUnit.HOURS.toSeconds(1));

        mHoursView.setText(String.format("%02d", hours));
        mMinutesView.setText(String.format("%02d", minutes));
        if (hours == 0 && minutes == 0) {
            mTipView.setVisibility(View.VISIBLE);
        } else {
            mTipView.setVisibility(View.INVISIBLE);
        }
    }

    /**
     * Keeps the time between 0 and {@link MAX_TIME_SECONDS}.
     */
    private float confineTimeSeconds(float timeSeconds) {
        if (timeSeconds < 0) {
            timeSeconds = 0;
        } else if (timeSeconds > MAX_TIME_SECONDS) {
            timeSeconds = MAX_TIME_SECONDS;
        }
        return timeSeconds;
    }

    /**
     * Plays a sound effect, overridable for testing.
     */
    protected void playSoundEffect(int soundId) {
        mAudioManager.playSoundEffect(soundId);
    }

    /**
     * Sets the {@link Activity} result, overridable for testing.
     */
    protected void setResultInternal(int resultCode, Intent resultIntent) {
        setResult(resultCode, resultIntent);
    }

    /**
     * Forces any ongoing animation to immediately 'jump to the end', visible for testing.
     * This method must be called from same thread that performs the animation.
     */
    void forceEndAnimation() {
        if (mInertialScrollAnimator.isRunning()) {
            mInertialScrollAnimator.end();
        }
    }

    /** Returns the Timer current time, visible for testing. */
    float getTimeSeconds() {
        return mTimeSeconds;
    }

}