package com.android.facelock;

import java.util.Random;

import android.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.widget.ImageView;

public class PicturePasswordView extends ImageView
{
	public interface OnFingerUpListener
	{
		void onFingerUp( PicturePasswordView picturePassword, boolean shouldUnlock );
	}

	// The seed for the random number generator
	private int mSeed;
	
	private final boolean DEBUG = false;

	private static final int DEFAULT_GRID_SIZE = 10;
	private static final int FONT_SIZE = 20;
	
	private static final int COLOR_UNLOCK_CIRCLE_OFF = Color.rgb( 150, 150, 150 );
	private static final int COLOR_UNLOCK_CIRCLE_ON  = Color.rgb( 132, 212,  39 );
	
	// How far we have scrolled from (0, 0)
	private float mScrollX = 0;
	private float mScrollY = 0;
	
	// The current position of the finger, used for scrolling calculations
	private float mFingerX;
	private float mFingerY;
	
	// Size of the number 8.
	private Rect mTextBounds;
	
	// Paint objects
	private Paint mPaint;        // Paint for text
	private Paint mCirclePaint;  // Paint for circles
	private Paint mUnlockPaint;  // Paint for unlock circles
	
	// Grid size and grid size without randomization 
	private int mGridSize;
	private int mActualSize;
	
	// Whether we should show numbers or not
	private boolean mShowNumbers;
	
	// Scalar for fade animations
	private float mScale;
	private ObjectAnimator mAnimator;
	
	// PRNG object
	private Random mRandom;
	
	// Whether we should highlight any number, and if so, which number
	private boolean mHighlight = false;
	private int mHighlightX;
	private int mHighlightY;
	
	// The position in the image of the highlighted number (0..1)
	private float mHighlightImageX;
	private float mHighlightImageY;
	
	// Listener we should notify when the user lifts their finger
	private OnFingerUpListener mListener;

	// Number + position combo required to unlock device
	private int mUnlockNumber = -1;
	private float mUnlockNumberX = -1;
	private float mUnlockNumberY = -1;
	
	// Whether we should unlock next time the user lifts their finger
	private boolean mShouldUnlock = false;
	
	// Whether we should highlight our unlock number
	private boolean mHighlightUnlockNumber = false;
	
	// Whether we're resetting. Used in setScale to reset parameters when scale is 0
	private boolean mResetting = false;
	
	// Whether the user has enabled grid size randomization
	private boolean mRandomGridSize = false;
	
	// Number of unlock circles to show at the bottom
	private int mUnlockCircles = 0;
	
	// Number of filled unlock circles
	// The decimal part is used to fade in/out the rightmost circle
	private float mUnlockProgress = 0;
	
	// Size/spacing/padding of unlock circles
	private int mCircleSize;
	private int mCircleSpacing;
	private int mCirclePadding;
	
	// Animator for circle progress
	private ObjectAnimator mCircleAnimator;
	
	private int getNumberForXY( int x, int y )
	{
		// TODO: still sucks

		return Math.abs( mSeed ^ ( x * 2138105 + 1 ) * ( y + 1 * 23490 ) ) % 10;
	}

	public PicturePasswordView( Context context, AttributeSet attrs )
	{
		super( context, attrs );
		
		setScaleType( ScaleType.CENTER_CROP );
		
		mRandom = new Random();
		mSeed = mRandom.nextInt();
		
		mGridSize = DEFAULT_GRID_SIZE;
		
		///////////////////////
		// Initialize Paints //
		///////////////////////
		final DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
		final float shadowOff = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 2, displayMetrics );
		
		mPaint = new Paint( Paint.LINEAR_TEXT_FLAG );
		
		mPaint.setColor( Color.WHITE );
		
		mPaint.setShadowLayer( 10, shadowOff, shadowOff, Color.BLACK );
		mPaint.setTextSize( TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, FONT_SIZE, displayMetrics ) );
		
		mPaint.setAntiAlias( true );

		mCirclePaint = new Paint( Paint.ANTI_ALIAS_FLAG );

		mCirclePaint.setColor( Color.argb( 255, 0x33, 0xb5, 0xe5 ) );
		
		mCirclePaint.setStyle( Paint.Style.STROKE );
		mCirclePaint.setStrokeWidth( 5 );
		
		mUnlockPaint = new Paint( Paint.ANTI_ALIAS_FLAG );
		
		mTextBounds = new Rect();
		mPaint.getTextBounds( "8", 0, 1, mTextBounds );
		
		///////////////////////////
		// Initialize animations //
		///////////////////////////

		mScale = 1.0f;
		
		mAnimator = new ObjectAnimator();
		mAnimator.setTarget( this );
		mAnimator.setFloatValues( 0, 1 );
		mAnimator.setPropertyName( "scale" );
		mAnimator.setDuration( 200 );
		
		mCircleAnimator = new ObjectAnimator();
		mCircleAnimator.setTarget( this );
		mCircleAnimator.setPropertyName( "internalUnlockProgress" ); // ugh!
		mCircleAnimator.setDuration( 300 );
		
		///////////////////////
		// Hide/show numbers //
		///////////////////////
		
		mShowNumbers = true;
		
		TypedArray a = context.getTheme().obtainStyledAttributes( attrs, R.styleable.PicturePasswordView, 0, 0 );
		
		try
		{
			mShowNumbers = a.getBoolean( R.styleable.PicturePasswordView_showNumbers, true );
		}
		finally
		{
			a.recycle();
		}
		
		//////////////////////
		// Initialize sizes //
		//////////////////////
		mCircleSize = ( int ) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 6, displayMetrics );
		mCircleSpacing = ( int ) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 5, displayMetrics );
		mCirclePadding = ( int ) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 10, displayMetrics );
	}
	
	public boolean isShowNumbers()
	{
		return mShowNumbers;
	}
	
	public void reset()
	{
		mResetting = true;
		
		mAnimator.setDuration( 400 );
		
		// Repeat 0 to ensure setScale( 0 ) is called at least once
		mAnimator.setFloatValues( 1, 0, 0, 1 );

		mAnimator.start();
		
		setEnabled( false );
	}
	
	public void setShowNumbers( boolean show )
	{
		setShowNumbers( show, false );
		
		invalidate();
	}
	
	public void setScale( float scale )
	{		
		if ( mScale != scale )
		{
			mScale = scale;
			invalidate();
			
			if ( mResetting && ( scale == 0 || scale == 1 ) )
			{
				if ( scale == 0 )
				{
					mSeed = mRandom.nextInt();
					mScrollX = 0;
					mScrollY = 0;
					
					if ( mRandomGridSize )
					{
						setGridSize( mActualSize );
					}
				}
				else
				{
					mAnimator.setFloatValues( 0, 1 );
					mAnimator.setDuration( 200 );
					
					mResetting = false;
					setEnabled( true );
				}
			}
		}
	}
	
	public float getScale()
	{
		return mScale;
	}
	
	public void setShowNumbers( boolean show, boolean animate )
	{
		if ( animate )
		{
			mShowNumbers = true;

			if ( show )
				mAnimator.start();
			else
				mAnimator.reverse();
		}
		else
		{
			mShowNumbers = show;
			
			mScale = show ? 1.0f : 0.0f;			
		}
		
		invalidate();
	}
	
	public Point findNumberInGrid( int number )
	{
		if ( number < 0 || number > 9 ) return null;
		
		for ( int x = 0; x < mGridSize; x++ )
		{
			for ( int y = 0; y < mGridSize; y++ )
			{
				if ( getNumberForXY( x, y ) == number )
				{
					return new Point( x, y );
				}
			}
		}
		
		return null;
	}
	
	public void enforceNumber( int number )
	{
		if ( number < 0 || number > 9 ) return;
		
		while ( findNumberInGrid( number ) == null )
		{
			mSeed = mRandom.nextInt(); 
		}
	}
	
	public void setFocusNumber( int number )
	{
		if ( number >= 0 && number <= 9 )
		{
			mHighlight = true;
			
			mScrollX = mScrollY = 0;
			
			enforceNumber( number );
			
			Point position = findNumberInGrid( number );

			mHighlightX = position.x;
			mHighlightY = position.y;
		}
		else
		{
			mHighlight = false;
		}
	}
	
	public void setUnlockNumber( int number, float x, float y )
	{
		mUnlockNumber = number;
		mUnlockNumberX = x;
		mUnlockNumberY = y;
	}
	
	public void setOnFingerUpListener( OnFingerUpListener l )
	{
		mListener = l;
	}
	
	public void setGridSize( int size )
	{
		if ( size > 3 && size <= 8 )
		{
			mGridSize = mActualSize = size;
			
			if ( mRandomGridSize )
			{
				mGridSize = mActualSize + mRandom.nextInt( 3 ) - 1;
			}
			
			invalidate();
		}
	}
	
	public void setRandomize( boolean randomize )
	{
		mRandomGridSize = randomize;
		setGridSize( mActualSize );
	}
	
	public int getGridSize()
	{
		return mGridSize;
	}
	
	public PointF getHighlightPosition()
	{
		if ( mHighlight == false ) return null;
		
		return new PointF( mHighlightImageX, mHighlightImageY );
	}
	
	public void setHighlightUnlockNumber( boolean highlight )
	{
		mHighlightUnlockNumber = highlight;
	}
	
	public void setUnlockCircles( int circles )
	{
		mUnlockCircles = circles;
	}
	
	public void setInternalUnlockProgress( float progress )
	{
		mUnlockProgress = progress;
	}
	
	public float getInternalUnlockProgress()
	{
		return mUnlockProgress;
	}
	
	public void setUnlockProgress( int progress )
	{
		mCircleAnimator.setFloatValues( mUnlockProgress, progress );
		mCircleAnimator.start();
	}
	
	private static int lerp( int a, int b, float t )
	{
		return ( int ) ( t * ( b - a ) + a );
	}
	@Override
	protected void onDraw( Canvas canvas )
	{
		super.onDraw( canvas );
		
		if ( !mShowNumbers ) return;
	
		mPaint.setAlpha( ( int ) ( mScale * ( float ) ( ( mHighlight || mHighlightUnlockNumber ) ? 64 : 255 ) ) );

		final float cellSize = ( canvas.getWidth() / ( float ) mGridSize ) * ( mScale * 0.4f + 0.6f );
		
		final float xOffset = ( 1.0f - ( mScale * 0.4f + 0.6f ) ) * canvas.getWidth() / 2;
		final float yOffset = ( 1.0f - ( mScale * 0.4f + 0.6f ) ) * canvas.getWidth() / 2;
		
		float drawX = -cellSize / 1.5F + xOffset;
		
		mShouldUnlock = false;
		
		for ( int x = -1; x < mGridSize + 1; x++ )
		{
			float drawY = -mTextBounds.bottom + cellSize / 1.5F - cellSize + yOffset;
			
			for ( int y = -1; y < mGridSize + 1; y++ )
			{
				if ( DEBUG )
				{
					if ( x == -1 || y == -1 || x == mGridSize || y == mGridSize )
					{
						mPaint.setColor( Color.RED );
					}
					else
					{
						mPaint.setColor( Color.WHITE );
					}
				}
				
				int cellX = ( int ) ( x - Math.floor( mScrollX / cellSize ) );
				int cellY = ( int ) ( y - Math.floor( mScrollY / cellSize ) );
				
				if ( mScrollX / cellSize <= 0 && cellX != 0 && mScrollX != 0 ) cellX--;
				if ( mScrollY / cellSize <= 0 && cellY != 0 && mScrollY != 0 ) cellY--;
				
				float numX = drawX + mScrollX % cellSize;
				float numY = drawY + mScrollY % cellSize;
				
				Integer number = getNumberForXY( cellX, cellY );
				boolean shouldHighlight = false;
				
				if ( number == mUnlockNumber )
				{
					float unlockX = mUnlockNumberX * getWidth();
					float unlockY = mUnlockNumberY * getWidth();
					
					float dist = PointF.length( unlockX - numX, unlockY - numY );
					
					if ( dist < mTextBounds.right * 1.3f )
					{
						mShouldUnlock = true;
						
						if ( mHighlightUnlockNumber )
							shouldHighlight = true;
					}
				}
				
				if ( ( mHighlight && mHighlightX == cellX && mHighlightY == cellY ) || shouldHighlight )
				{
					mPaint.setAlpha( ( int ) ( mScale * 255 ) );
					canvas.drawCircle( numX + ( mTextBounds.right - mTextBounds.left ) / 2,
							numY + mTextBounds.top / 2,
							mPaint.getTextSize() / 1.5f, mCirclePaint );
				}
				
				canvas.drawText( number.toString(), numX, numY, mPaint );
				
				if ( ( mHighlight && mHighlightX == cellX && mHighlightY == cellY ) || shouldHighlight )
				{
					mHighlightImageX = numX / getWidth();
					mHighlightImageY = numY / getHeight();
					
					mPaint.setAlpha( ( int ) ( mScale * 64 ) );
				}

				drawY += cellSize;
			}
			
			drawX += cellSize;
		}
		
		int circlesWidth = mCircleSize * mUnlockCircles + mCircleSpacing * ( mUnlockCircles - 1 );
		
		int x = canvas.getWidth() / 2 - circlesWidth / 2;
		int y = canvas.getHeight() - mCirclePadding - mCircleSize / 2;
		int fullCircles = ( int ) Math.floor( mUnlockProgress );
		float partCircles = mUnlockProgress - fullCircles;
		
		for ( int i = 1; i < mUnlockCircles + 1; i++ )
		{
			if ( i <= fullCircles )
			{
				mUnlockPaint.setColor( COLOR_UNLOCK_CIRCLE_ON );
			}
			else if ( i == fullCircles + 1 )
			{
				int r = lerp( Color.red( COLOR_UNLOCK_CIRCLE_OFF ), Color.red( COLOR_UNLOCK_CIRCLE_ON ), partCircles );
				int g = lerp( Color.green( COLOR_UNLOCK_CIRCLE_OFF ), Color.green( COLOR_UNLOCK_CIRCLE_ON ), partCircles );
				int b = lerp( Color.blue( COLOR_UNLOCK_CIRCLE_OFF ), Color.blue( COLOR_UNLOCK_CIRCLE_ON ), partCircles );
				
				mUnlockPaint.setColor( Color.rgb( r, g, b ) );
			}
			else
			{
				mUnlockPaint.setColor( COLOR_UNLOCK_CIRCLE_OFF );
			}
			
			mUnlockPaint.setAlpha( 150 );
			canvas.drawCircle( x + mCircleSize / 2, y, mCircleSize, mUnlockPaint );
			
			x += mCircleSize * 2 + mCircleSpacing;
		}
		
		if ( DEBUG )
		{
			canvas.drawText( mScrollX / cellSize + "," + mScrollY / cellSize, 0, mTextBounds.bottom * 26.5f, mPaint );
		}
	}
	
	@Override
	public boolean onTouchEvent( MotionEvent event )
	{
		if ( !isEnabled() ) return true;
		
		float x = event.getX();
		float y = event.getY();
		
		switch( event.getAction() )
		{
			case MotionEvent.ACTION_DOWN:
				mFingerX = x;
				mFingerY = y;
				break;
				
			case MotionEvent.ACTION_MOVE:
				float diffx = x - mFingerX;
				float diffy = y - mFingerY;

				mScrollX += diffx;
				mScrollY += diffy;
				
				mFingerX = x;
				mFingerY = y;
				
				invalidate();
				break;
				
			case MotionEvent.ACTION_UP:
				if ( mListener != null )
				{
					mListener.onFingerUp( this, mShouldUnlock );
				}
		}

		return true; // super.onTouchEvent( event );
	}
}