package ca.yorku.cse.mack.tiltmaze;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.RadialGradient;
import android.graphics.RectF;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.ToneGenerator;
import android.os.Vibrator;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;

import java.util.ArrayList;

public class RollingBallPanel extends View
{
    final static String MYDEBUG = "MYDEBUG"; // for Log.i messages

    final float DEGREES_TO_RADIANS = 0.0174532925f;
    final int DEFAULT_BALL_DIAMETER = 10;
    final int DIV = 7; // number of divisions across the display for creating the path

    final int DEFAULT_TITLE_TEXT_SIZE = 20; // tweak as necessary
    final int DEFAULT_STATS_TEXT_SIZE = 10;
    final int DEFAULT_GAP = 7; // between lines of text
    final int DEFAULT_OFFSET = 10; // from bottom of display

    Path path1, path2, pathNom, pathMin;
    float pathTop, pathHeight;
    PointF[] p1, p2, p1Test, p2Test;

    ResultsData[] data;
    int trialCount;
    float movementNominal, movementMinimum, movementActual, movementOffPath;

    Bitmap ball, temp;
    int ballDiameter;
    ArrayList<HitPoint> hitPoint = new ArrayList<HitPoint>();
    MediaPlayer click;

    float dT; // time since last sensor event (seconds)
    float screenWidth, screenHeight, pixelDensity, div;
    int labelTextSize, statsTextSize, gap, offset;

    RectF ballNow;
    RectF topCircle, bottomCircle;
    RectF[] trialProgress;

    float pathWidth;
    boolean firstOffPathSample;
    Vibrator vib;
    int wallHits; // called upon

    // Lap Line Indicators
    boolean gameStart, seekBottom, done;
    ToneGenerator tg;
    boolean topToneHeard, bottomToneHeard;
    float timeForTrial; // called upon
    float timeOffPath; // called upon

    float yBallOld;
    float xBallOld;

    float xBall, yBall; // top-left of the ball (for painting)
    float xBallCenter, yBallCenter; // center of the ball

    float pitch, roll;
    float tiltAngle, tiltMagnitude;
    int numberOfTrials;
    String orderOfControl;
    float gain;
    boolean showNominalPath, showMinimumPath, showWallHits, soundAlarmOnWallHit;
    String blockCode;
    float velocity; // in pixels/second (velocity = tiltMagnitude * tiltVelocityGain
    float dBall; // the amount to move the ball (in pixels): dBall = dT * velocity
    float xCenter, yCenter; // the center of the screen
    long now, lastT;
    Paint statsPaint, titlePaint, orderOfControlPaint, blockCodePaint, circleLinePaint, pathLinePaint, pathFillPaint,
            backgroundPaint, nominalPathPaint, progressLinePaint, grayFillPaint, greenFillPaint;

    public RollingBallPanel(Context contextArg)
    {
        super(contextArg);
        initialize(contextArg);
    }

    public RollingBallPanel(Context contextArg, AttributeSet attrs)
    {
        super(contextArg, attrs);
        initialize(contextArg);
    }

    public RollingBallPanel(Context contextArg, AttributeSet attrs, int defStyle)
    {
        super(contextArg, attrs, defStyle);
        initialize(contextArg);
    }

    // things that can be initialized from within this View
    private void initialize(Context context)
    {
        // line paint for the circles
        circleLinePaint = new Paint();
        circleLinePaint.setColor(0xff008800);
        circleLinePaint.setStyle(Paint.Style.STROKE);
        circleLinePaint.setStrokeWidth(2);
        circleLinePaint.setAntiAlias(true);

        // line paint for the paths
        pathLinePaint = new Paint();
        pathLinePaint.setColor(Color.RED);
        pathLinePaint.setStyle(Paint.Style.STROKE);
        pathLinePaint.setStrokeWidth(2);
        pathLinePaint.setAntiAlias(true);

        // fill paint for the paths
        pathFillPaint = new Paint();
        pathFillPaint.setColor(0xffccbbbb);
        pathFillPaint.setStyle(Paint.Style.FILL);

        // line paint for the paths
        nominalPathPaint = new Paint();
        nominalPathPaint.setColor(Color.GRAY);
        nominalPathPaint.setStyle(Paint.Style.STROKE);
        nominalPathPaint.setStrokeWidth(1);
        nominalPathPaint.setAntiAlias(true);

        // line paint for progress circles
        progressLinePaint = new Paint();
        progressLinePaint.setColor(0xff008800);
        progressLinePaint.setStyle(Paint.Style.STROKE);
        progressLinePaint.setStrokeWidth(1);
        progressLinePaint.setAntiAlias(true);

        // gray fill (used when the goal is to reach the "other" circle)
        grayFillPaint = new Paint();
        grayFillPaint.setColor(0xffbbbbbb);
        grayFillPaint.setStyle(Paint.Style.FILL);

        greenFillPaint = new Paint();
        greenFillPaint.setColor(0xff88ff88);
        greenFillPaint.setStyle(Paint.Style.FILL);

        backgroundPaint = new Paint();
        backgroundPaint.setColor(Color.LTGRAY);
        backgroundPaint.setStyle(Paint.Style.FILL);

        titlePaint = new Paint();
        titlePaint.setColor(Color.BLACK);
        titlePaint.setTextSize(DEFAULT_TITLE_TEXT_SIZE);
        titlePaint.setAntiAlias(true);
        titlePaint.setTextAlign(Paint.Align.CENTER);

        orderOfControlPaint = new Paint();
        orderOfControlPaint.setColor(0xffaaaaaa);
        orderOfControlPaint.setTextSize(3 * DEFAULT_TITLE_TEXT_SIZE);
        orderOfControlPaint.setAntiAlias(true);
        orderOfControlPaint.setTextAlign(Paint.Align.CENTER);

        blockCodePaint = new Paint();
        blockCodePaint.setColor(Color.DKGRAY);
        blockCodePaint.setTextSize(DEFAULT_TITLE_TEXT_SIZE);
        blockCodePaint.setAntiAlias(true);
        blockCodePaint.setTextAlign(Paint.Align.LEFT);

        statsPaint = new Paint();
        statsPaint.setAntiAlias(true);
        statsPaint.setTextSize(DEFAULT_STATS_TEXT_SIZE);

        lastT = System.nanoTime();

        ballDiameter = DEFAULT_BALL_DIAMETER;
        temp = BitmapFactory.decodeResource(getResources(), R.drawable.ball);
        ball = Bitmap.createScaledBitmap(temp, ballDiameter, ballDiameter, true);

        this.setBackgroundColor(Color.LTGRAY);

        click = MediaPlayer.create(context, R.raw.click);

        pathWidth = 4 * ballDiameter; // default
        firstOffPathSample = true;

        // Top and bottom circles
        topCircle = new RectF();
        bottomCircle = new RectF();

        timeForTrial = 0; // added called upon
        timeOffPath = 0; // added called upon

        yBallOld = 0; // added
        xBallOld = 0; // added
        // south = false; // added
        // east = false; // added

        ballNow = new RectF();
        wallHits = 0;

        done = false;
        gameStart = false; // added
        seekBottom = false;
        topToneHeard = false; // added
        bottomToneHeard = false;
        tg = new ToneGenerator(AudioManager.STREAM_NOTIFICATION, 100); // added

        path1 = new Path();
        path2 = new Path();
        pathNom = new Path();
        pathMin = new Path();
    }

    public void setNumberOfTrials(int numberOfTrialsArg)
    {
        numberOfTrials = numberOfTrialsArg;
        data = new ResultsData[numberOfTrials];

        // init the rectangles (i.e., circles) that display the result of each trial
        trialProgress = new RectF[numberOfTrials];
        for (int i = 0; i < trialProgress.length; ++i)
        {
            float xProgress = i * div + 1 * pixelDensity;
            float yProgress = screenHeight - div - 1 * pixelDensity;
            trialProgress[i] = new RectF();
            trialProgress[i].left = xProgress;
            trialProgress[i].top = yProgress;
            trialProgress[i].right = xProgress + div;
            trialProgress[i].bottom = yProgress + div;
        }
    }

    public void setGain(float gainArg)
    {
        gain = gainArg;
    }

    public void setOrderOfControl(String orderOfControlArg)
    {
        orderOfControl = orderOfControlArg;
    }

    public void setShowNominalPath(boolean showNominalPathArg)
    {
        showNominalPath = showNominalPathArg;
    }

    public void setShowMinimumPath(boolean showMinimumPathArg)
    {
        showMinimumPath = showMinimumPathArg;
    }

    public void setShowWallHits(boolean showWallHitsArg)
    {
        showWallHits = showWallHitsArg;
    }

    public void setSoundAlarmOnWallHit(boolean soundAlarmOnWallHitArg)
    {
        soundAlarmOnWallHit = soundAlarmOnWallHitArg;
    }

    public void setBlockCode(String blockCodeArg)
    {
        blockCode = blockCodeArg;
    }

    /*
     * Do the heavy lifting here! Update the ball position based on the tilt angle, tilt magnitude,
     * order of control, etc. Also, check for wall hits, etc.
     */
    public void updateBallPosition(float pitchArg, float rollArg, float tiltAngleArg, float tiltMagnitudeArg)
    {
        pitch = pitchArg; // for information only (see onDraw)
        roll = rollArg; // for information only (see onDraw)
        tiltAngle = tiltAngleArg;
        tiltMagnitude = tiltMagnitudeArg;

        // get current time and delta since last time we were here
        now = System.nanoTime();
        dT = (now - lastT) / 1000000000f; // seconds
        lastT = now;

        // don't allow tiltMagnitude to exceed 45 degrees
        final float MAX_MAGNITUDE = 45f;
        tiltMagnitude = tiltMagnitude > MAX_MAGNITUDE ? MAX_MAGNITUDE : tiltMagnitude;

        // save last position of ball (used to compute ball distance actually moved)
        xBallOld = xBall;
        yBallOld = yBall;

        // compute new position of ball

        // This is the only code that distinguishes velocity-control from position-control
        if (orderOfControl.equals("Velocity")) // velocity control
        {
            // compute how far the ball should move
            velocity = tiltMagnitude * gain;
            dBall = dT * velocity; // make the ball move this amount (pixels)

            // compute the ball's new coordinates
            float dx = (float)Math.sin(tiltAngle * DEGREES_TO_RADIANS) * dBall;
            float dy = -(float)Math.cos(tiltAngle * DEGREES_TO_RADIANS) * dBall;
            xBall += dx;
            yBall += dy;

        } else
        // position control
        {
            // compute how far the ball should move
            dBall = tiltMagnitude * gain;

            // compute the ball's new coordinates
            float dx = (float)Math.sin(tiltAngle * DEGREES_TO_RADIANS) * dBall;
            float dy = -(float)Math.cos(tiltAngle * DEGREES_TO_RADIANS) * dBall;
            xBall = xCenter + dx;
            yBall = yCenter + dy;
        }

        // keep the ball visible (also, restore if NaN)
        if (Float.isNaN(xBall) || xBall < 0)
            xBall = 0;
        else if (xBall > screenWidth - ballDiameter)
            xBall = screenWidth - ballDiameter;
        if (Float.isNaN(yBall) || yBall < 0)
            yBall = 0;
        else if (yBall > screenHeight - ballDiameter)
            yBall = screenHeight - ballDiameter;

        xBallCenter = xBall + ballDiameter / 2f;
        yBallCenter = yBall + ballDiameter / 2f;

        ballNow.left = xBall;
        ballNow.top = yBall;
        ballNow.right = xBall + ballDiameter;
        ballNow.bottom = yBall + ballDiameter;

        // things to do -- provided the game has started
        if (gameStart)
        {
            // increment elapsed time for trial
            timeForTrial += dT;

            // check if the ball has ventured off the desired path
            if (ballOffPath(xBallCenter, yBallCenter))
            {
                /*
				 * If this is the first off-path sample, vibrate, sound an auditory click (if option
				 * selected), increment wall hits, and create a gradient bitmap for the hit point
				 */

                if (firstOffPathSample)
                {
                    firstOffPathSample = false;
                    vib.vibrate(10);
                    if (soundAlarmOnWallHit)
                        click.start();
                    ++wallHits;
                    hitPoint.add(new HitPoint(xBallCenter, yBallCenter));
                } else
                {
					/*
					 * The ball is off the desired path, but this is *not* the first off-path
					 * sample. In this case, add the delta time and delta distance to the relevant
					 * variables.
					 */

                    timeOffPath += dT; // add to off-path time
                    movementOffPath += Math.sqrt((xBall - xBallOld) * (xBall - xBallOld) + (yBall - yBallOld)
                            * (yBall - yBallOld)); // add to off-path movement distance
                }

            } else
            {
                firstOffPathSample = true;
            }

            // add to the actual ball movement (includes off-path movement)
            movementActual += Math.sqrt((xBall - xBallOld) * (xBall - xBallOld) + (yBall - yBallOld)
                    * (yBall - yBallOld));
        }

        // check if the ball has entered the top circle (while we are seeking it)
        if (gameStart && !seekBottom && topCircle.contains(ballNow))
        {
            if (!topToneHeard)
            {
                topToneHeard = true; // use flag to avoid repeated beeps
                tg.startTone(ToneGenerator.TONE_PROP_BEEP); // Beep
            }

            seekBottom = true; // we are now seeking the bottom circle
            bottomToneHeard = false;

            doEndOfTrial();
        }

        // check if the ball has entered the bottom circle (while we are seeking it)
        if (gameStart && seekBottom && bottomCircle.contains(ballNow))
        {
            if (!bottomToneHeard)
            {
                bottomToneHeard = true; // use flag to avoid repeated beeps
                tg.startTone(ToneGenerator.TONE_PROP_BEEP); // Beep
            }

            seekBottom = false; // we are now seeking the top circle
            topToneHeard = false;

            doEndOfTrial();
        }

        // check if the ball has entered the top circle for the 1st time (to start the game)
        if (!gameStart && topCircle.contains(ballNow))
            doStartGame(); // init some things to start the game

        invalidate(); // force onDraw to redraw the screen with the ball in its new position
    }

    protected void onDraw(Canvas canvas)
    {
        // draw the Path objects that determine the ball path
        canvas.drawPath(path1, pathFillPaint);
        canvas.drawPath(path1, pathLinePaint);

        canvas.drawPath(path2, pathFillPaint);
        canvas.drawPath(path2, pathLinePaint);

		/*
		 * Draw the start and end circles and fill with the appropriate color. The appropriate fill
		 * color is green is we are seeking that circle, gray otherwise.
		 */
        if (!seekBottom)
            canvas.drawOval(topCircle, greenFillPaint);
        else
            canvas.drawOval(topCircle, grayFillPaint);
        canvas.drawOval(topCircle, circleLinePaint);

        if (seekBottom)
            canvas.drawOval(bottomCircle, greenFillPaint);
        else
            canvas.drawOval(bottomCircle, grayFillPaint);
        canvas.drawOval(bottomCircle, circleLinePaint);

        // draw title label
        canvas.drawText("Tilt Maze", screenWidth / 2f, labelTextSize, titlePaint);

        // draw order of control
        canvas.drawText(orderOfControl + " Control", screenWidth / 2f, 3f / 5f * pathTop, orderOfControlPaint);

        // show nominal path (if option set)
        if (showNominalPath)
            canvas.drawPath(pathNom, nominalPathPaint);

        // show minimum path (if option set)
        if (showMinimumPath)
            canvas.drawPath(pathMin, nominalPathPaint);

        // show wall hits (if option set)
        if (showWallHits)
        {
            for (HitPoint hp : hitPoint)
                canvas.drawBitmap(hp.splat, hp.x - ballDiameter / 2f, hp.y - ballDiameter / 2f, null);
        }

        // draw block code
        canvas.drawText(blockCode, 2 * pixelDensity, screenHeight - div - 4 * pixelDensity, blockCodePaint);

        // draw the results circles
        for (int i = 0; i < trialProgress.length; ++i)
        {
            if (i < trialCount)
                canvas.drawOval(trialProgress[i], greenFillPaint);
            canvas.drawOval(trialProgress[i], progressLinePaint);
        }

        // draw the ball in its new location
        canvas.drawBitmap(ball, xBall, yBall, null);

    }// end onDraw

    // configure the rolling ball panel according to screen size and pixel density
    public void configure(float w, float h, float scalingFactorArg)
    {
        screenWidth = w;
        screenHeight = h;
        pixelDensity = scalingFactorArg;

        div = 16 * pixelDensity; // 1/10th of an inch

        xCenter = w / 2f;
        yCenter = h / 2f;
        xBall = xCenter;
        yBall = yCenter;
        xBallCenter = xBall + ballDiameter / 2f;
        yBallCenter = yBall + ballDiameter / 2f;

        // set the ball diameter to 1/4 the path width
        ballDiameter = (int)((screenWidth / DIV) / 4f + 0.5f);
        ball = Bitmap.createScaledBitmap(temp, ballDiameter, ballDiameter, true);

        pathTop = 0.20f * h;
        pathHeight = 0.60f * h;
        pathWidth = screenWidth / DIV;

        p1 = new PointF[8];
        p1[0] = new PointF(0f, pathTop);
        p1[1] = new PointF(5f / DIV * w, pathTop);
        p1[2] = new PointF(5f / DIV * w, pathTop + pathHeight - 2f * pathWidth);
        p1[3] = new PointF(4f / DIV * w, pathTop + pathHeight - 2f * pathWidth);
        p1[4] = new PointF(4f / DIV * w, pathTop + pathWidth);
        p1[5] = new PointF(1f / DIV * w, pathTop + pathWidth);
        p1[6] = new PointF(1f / DIV * w, pathTop + pathHeight);
        p1[7] = new PointF(0f, pathTop + pathHeight);

        path1.moveTo(p1[0].x, p1[0].y);
        for (int i = 1; i < p1.length; ++i)
            path1.lineTo(p1[i].x, p1[i].y);
        path1.close();

        p2 = new PointF[8];
        p2[0] = new PointF(w, pathTop);
        p2[1] = new PointF(w, pathTop + pathHeight);
        p2[2] = new PointF(2f / DIV * w, pathTop + pathHeight);
        p2[3] = new PointF(2f / DIV * w, pathTop + 2f * pathWidth);
        p2[4] = new PointF(3f / DIV * w, pathTop + 2f * pathWidth);
        p2[5] = new PointF(3f / DIV * w, pathTop + pathHeight - pathWidth);
        p2[6] = new PointF(6f / DIV * w, pathTop + pathHeight - pathWidth);
        p2[7] = new PointF(6f / DIV * w, pathTop);

        path2.moveTo(p2[0].x, p2[0].y);
        for (int i = 1; i < p2.length; ++i)
            path2.lineTo(p2[i].x, p2[i].y);
        path2.close();

		/*
		 * Since we must check if the *edge* of the ball touches the edge of the paths, we need
		 * separate arrays of test points. These points mark-out a path that lies outside each path
		 * by a distance equal to the ball radius.
		 */
        p1Test = new PointF[8];
        final float r = ballDiameter / 2f;
        p1Test[0] = new PointF(p1[0].x - r, p1[0].y - r);
        p1Test[1] = new PointF(p1[1].x + r, p1[1].y - r);
        p1Test[2] = new PointF(p1[2].x + r, p1[2].y + r);
        p1Test[3] = new PointF(p1[3].x - r, p1[3].y + r);
        p1Test[4] = new PointF(p1[4].x - r, p1[4].y + r);
        p1Test[5] = new PointF(p1[5].x + r, p1[5].y + r);
        p1Test[6] = new PointF(p1[6].x + r, p1[6].y + r);
        p1Test[7] = new PointF(p1[7].x - r, p1[7].y + r);

        p2Test = new PointF[8];
        p2Test[0] = new PointF(p2[0].x + r, p2[0].y - r);
        p2Test[1] = new PointF(p2[1].x + r, p2[1].y + r);
        p2Test[2] = new PointF(p2[2].x - r, p2[2].y + r);
        p2Test[3] = new PointF(p2[3].x - r, p2[3].y - r);
        p2Test[4] = new PointF(p2[4].x + r, p2[4].y - r);
        p2Test[5] = new PointF(p2[5].x + r, p2[5].y - r);
        p2Test[6] = new PointF(p2[6].x - r, p2[6].y - r);
        p2Test[7] = new PointF(p2[7].x - r, p2[7].y - r);

        // Start circle
        topCircle.left = 30;
        topCircle.top = 30;
        topCircle.right = topCircle.left + 4 * ballDiameter;
        topCircle.bottom = topCircle.top + 4 * ballDiameter;

        // End circle
        bottomCircle.right = screenWidth - 30;
        bottomCircle.bottom = screenHeight - 30;
        bottomCircle.left = bottomCircle.right - 4 * ballDiameter;
        bottomCircle.top = bottomCircle.bottom - 4 * ballDiameter;

		/*
		 * Create a Path object for the "nominal" ball movement, then use the point array to compute
		 * the nominal distance for the ball. Note: the nominal path passes through the middle of
		 * the opening.
		 */
        PointF[] pathNominal = new PointF[8];
        float y2 = pathTop - ballDiameter / 2f;
        float y1 = (topCircle.bottom + topCircle.top) / 2f;
        float x2 = screenWidth - 2f * pathWidth + ballDiameter / 2f;
        float x1 = (topCircle.bottom + topCircle.top) / 2f;
        float z = (topCircle.right - topCircle.left) / 2f - ballDiameter / 2f;
        float theta = (float)Math.atan((y2 - y1) / (x2 - x1));
        float x = (float)(x1 + z * Math.cos(theta)) - 1f;
        float y = (float)(y1 + z * Math.sin(theta)) - 1f;

        pathNominal[0] = new PointF(x, y);
        pathNominal[1] = new PointF(screenWidth - 1.5f * pathWidth, pathTop - ballDiameter);
        pathNominal[2] = new PointF(screenWidth - 1.5f * pathWidth, pathTop + pathHeight - 1.5f * pathWidth);
        pathNominal[3] = new PointF(screenWidth / 2f, pathTop + pathHeight - 1.5f * pathWidth);
        pathNominal[4] = new PointF(screenWidth / 2f, pathTop + 1.5f * pathWidth);
        pathNominal[5] = new PointF(1.5f * pathWidth, pathTop + 1.5f * pathWidth);
        pathNominal[6] = new PointF(1.5f * pathWidth, pathTop + pathHeight + ballDiameter);

        y2 = (bottomCircle.bottom + bottomCircle.top) / 2f;
        y1 = pathTop + pathHeight + ballDiameter;
        x2 = (bottomCircle.right + bottomCircle.left) / 2f;
        x1 = 1.5f * pathWidth;
        theta = (float)Math.atan((y2 - y1) / (x2 - x1));
        x = (float)(x2 - z * Math.cos(theta)) + 1f;
        y = (float)(y2 - z * Math.sin(theta)) + 1f;
        pathNominal[7] = new PointF(x, y);

        movementNominal = 0f;
        pathNom.moveTo(pathNominal[0].x, pathNominal[0].y);
        for (int i = 1; i < pathNominal.length; ++i)
        {
            pathNom.lineTo(pathNominal[i].x, pathNominal[i].y);
            x1 = pathNominal[i - 1].x;
            y1 = pathNominal[i - 1].y;
            x2 = pathNominal[i].x;
            y2 = pathNominal[i].y;
            movementNominal += Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
        }
		
		
		/*
		 * Create a Path object for the "minimum" ball movement, then use the point array to compute
		 * the optimal distance for the ball. Note: the minimun path is the shortest path the ball can
		 * travel along without hitting a wall.
		 */
        PointF[] pathMinimum = new PointF[8];
        pathMinimum[0] = pathNominal[0];
        pathMinimum[1] = new PointF(screenWidth - 2f * pathWidth + ballDiameter / 2f + 1f, pathTop - ballDiameter /
				2f - 1f);
        pathMinimum[2] = new PointF(screenWidth - 2f * pathWidth + ballDiameter / 2f + 1f, pathTop + pathHeight - 2f
				* pathWidth + ballDiameter / 2f + 1f);
        pathMinimum[3] = new PointF(screenWidth - 3f * pathWidth - ballDiameter / 2f - 1f, pathTop + pathHeight - 2f
				* pathWidth + ballDiameter / 2f + 1f);
        pathMinimum[4] = new PointF(3f * pathWidth + ballDiameter / 2f + 1f, pathTop + 2f * pathWidth - ballDiameter
				/ 2f - 1f);
        pathMinimum[5] = new PointF(2f * pathWidth - ballDiameter / 2f - 1f, pathTop + 2f * pathWidth - ballDiameter
				/ 2f - 1f);
        pathMinimum[6] = new PointF(2f * pathWidth - ballDiameter / 2f - 1f, pathTop + pathHeight + ballDiameter / 2f
				+ 1f);
        pathMinimum[7] = pathNominal[7];

        movementMinimum = 0f;
        pathMin.moveTo(pathMinimum[0].x, pathMinimum[0].y);
        for (int i = 1; i < pathMinimum.length; ++i)
        {
            pathMin.lineTo(pathMinimum[i].x, pathMinimum[i].y);
            x1 = pathMinimum[i - 1].x;
            y1 = pathMinimum[i - 1].y;
            x2 = pathMinimum[i].x;
            y2 = pathMinimum[i].y;
            movementMinimum += Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
        }

        Log.i(MYDEBUG, "movementNominal=" + movementNominal + ", movementMinimum=" + movementMinimum);

        labelTextSize = (int)(DEFAULT_TITLE_TEXT_SIZE * pixelDensity + 0.5f);
        titlePaint.setTextSize(labelTextSize);

        statsTextSize = (int)(DEFAULT_STATS_TEXT_SIZE * pixelDensity + 0.5f);
        statsPaint.setTextSize(statsTextSize);

        gap = (int)(DEFAULT_GAP * pixelDensity + 0.5f);
        offset = (int)(DEFAULT_OFFSET * pixelDensity + 0.5f);
    }

    public void setVibrator(Vibrator v)
    {
        vib = v;
    }

    // things to the at the start of the game (before the first trial)
    private void doStartGame()
    {
        gameStart = true;
        seekBottom = true;
        wallHits = 0;
        trialCount = 0;
        timeOffPath = 0;
        timeForTrial = 0;
        movementActual = 0;
        movementOffPath = 0;
        tg.startTone(ToneGenerator.TONE_PROP_BEEP); // Beep
    }

    // things to do at the end of each trial
    private void doEndOfTrial()
    {
        ++trialCount;
        data[trialCount - 1] = new ResultsData(trialCount, timeForTrial, timeOffPath, wallHits, movementMinimum,
                movementActual, movementOffPath, (movementNominal / movementActual));
        timeForTrial = 0;
        timeOffPath = 0;
        wallHits = 0;
        movementActual = 0f;
        movementOffPath = 0f;
        hitPoint = new ArrayList<HitPoint>();
        if (trialCount == numberOfTrials)
        {
            done = true; // Note: the flag is tested in the main activity
            if (click != null)
            {
                click.release();
                click = null;
            }
        }
    }

    // return true of the ball is off the desired path (i.e., touching or inside the Path objects)
    public boolean ballOffPath(float x, float y)
    {
        // NOTE: We are using the "crossing number algorithm" (Google for details)

        // check if the ball edge is in path 1
        int polySides = p1Test.length;
        boolean oddTransitionsPath1 = false;
        for (int i = 0, j = polySides - 1; i < polySides; j = i++)
            if ((p1Test[i].y < y && p1Test[j].y >= y) || (p1Test[j].y < y && p1Test[i].y >= y))
                if (p1Test[i].x + (y - p1Test[i].y) / (p1Test[j].y - p1Test[i].y) * (p1Test[j].x - p1Test[i].x) < x)
                    oddTransitionsPath1 = !oddTransitionsPath1;

        // check if the ball edge is in path 2
        polySides = p2Test.length;
        boolean oddTransitionsPath2 = false;
        for (int i = 0, j = polySides - 1; i < polySides; j = i++)
            if ((p2Test[i].y < y && p2Test[j].y >= y) || (p2Test[j].y < y && p2Test[i].y >= y))
                if (p2Test[i].x + (y - p2Test[i].y) / (p2Test[j].y - p2Test[i].y) * (p2Test[j].x - p2Test[i].x) < x)
                    oddTransitionsPath2 = !oddTransitionsPath2;

        return oddTransitionsPath1 || oddTransitionsPath2;
    }

    /*
     * Create a feathered splat to display at the wall-hit point. This is based on a StackOverflow
     * posting. See...
     *
     * http://stackoverflow.com/questions/9954395/how-to-achieve-feathering-effect-in-android
     */
    private Bitmap getFeatheredHitPoint(int widthArg, int heightArg)
    {
        // Determine largest dimension, use for rectangle.
        int size = Math.max(widthArg, heightArg);

        RadialGradient gradient = new RadialGradient(size / 2, size / 2, size / 2,
                new int[] {0xFFff0000, 0x00ff0000}, new float[] {0.0f, 1.0f},
                android.graphics.Shader.TileMode.CLAMP);
        Paint paint = new Paint();
        paint.setShader(gradient);

        Bitmap bitmap = Bitmap.createBitmap(size, size, Config.ARGB_8888);
        Canvas canvas = new Canvas(bitmap);
        canvas.drawCircle(size / 2, size / 2, size / 2, paint);
        return Bitmap.createScaledBitmap(bitmap, widthArg, heightArg, true);
    }

    // ====================
    // inner classes at end
    // ====================

    /*
     * Simple class to hold the data that are provided to the main activity
     */
    public class ResultsData
    {
        int trialNumberResult; // count
        float timeForTrialResult; // seconds
        float timeOffPathResult; // seconds
        int wallHitsResult; // count
        float movementNominalResult; // pixels
        float movementActualResult; // pixels
        float movementOffPathResult; // pixels
        float movementEfficiencyResult; // = movementNominal / movementActual

        ResultsData(int trialNumberArg, float timeElapsedArg, float timeOffPathArg, int wallHitsArg,
                    float movementNominalArg, float movementActualArg, float movementOffPathArg, float movementEfficiencyArg)
        {
            trialNumberResult = trialNumberArg;
            timeForTrialResult = timeElapsedArg;
            timeOffPathResult = timeOffPathArg;
            wallHitsResult = wallHitsArg;
            movementNominalResult = movementNominalArg;
            movementActualResult = movementActualArg;
            movementOffPathResult = movementOffPathArg;
            movementEfficiencyResult = movementEfficiencyArg;
        }
    }

    /*
     * Simple class to hold the coordinate and bitmap for a hit point -- a marker showing where a
     * wall hit occurred.
     */
    public class HitPoint
    {
        float x, y;
        Bitmap splat;

        HitPoint(float xArg, float yArg)
        {
            x = xArg;
            y = yArg;
            splat = getFeatheredHitPoint(ballDiameter, ballDiameter);
        }
    }
}
