//	Contour - Android drawing teacher application
//	Copyright (C) 2014  Johannes Riedmann
//	
//	This program is free software: you can redistribute it and/or modify
//	it under the terms of the GNU General Public License as published by
//	the Free Software Foundation, either version 3 of the License, or
//	(at your option) any later version.
//	
//	This program is distributed in the hope that it will be useful,
//	but WITHOUT ANY WARRANTY; without even the implied warranty of
//	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//	GNU General Public License for more details.
//	
//	You should have received a copy of the GNU General Public License
//	along with this program.  If not, see <http://www.gnu.org/licenses/>.

package com.j0h1.contour;

import java.util.ArrayList;

import com.j0h1.contour.serialized.CustomMatrix;
import com.j0h1.contour.serialized.CustomPath;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.ImageView;

/**
 * Custom View that holds a Canvas the user can drawn on.
 * In the GameActivity the DrawingView's background is used for
 * the edge image, the user has to retrace. The Bitmap of the
 * DrawingView is the user drawing that is rendered by the Canvas.
 * @author Johannes Riedmann
 *
 */
public class DrawingView extends ImageView {

	private Bitmap bitmap;
	private Canvas canvas;
	private CustomPath currentPath;
	private Paint paint;
	
	Context context;
	
	private boolean sizeChanged = false;	
	private boolean isLocked = false;
	
	private int recentScreenOrientation;
	
	public static boolean IS_PORTRAIT_MODE;

	private ArrayList<CustomPath> paths = new ArrayList<CustomPath>();
	private ArrayList<CustomPath> undonePaths= new ArrayList<CustomPath>();
	
	private float strokeWidth = 9f;

	public DrawingView(Context c) {
		super(c);
		context = c;		
		definePaintAndBg();
	}
	
	public DrawingView(Context c, AttributeSet as) {
		super(c, as);		
		context = c;
		definePaintAndBg();
	}
	
	public DrawingView(Context c, AttributeSet as, int defStyle) {
		super(c, as, defStyle);		
		context = c;
		definePaintAndBg();
	}
	
	private void definePaintAndBg() {
		paint = new Paint();
		paint.setStyle(Paint.Style.STROKE);
		paint.setStrokeWidth(strokeWidth);
//		paint.setAntiAlias(true);
	}
	
	public void setHeight(int height) {
		getLayoutParams().height = height;
	}
	
	public void setWidth(int width) {
		getLayoutParams().width = width;
	}

	/**
	 * This method is called as soon as DrawingView changes it's size, which is
	 * at the start up and when the player changes the display orientation. When
	 * the display orientation changes, the paths have to be transformed according
	 * to the new dimensions of the DrawingView.
	 */
	@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		super.onSizeChanged(w, h, oldw, oldh);
		
		resetBitmapAndCanvas();
		
		// don't call onSizeChanged before DrawingView is set up and the game has
		// actually started
		if (oldw == 0 || oldh == 0 || !GameActivity.GAME_STARTED) {
			return;
		}
		
		// iterate through the paths and scale/translate them according to the new
		// image resolution
		for (CustomPath path : paths) {
			RectF pathBounds = new RectF();
			
			path.computeBounds(pathBounds, true);
			
			// defining scale factor (width and height have the same scale factor)
			float scaleFactor = (float) w / (float) oldw;
			
			float[] pathBoundArray = new float[4];
			pathBoundArray[0] = pathBounds.left;
			pathBoundArray[1] = pathBounds.top;
			pathBoundArray[2] = pathBounds.right;
			pathBoundArray[3] = pathBounds.bottom;
			
			CustomMatrix manipulationMatrix = 
					defineTransformationMatrix(pathBoundArray, scaleFactor);
			
			// apply the transformations to the path
			path.transform(manipulationMatrix); 
			
			// set new bounds and scale factor
			path.computeBounds(pathBounds, true);
			
			float[] transformedBounds = new float[4];
			transformedBounds[0] = pathBounds.left;
			transformedBounds[1] = pathBounds.top;
			transformedBounds[2] = pathBounds.right;
			transformedBounds[3] = pathBounds.bottom;
			
			path.setPathBounds(transformedBounds);
			
			path.setScaleFactor(scaleFactor);
		}
		
		invalidate();
	}
	
	/**
	 * Defines a transformation matrix for the given parameters.
	 * @param pathBounds Rectangular border of path object.
	 * @param scaleFactor Factor the path object is scaled.
	 * @return
	 */
	private CustomMatrix defineTransformationMatrix(float[] pathBounds, float scaleFactor) {
		CustomMatrix tranformMatrix = new CustomMatrix();
		
		float centerX = pathBounds[2] - pathBounds[0];
		float centerY = pathBounds[3] - pathBounds[1];
		
		// first scale the path with it's center fixed
		tranformMatrix.setScale(scaleFactor, scaleFactor, centerX, centerY);
		
		// then translate the path to it's position in the rescaled image
		float translateX = centerX - centerX * scaleFactor;
		float translateY = centerY - centerY * scaleFactor;
		
		tranformMatrix.postTranslate(-translateX, -translateY);
		
		return tranformMatrix;
	}

	/**
	 * Draw paths to the canvas.
	 */
	@Override
	protected void onDraw(Canvas canvas) {
		// draw all paths stored in the paths list
		for (CustomPath p : paths) {
			canvas.drawPath(p, paint);
		}
	}

	private float mX, mY;
	private static final float TOUCH_TOLERANCE = 0;

	/**
	 * When the user starts a touch interaction with the DrawingView,
	 * initialize a path and add it to the path list
	 * @param x X-coordinate of the users finger on the screen.
	 * @param y Y-coordinate of the users finger on the screen.
	 */
	private void touch_start(float x, float y) {
		currentPath = new CustomPath();
		currentPath.setPortraitPath(IS_PORTRAIT_MODE);
		paths.add(currentPath);
		currentPath.moveTo(x, y);
		mX = x;
		mY = y;
	}

	/**
	 * When the user moves his finger around the DrawingView, the
	 * path is drawn via the quadTo method.
	 * @param x 
	 * @param y Y-coordinate of the users finger on the screen.
	 */
	private void touch_move(float x, float y) {
		float dx = Math.abs(x - mX);
		float dy = Math.abs(y - mY);

		if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
			currentPath.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
			mX = x;
			mY = y;
		}
	}

	/**
	 * When the user stops interacting with the DrawingView, the last
	 * line is drawn and the path is committed to the offscreen.
	 */
	private void touch_up() {
		currentPath.lineTo(mX, mY);
		
		// commit the path to our offscreen
		canvas.drawPath(currentPath, paint);
	}

	/**
	 * When the undo button is clicked, remove the last path that 
	 * was added and add it to the deleted paths list.
	 * @return True, if a path was removed, false otherwise.
	 */
	public boolean onClickUndo() {
		if (paths.size() > 0) {
			undonePaths.add(paths.remove(paths.size() - 1));
			repaint();
			
			return true;
		}
		
		return false;
	}

	/**
	 * Add the most recently deleted path from the deleted list to
	 * the paths list and make it available again for the onDraw
	 * method.
	 * @return True, if a path was added, false otherwise.
	 */
	public boolean onClickRedo() {
		if (undonePaths.size() > 0) {
			paths.add(undonePaths.remove(undonePaths.size() - 1));
			repaint();
			
			return true;
		}
		
		return false;
	}
	
	/**
	 * Delete all removed and added paths and clear the canvas and
	 * bitmap, held by the DrawingView.
	 */
	public void onClickReset() {
    	paths.clear();
    	undonePaths.clear();
    	resetBitmapAndCanvas();
    	invalidate();
    }
	
	/**
	 * Define which action to take when the user interacts with
	 * the DrawingView.
	 */
	@Override
    public boolean onTouchEvent(MotionEvent event) {
		if (isLocked) {
			return true;
		}
		
        float x = event.getX();
        float y = event.getY();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                touch_start(x, y);
                invalidate();
                break;
            case MotionEvent.ACTION_MOVE:
                touch_move(x, y);
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                touch_up();
                invalidate();
                break;
        }
        
        return true;
    }
	
	/**
	 * Resets the canvas and bitmap held by the DrawingView and
	 * draws the paths manually.
	 */
	public void repaint() {
		// commit the paths to our offscreen
		resetBitmapAndCanvas();
		
		for (CustomPath p : paths) {
			canvas.drawPath(p, paint);
		}
		
		invalidate();
	}
	
	/**
	 * Draws paths from a previously paused session to the DrawingView.
	 * @param previousPaths List of paths from a previously paused session.
	 */
	public void drawPreviousPaths(ArrayList<CustomPath> previousPaths) {
		paths.addAll(previousPaths);
		
		// draw paths
		for (CustomPath p : paths) {
			// if this path is not a path drawn in the current mode, 
			// translate and scale it
			if (recentScreenOrientation == 1 && 
					IS_PORTRAIT_MODE && !p.isPortraitPath()) {
				if (p.getPathBounds() != null) {
					CustomMatrix manipulationMatrix = 
							defineTransformationMatrix(p.getPathBounds(), 
									p.getScaleFactor());
					p.transform(manipulationMatrix);
				}
			}
			
			if (recentScreenOrientation == 2 &&
					!IS_PORTRAIT_MODE && p.isPortraitPath()) {
				if (p.getPathBounds() != null) {
					CustomMatrix manipulationMatrix = 
							defineTransformationMatrix(p.getPathBounds(), 
									p.getScaleFactor());
					p.transform(manipulationMatrix);
				}				
			}
			
			// draw the path (whether if it was transformed or not)
			p.drawThisPath();
		}
	}
	
	/**
	 * Resets the current Bitmap and Canvas object held by the DrawingView.
	 */
	private void resetBitmapAndCanvas() {
		bitmap = Bitmap.createBitmap(getWidth(), getHeight(), 
				Bitmap.Config.ARGB_8888);
		
		if (canvas == null) {
			canvas = new Canvas(bitmap);
		} else {
			canvas.setBitmap(bitmap);
		}
	}
	
	public ArrayList<CustomPath> getPaths() {
		return paths;
	}
	
	public void setPaths(ArrayList<CustomPath> paths) {
		this.paths = paths;
	}
	
	public boolean getLocked() {
		return isLocked;
	}
	
	public void setLocked(boolean locked) {
		isLocked = locked;
	}
	
	public Bitmap getBitmap() {
		return bitmap;
	}
	
	public void setBitmap(Bitmap bitmap) {
		this.bitmap = bitmap;
	}
	
	public float getStrokeWidth() {
		return strokeWidth;
	}
	
	public void setStrokeWidth(float strokeWidth) {
		this.strokeWidth = strokeWidth;
	}

	public int getRecentScreenOrientation() {
		return recentScreenOrientation;
	}

	public void setRecentScreenOrientation(int recentScreenOrientation) {
		this.recentScreenOrientation = recentScreenOrientation;
	}

	public boolean isSizeChanged() {
		return sizeChanged;
	}

	public void setSizeChanged(boolean sizeChanged) {
		this.sizeChanged = sizeChanged;
	}

}
