/**
 * Part of the SurfaceMapper library: http://surfacemapper.sourceforge.net/
 * Copyright (c) 2011-12 Ixagon AB 
 *
 * This source 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 2 of the License, or
 * (at your option) any later version.
 *
 * This code 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.
 *
 * A copy of the GNU General Public License is available on the World
 * Wide Web at <http://www.gnu.org/copyleft/gpl.html>. You can also
 * obtain it by writing to the Free Software Foundation,
 * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

package ixagon.surface.mapper;

import jto.processing.sketch.mapper.Sketch;
import processing.core.PApplet;
import processing.core.PGraphics;
import processing.core.PImage;
import processing.core.PVector;
import processing.data.XML;

import java.awt.*;
import java.io.File;

//Parts derived from MappingTools library

public class BezierSurface implements SuperSurface {

    static private int GRID_LINE_COLOR;
    static private int GRID_LINE_SELECTED_COLOR;
    static private int SELECTED_OUTLINE_OUTER_COLOR;
    static private int CORNER_MARKER_COLOR;
    static private int SELECTED_OUTLINE_INNER_COLOR;
    static private int SELECTED_CORNER_MARKER_COLOR;
    final private int MODE_RENDER = 0;
    final private int MODE_CALIBRATE = 1;
    private PApplet parent;
    private SurfaceMapper sm;
    private int MODE = MODE_RENDER;
    private int activePoint = -1; // Which corner point is selected?
    // Corners of the Surface
    private Point3D[] cornerPoints;

    // Contains all coordinates
    private Point3D[][] vertexPoints;

    // Coordinates of the bezier vectors
    private Point3D[] bezierPoints;

    private PVector[] textureWindow = new PVector[2];

    // Displacement forces

    private int horizontalForce = 0;
    private int verticalForce = 0;

    private int GRID_RESOLUTION;
    private int surfaceId;

    private boolean isSelected;
    private boolean isLocked;
    private int selectedCorner;
    private int selectedBezierControl;

    private int ccolor = 0xFF3c3c3c;

    private String surfaceName;

    private Polygon poly = new Polygon();

    private float currentZ;
    private boolean shaking;
    private float shakeStrength;
    private int shakeSpeed;
    private int fallOfSpeed;
    private float shakeAngle;

    private boolean hidden = false;

    private PImage surfaceMask;
    private PImage maskedTex;
    private File maskFile;
    private PImage maskFilter;
    private boolean usingEdgeBlend = false;
    private PImage edgeBlendTex;
    private boolean blendRight = false, blendLeft = false;
    private float blendRightSize = 0, blendLeftSize = 0;
    private PGraphics blendScreen;
    private PGraphics bufferScreen;
    private int bufferScreenWidth = 0;

    private Sketch sketch;
    private int sketchIndex;

    /**
     * Constructor for creating a new surface at X,Y with RES subdivision.
     *
     * @param parent
     * @param ks
     * @param x
     * @param y
     * @param res
     * @param id
     */
    BezierSurface(PApplet parent, SurfaceMapper ks, float x, float y, int res, int id) {
        if (res % 2 != 0) res++;
        init(parent, ks, res, id, null);

        this.cornerPoints[0].x = (float) (x - (SuperSurface.DEFAULT_SIZE * 0.5));
        this.cornerPoints[0].y = (float) (y - (SuperSurface.DEFAULT_SIZE * 0.5));

        this.cornerPoints[1].x = (float) (x + (SuperSurface.DEFAULT_SIZE * 0.5));
        this.cornerPoints[1].y = (float) (y - (SuperSurface.DEFAULT_SIZE * 0.5));

        this.cornerPoints[2].x = (float) (x + (SuperSurface.DEFAULT_SIZE * 0.5));
        this.cornerPoints[2].y = (float) (y + (SuperSurface.DEFAULT_SIZE * 0.5));

        this.cornerPoints[3].x = (float) (x - (SuperSurface.DEFAULT_SIZE * 0.5));
        this.cornerPoints[3].y = (float) (y + (SuperSurface.DEFAULT_SIZE * 0.5));

        //bezier points init

        this.bezierPoints[0].x = (float) (this.cornerPoints[0].x + (SuperSurface.DEFAULT_SIZE * 0.0));
        this.bezierPoints[0].y = (float) (this.cornerPoints[0].y + (SuperSurface.DEFAULT_SIZE * 0.3));

        this.bezierPoints[1].x = (float) (this.cornerPoints[0].x + (SuperSurface.DEFAULT_SIZE * 0.3));
        this.bezierPoints[1].y = (float) (this.cornerPoints[0].y + (SuperSurface.DEFAULT_SIZE * 0.0));

        this.bezierPoints[2].x = (float) (this.cornerPoints[1].x - (SuperSurface.DEFAULT_SIZE * 0.3));
        this.bezierPoints[2].y = (float) (this.cornerPoints[1].y + (SuperSurface.DEFAULT_SIZE * 0.0));

        this.bezierPoints[3].x = (float) (this.cornerPoints[1].x - (SuperSurface.DEFAULT_SIZE * 0.0));
        this.bezierPoints[3].y = (float) (this.cornerPoints[1].y + (SuperSurface.DEFAULT_SIZE * 0.3));

        this.bezierPoints[4].x = (float) (this.cornerPoints[2].x - (SuperSurface.DEFAULT_SIZE * 0.0));
        this.bezierPoints[4].y = (float) (this.cornerPoints[2].y - (SuperSurface.DEFAULT_SIZE * 0.3));

        this.bezierPoints[5].x = (float) (this.cornerPoints[2].x - (SuperSurface.DEFAULT_SIZE * 0.3));
        this.bezierPoints[5].y = (float) (this.cornerPoints[2].y - (SuperSurface.DEFAULT_SIZE * 0.0));

        this.bezierPoints[6].x = (float) (this.cornerPoints[3].x + (SuperSurface.DEFAULT_SIZE * 0.3));
        this.bezierPoints[6].y = (float) (this.cornerPoints[3].y + (SuperSurface.DEFAULT_SIZE * 0.0));

        this.bezierPoints[7].x = (float) (this.cornerPoints[3].x - (SuperSurface.DEFAULT_SIZE * 0.0));
        this.bezierPoints[7].y = (float) (this.cornerPoints[3].y - (SuperSurface.DEFAULT_SIZE * 0.3));

        this.setTextureWindow(0, 0, 1, 1);

        this.updateTransform();
    }

    /**
     * Constructor used when loading a surface from file
     *
     * @param parent
     * @param ks
     * @param xml
     * @param name
     * @param id
     */
    BezierSurface(PApplet parent, SurfaceMapper ks, XML xml, int id, String name) {

        init(parent, ks, (xml.getInt("res")), id, name);

        if (Boolean.TRUE.equals(Boolean.valueOf(xml.getString("lock")))) {
            this.toggleLocked();
        }

        for (Sketch sketch : ks.getSketchList()) {
            if (sketch.getName().equals(xml.getString("sketch"))) {
                setSketch(sketch);
                break;
            }
        }

        // reload the Corners
        for (int i = 0; i < xml.getChildCount(); i++) {
            XML point = xml.getChild(i);
            if (point.getName().equals("cornerpoint"))
                setCornerPoint(point.getInt("i"), point.getFloat("x"), point.getFloat("y"));
            if (point.getName().equals("bezierpoint"))
                this.setBezierPoint(point.getInt("i"), point.getFloat("x"), point.getFloat("y"));
        }

        horizontalForce = xml.getInt("horizontalForce");
        verticalForce = xml.getInt("verticalForce");

        PVector offset = new PVector(0, 0);
        PVector size = new PVector(1, 1);
        for (XML xo : xml.getChildren()) {
            if (xo.getName().equalsIgnoreCase("texturewindowoffset")) {
                offset = new PVector(xo.getFloat("x"), xo.getFloat("y"));
            }
            if (xo.getName().equalsIgnoreCase("texturewindowsize")) {
                size = new PVector(xo.getFloat("x"), xo.getFloat("y"));
            }
            if (xo.getName().equalsIgnoreCase("surfacemask")) {
                //this.setSurfaceMask(new PImage(parent, xo.getString("path")+"/"+xo.getString("filename")));
                this.setMaskFile(new File(xo.getString("path") + "/" + xo.getString("filename")));
            }
            if (xo.getName().equalsIgnoreCase("blendleft")) {
                this.setBlendLeft(true);
                this.setBlendLeftSize(xo.getFloat("blendsize"));
            }
            if (xo.getName().equalsIgnoreCase("blendright")) {
                this.setBlendRight(true);
                this.setBlendRightSize(xo.getFloat("blendsize"));
            }
        }
        this.setTextureWindow(offset.x, offset.y, size.x, size.y);


        this.updateTransform();
    }

    private void applyEdgeBlendToTexture(PImage tex) {
        if (this.isUsingEdgeBlend()) {

            if (maskedTex == null) {
                maskedTex = new PImage(parent.width, parent.height);
            }
//			maskFilter.setParameterValue("mask_factor", 0.0f);
//			maskFilter.apply(new GLTexture[]{tex, blendScreen.getTexture()}, maskedTex);
        }
    }

    public void clearSurfaceMask() {
        surfaceMask = null;
        maskFile = null;
    }

    /**
     * Decrease the amount of horizontal displacement force used for spherical mapping for bezier surfaces. (using orthographic projection)
     */
    public void decreaseHorizontalForce() {
        this.horizontalForce -= 2;
        this.updateTransform();
    }

    /**
     * Decrease the subdivision
     */
    public void decreaseResolution() {
        if ((this.GRID_RESOLUTION - 1) > 2) {
            this.GRID_RESOLUTION -= 2;
            this.vertexPoints = new Point3D[this.GRID_RESOLUTION + 1][this.GRID_RESOLUTION + 1];
            this.updateTransform();
        }
    }

    /**
     * Decrease the amount of vertical displacement force used for spherical mapping for bezier surfaces. (using orthographic projection)
     */
    public void decreaseVerticalForce() {
        this.verticalForce -= 2;
        this.updateTransform();
    }

    /**
     * Returns index 0-7 if coordinates are on a bezier control
     *
     * @param mX
     * @param mY
     * @return
     */
    public int getActiveBezierPointIndex(int mX, int mY) {
        for (int i = 0; i < this.bezierPoints.length; i++) {
            if (PApplet.dist(mX, mY, this.bezierPoints[i].x, this.bezierPoints[i].y) < sm.getSelectionDistance()) {
                this.setSelectedBezierControl(i);
                return i;
            }
        }
        return -1;
    }

    /**
     * Returns index 0-3 if coordinates are near a corner or index 4 if on a surface
     *
     * @param mX
     * @param mY
     * @return
     */
    public int getActiveCornerPointIndex(int mX, int mY) {
        for (int i = 0; i < this.cornerPoints.length; i++) {
            if (PApplet.dist(mX, mY, this.cornerPoints[i].x, this.cornerPoints[i].y) < sm.getSelectionDistance()) {
                setSelectedCorner(i);
                return i;
            }
        }
        if (this.isInside(mX, mY))
            return 2000;
        return -1;
    }

    /**
     * Get the index of active corner (or surface)
     *
     * @return
     */
    public int getActivePoint() {
        return this.activePoint;
    }

    public double getArea() {
        return 0;
    }

    /**
     * Get the target bezier point
     *
     * @param index
     * @return
     */
    public Point3D getBezierPoint(int index) {
        return this.bezierPoints[index];
    }

    /**
     * Get all bezier points
     *
     * @return
     */
    public Point3D[] getBezierPoints() {
        return this.bezierPoints;
    }

    public float getBlendLeftSize() {
        return blendLeftSize;
    }

    public float getBlendRightSize() {
        return blendRightSize;
    }

    public int getBufferScreenWidth() {
        return bufferScreenWidth;
    }

    /**
     * Get the average center point of the surface
     *
     * @return
     */
    public Point3D getCenter() {
        // Find the average position of all the control points, use that as the
        // center point.
        float avgX = 0;
        float avgY = 0;
        for (int c = 0; c < 4; c++) {
            avgX += this.cornerPoints[c].x;
            avgY += this.cornerPoints[c].y;
        }
        avgX /= 4;
        avgY /= 4;

        return new Point3D(avgX, avgY);
    }

    /**
     * Get the fill color of the surface in calibration mode
     *
     * @return
     */
    public int getColor() {
        return ccolor;
    }

    /**
     * Get the target corner point
     *
     * @param index
     * @return
     */
    public Point3D getCornerPoint(int index) {
        return this.cornerPoints[index];
    }

    /**
     * Get all corner points
     *
     * @return
     */
    public Point3D[] getCornerPoints() {
        return this.cornerPoints;
    }

    /**
     * Get the amount of horizontal displacement force used for spherical mapping for bezier surfaces.
     *
     * @return
     */
    public int getHorizontalForce() {
        return horizontalForce;
    }

    /**
     * Get the surfaces ID
     *
     * @return
     */
    public int getId() {
        return this.surfaceId;
    }

    /**
     * See if the surface is locked
     *
     * @return
     */
    public boolean getLocked() {
        return this.isLocked;
    }

    /**
     * Get the longest side as double
     *
     * @return
     */
    public double getLongestSide() {
        double[] longest = new double[4];
        longest[0] = PVector.dist(new PVector(cornerPoints[0].x, cornerPoints[0].y), new PVector(cornerPoints[1].x, cornerPoints[1].y));
        longest[1] = PVector.dist(new PVector(cornerPoints[2].x, cornerPoints[2].y), new PVector(cornerPoints[3].x, cornerPoints[3].y));
        longest[2] = PVector.dist(new PVector(cornerPoints[0].x, cornerPoints[0].y), new PVector(cornerPoints[3].x, cornerPoints[3].y));
        longest[3] = PVector.dist(new PVector(cornerPoints[1].x, cornerPoints[1].y), new PVector(cornerPoints[2].x, cornerPoints[2].y));

        double longer = 0;
        for (int i = 0; i < longest.length; i++) {
            if (longest[i] > longer) longer = longest[i];
        }
        return longer;
    }

    public File getMaskFile() {
        return maskFile;
    }

    /**
     * Get the surfaces polygon
     *
     * @return
     */
    public Polygon getPolygon() {
        return poly;
    }

    /**
     * Get the amount of subdivision used in the surface
     *
     * @return
     */
    public int getRes() {
        // The actual resolution is the number of tiles, not the number of mesh
        // points
        return GRID_RESOLUTION;
    }

    /**
     * Get the currently selected bezier control
     *
     * @return
     */
    public int getSelectedBezierControl() {
        return selectedBezierControl;
    }

    /**
     * Get the currently selected corner
     *
     * @return
     */
    public int getSelectedCorner() {
        return this.selectedCorner;
    }

    public Sketch getSketch() {
        return sketch;
    }

    public PImage getSurfaceMask() {
        return surfaceMask;
    }

    public String getSurfaceName() {
        if (surfaceName == null) return String.valueOf(this.getId());
        return surfaceName;
    }

    @Override
    public int getSurfaceType() {
        return SuperSurface.BEZIER;
    }

    public PVector[] getTextureWindow() {
        return this.textureWindow;
    }

    /**
     * Get the amount of vertical displacement force used for spherical mapping for bezier surfaces.
     *
     * @return
     */
    public int getVerticalForce() {
        return verticalForce;
    }

    /**
     * Increase the amount of horizontal displacement force used for spherical mapping for bezier surfaces. (using orthographic projection)
     */
    public void increaseHorizontalForce() {
        this.horizontalForce += 2;
        this.updateTransform();
    }

    /**
     * Increase the subdivision
     */
    public void increaseResolution() {
        this.GRID_RESOLUTION += 2;
        this.vertexPoints = new Point3D[this.GRID_RESOLUTION + 1][this.GRID_RESOLUTION + 1];
        this.updateTransform();
    }

    /**
     * Increase the amount of vertical displacement force used for spherical mapping for bezier surfaces. (using orthographic projection)
     */
    public void increaseVerticalForce() {
        this.verticalForce += 2;
        this.updateTransform();

    }

    /**
     * Convenience method used by the constructors.
     *
     * @param parent
     * @param ks
     * @param res
     * @param id
     */
    private void init(PApplet parent, SurfaceMapper ks, int res, int id, String name) {
        this.parent = parent;
        this.sm = ks;
        this.surfaceName = name;
        this.surfaceId = id;
        this.GRID_RESOLUTION = res;
        this.horizontalForce = 0;
        this.verticalForce = 0;
        this.selectedBezierControl = -1;

        this.cornerPoints = new Point3D[4];
        this.bezierPoints = new Point3D[8];
        this.vertexPoints = new Point3D[this.GRID_RESOLUTION + 1][this.GRID_RESOLUTION + 1];

        for (int i = 0; i < this.cornerPoints.length; i++) {
            this.cornerPoints[i] = new Point3D();
        }

        for (int i = 0; i < this.bezierPoints.length; i++) {
            this.bezierPoints[i] = new Point3D();
        }

        GRID_LINE_COLOR = parent.color(160, 160, 160, 50);
        GRID_LINE_SELECTED_COLOR = parent.color(255, 128, 0);
        SELECTED_OUTLINE_OUTER_COLOR = parent.color(255, 128, 0, 128);
        SELECTED_OUTLINE_INNER_COLOR = parent.color(255, 128, 50);
        CORNER_MARKER_COLOR = parent.color(255, 255, 255, 100);
        SELECTED_CORNER_MARKER_COLOR = parent.color(0, 255, 255);

        this.updateTransform();

        //maskFilter = new PImage(parent, "Mask.xml");
    }

    public boolean isBlendLeft() {
        return blendLeft;
    }

    public boolean isBlendRight() {
        return blendRight;
    }

    public boolean isCornerMovementAllowed() {
        return true;
    }

    /**
     * See if surface is hidden
     *
     * @return
     */
    public boolean isHidden() {
        return hidden;
    }

    /**
     * Returns true if coordinates are inside a surface
     *
     * @param mX
     * @param mY
     * @return
     */
    public boolean isInside(float mX, float mY) {
        if (poly.contains(mX, mY)) return true;
        return false;
    }

    @Override
    public boolean isLocked() {
        return this.isLocked;
    }

    /**
     * See if the surface is selected
     *
     * @return
     */
    public boolean isSelected() {
        return this.isSelected;
    }

    public boolean isUsingEdgeBlend() {
        if (this.isBlendLeft() || this.isBlendRight()) return true;
        return false;
    }

    public boolean isUsingSurfaceMask() {
        if (surfaceMask != null) return true;
        return false;
    }

    /**
     * Render method for rendering while in calibration mode
     *
     * @param g
     */
    public void render(PGraphics g) {
        if (this.MODE == this.MODE_CALIBRATE && !this.isHidden()) {
            this.renderGrid(g);
        }
    }

    /**
     * Render method for rendering in RENDER mode.
     * Takes one GLGraphicsOffScreen and one GLTexture. The GLTexture is the texture used for the surface, and is drawn to the offscreen buffer.
     *
     * @param g
     * @param tex
     */
    public void render(PGraphics g, PImage tex) {
        if (this.isHidden()) return;
        this.renderSurface(g, tex);
    }

    /**
     * Draws the bezier points
     *
     * @param g
     * @param x
     * @param y
     * @param selected
     * @param cornerIndex
     */
    private void renderBezierPoint(PGraphics g, float x, float y, boolean selected, int cornerIndex) {
        g.noFill();
        g.strokeWeight(1);
        if (selected) {
            g.stroke(BezierSurface.SELECTED_CORNER_MARKER_COLOR);
        } else {
            g.stroke(BezierSurface.CORNER_MARKER_COLOR);
        }
        if (cornerIndex == getSelectedBezierControl() && isSelected()) {
            g.fill(BezierSurface.SELECTED_CORNER_MARKER_COLOR, 100);
            g.stroke(BezierSurface.SELECTED_CORNER_MARKER_COLOR);
        }
        g.ellipse(x, y, 10, 10);
        g.line(x, y - 5, x, y + 5);
        g.line(x - 5, y, x + 5, y);
    }

    /**
     * Draws the Corner points
     *
     * @param g
     * @param x
     * @param y
     * @param selected
     * @param cornerIndex
     */
    private void renderCornerPoint(PGraphics g, float x, float y, boolean selected, int cornerIndex) {
        g.noFill();
        g.strokeWeight(2);
        if (selected) {
            g.stroke(BezierSurface.SELECTED_CORNER_MARKER_COLOR);
        } else {
            g.stroke(BezierSurface.CORNER_MARKER_COLOR);
        }
        if (cornerIndex == getSelectedCorner() && isSelected()) {
            g.fill(BezierSurface.SELECTED_CORNER_MARKER_COLOR, 100);
            g.stroke(BezierSurface.SELECTED_CORNER_MARKER_COLOR);
        }
        g.ellipse(x, y, 10, 10);
        g.line(x, y - 5, x, y + 5);
        g.line(x - 5, y, x + 5, y);
    }

    /**
     * Renders the grid in the surface. (useful in calibration mode)
     *
     * @param g
     */
    private void renderGrid(PGraphics g) {
        g.beginDraw();


        g.fill(ccolor);

        g.noStroke();
        for (int i = 0; i < GRID_RESOLUTION; i++) {
            for (int j = 0; j < GRID_RESOLUTION; j++) {

                g.beginShape();
                g.vertex(vertexPoints[i][j].x, vertexPoints[i][j].y);
                g.vertex(vertexPoints[i + 1][j].x, vertexPoints[i + 1][j].y);
                g.vertex(vertexPoints[i + 1][j + 1].x, vertexPoints[i + 1][j + 1].y);
                g.vertex(vertexPoints[i][j + 1].x, vertexPoints[i][j + 1].y);
                g.endShape();

            }
        }

        g.textFont(sm.getIdFont());
        g.fill(255);
        g.textAlign(PApplet.CENTER, PApplet.CENTER);
        g.textSize(20);
        g.text("" + this.getSurfaceName(), (float) (this.getCenter().x), (float) this.getCenter().y);
        if (isLocked) {
            g.textSize(12);
            g.text("Surface locked", (float) this.getCenter().x, (float) this.getCenter().y + 26);
        }
        if (sketch != null) {
            g.textSize(10);
            g.text(sketch.getName(), (float) this.getCenter().x, (float) this.getCenter().y + 40);
        }


        g.noFill();
        g.stroke(BezierSurface.GRID_LINE_COLOR);
        g.strokeWeight(2);
        if (isSelected)
            g.stroke(BezierSurface.GRID_LINE_SELECTED_COLOR);

        if (!isLocked) {
            for (int i = 0; i <= GRID_RESOLUTION; i++) {
                for (int j = 0; j <= GRID_RESOLUTION; j++) {
                    g.point(vertexPoints[i][j].x, vertexPoints[i][j].y, vertexPoints[i][j].z);
                }
            }
        }

        if (isSelected) {
            g.strokeWeight(4);
            g.stroke(BezierSurface.GRID_LINE_SELECTED_COLOR);

            //draw the outline here
            for (int i = 0; i < poly.npoints - 1; i++) {
                g.line(poly.xpoints[i], poly.ypoints[i], poly.xpoints[i + 1], poly.ypoints[i + 1]);
                if (i == poly.npoints - 2)
                    g.line(poly.xpoints[i + 1], poly.ypoints[i + 1], poly.xpoints[0], poly.ypoints[0]);
            }
        }

        g.strokeWeight(1);
        g.stroke(SELECTED_OUTLINE_INNER_COLOR);
        //draw the outline here
        for (int i = 0; i < poly.npoints - 1; i++) {
            g.line(poly.xpoints[i], poly.ypoints[i], poly.xpoints[i + 1], poly.ypoints[i + 1]);
            if (i == poly.npoints - 2)
                g.line(poly.xpoints[i + 1], poly.ypoints[i + 1], poly.xpoints[0], poly.ypoints[0]);
        }


        if (!isLocked) {
            // Draw the control points.
            for (int i = 0; i < this.cornerPoints.length; i++) {
                this.renderCornerPoint(g, this.cornerPoints[i].x, this.cornerPoints[i].y, (this.activePoint == i), i);

            }

            for (int i = 0; i < this.bezierPoints.length; i++) {
                this.renderBezierPoint(g, this.bezierPoints[i].x, this.bezierPoints[i].y, (this.selectedBezierControl == i), i);
                g.strokeWeight(1);
                g.stroke(255);
                g.line(this.bezierPoints[i].x, this.bezierPoints[i].y, this.cornerPoints[(int) (i / 2)].x, this.cornerPoints[(int) (i / 2)].y);
            }

        }

        g.endDraw();
    }

    /**
     * Actual rendering of the surface. Is called from the render method.
     * Should normally not be accessed directly.
     *
     * @param g
     * @param tex
     */
    private void renderSurface(PGraphics g, PImage tex) {
        float tWidth = 1;
        float tHeight = 1;
        float tOffX = 0;
        float tOffY = 0;

        tWidth = tex.width * (textureWindow[1].x);
        tHeight = tex.height * (textureWindow[1].y);
        tOffX = tex.width * textureWindow[0].x;
        tOffY = tex.height * textureWindow[0].y;

        if (this.isUsingEdgeBlend() || this.isUsingSurfaceMask()) {

            if (bufferScreen == null || bufferScreen.width != this.getBufferScreenWidth()) {
                bufferScreen = parent.createGraphics(this.getBufferScreenWidth(), this.getBufferScreenWidth());
            }
            bufferScreen.beginDraw();
            bufferScreen.beginShape(PApplet.QUADS);
            bufferScreen.texture(tex);
            bufferScreen.vertex(0, 0, tOffX, tOffY);
            bufferScreen.vertex(bufferScreen.width, 0, tWidth + tOffX, tOffY);
            bufferScreen.vertex(bufferScreen.width, bufferScreen.height, tWidth + tOffX, tHeight + tOffY);
            bufferScreen.vertex(0, bufferScreen.height, tOffX, tHeight + tOffY);
            bufferScreen.endShape(PApplet.CLOSE);
            bufferScreen.endDraw();


            if (this.isUsingSurfaceMask()) {
//				maskFilter.setParameterValue("mask_factor", 0.0f);
//				maskFilter.apply(new GLTexture[]{bufferScreen.getTexture(), surfaceMask}, maskedTex);
//				applyEdgeBlendToTexture(maskedTex);
            } else {
                applyEdgeBlendToTexture(bufferScreen.get());
            }
        }
        g.beginDraw();
        g.noStroke();
        g.beginShape(PApplet.QUADS);

        if (this.isUsingSurfaceMask() || this.isUsingEdgeBlend()) {
            g.texture(maskedTex);
            tOffX = 0;
            tOffY = 0;
            tWidth = maskedTex.width;
            tHeight = maskedTex.height;
        } else {
            g.texture(tex);
            if (bufferScreen != null)
                bufferScreen = null;
        }


        for (int i = 0; i < GRID_RESOLUTION; i++) {
            for (int j = 0; j < GRID_RESOLUTION; j++) {


                g.vertex(vertexPoints[i][j].x,
                        vertexPoints[i][j].y,
                        vertexPoints[i][j].z + currentZ,
                        ((float) i / GRID_RESOLUTION) * tWidth + tOffX,
                        ((float) j / GRID_RESOLUTION) * tHeight + tOffY);

                g.vertex(vertexPoints[i + 1][j].x,
                        vertexPoints[i + 1][j].y,
                        vertexPoints[i + 1][j].z + currentZ,
                        (((float) i + 1) / GRID_RESOLUTION) * tWidth + tOffX,
                        ((float) j / GRID_RESOLUTION) * tHeight + tOffY);

                g.vertex(vertexPoints[i + 1][j + 1].x,
                        vertexPoints[i + 1][j + 1].y,
                        vertexPoints[i + 1][j + 1].z + currentZ,
                        (((float) i + 1) / GRID_RESOLUTION) * tWidth + tOffX,
                        (((float) j + 1) / GRID_RESOLUTION) * tHeight + tOffY);

                g.vertex(vertexPoints[i][j + 1].x,
                        vertexPoints[i][j + 1].y,
                        vertexPoints[i][j + 1].z + currentZ,
                        ((float) i / GRID_RESOLUTION) * tWidth + tOffX,
                        (((float) j + 1) / GRID_RESOLUTION) * tHeight + tOffY);


            }
        }
        g.endShape(PApplet.CLOSE);
        g.endDraw();
    }

    /**
     * Rotate the cornerpoints in direction (0=ClockWise 1=CounterClockWise)
     *
     * @param direction
     */
    public void rotateCornerPoints(int direction) {
//		Point3D[] sourcePoints = cornerPoints.clone();
//		switch(direction){
//		case 0:
//			cornerPoints[0] = sourcePoints[1];
//			cornerPoints[1] = sourcePoints[2];
//			cornerPoints[2] = sourcePoints[3];
//			cornerPoints[3] = sourcePoints[0];
//			this.updateTransform();
//			break;
//		case 1:
//			cornerPoints[0] = sourcePoints[3];
//			cornerPoints[1] = sourcePoints[0];
//			cornerPoints[2] = sourcePoints[1];
//			cornerPoints[3] = sourcePoints[2];
//			this.updateTransform();
//			break;
//		}
    }

    /**
     * Translates a point on the screen into a point in the surface. (not implemented in Bezier Surfaces yet)
     *
     * @param x
     * @param y
     * @return
     */
    public Point3D screenCoordinatesToQuad(float x, float y) {
        //TODO :: maybe add this code
        return null;
    }

    /**
     * Set index of which corner is active
     *
     * @param activePoint
     */
    public void setActivePoint(int activePoint) {
        this.activePoint = activePoint;
    }

    /**
     * Set target bezier control point to coordinates
     *
     * @param pointIndex
     * @param x
     * @param y
     */
    public void setBezierPoint(int pointIndex, float x, float y) {
        this.bezierPoints[pointIndex].x = x;
        this.bezierPoints[pointIndex].y = y;
        this.updateTransform();
    }

    public void setBlendLeft(boolean blendLeft) {
        this.blendLeft = blendLeft;
        updateBlendScreen();
    }

    public void setBlendLeftSize(float blendLeftSize) {
        this.blendLeftSize = blendLeftSize;
        updateBlendScreen();
    }

    public void setBlendRight(boolean blendRight) {
        this.blendRight = blendRight;
        updateBlendScreen();
    }

    public void setBlendRightSize(float blendRightSize) {
        this.blendRightSize = blendRightSize;
        updateBlendScreen();
    }

    public void setBufferScreenWidth(int bufferScreenWidth) {
        this.bufferScreenWidth = bufferScreenWidth;
    }

    /**
     * Sets the fill color of the surface in calibration mode
     *
     * @param ccolor
     */
    public void setColor(int ccolor) {
        this.ccolor = ccolor;
    }

    /**
     * Set target corner point to coordinates
     *
     * @param pointIndex
     * @param x
     * @param y
     */
    public void setCornerPoint(int pointIndex, float x, float y) {
        this.cornerPoints[pointIndex].x = x;
        this.cornerPoints[pointIndex].y = y;
        this.updateTransform();
    }

    /**
     * Manually set coordinates for all corners of the surface
     *
     * @param x0
     * @param y0
     * @param x1
     * @param y1
     * @param x2
     * @param y2
     * @param x3
     * @param y3
     */
    public void setCornerPoints(float x0, float y0, float x1, float y1, float x2, float y2, float x3, float y3) {
        this.cornerPoints[0].x = x0;
        this.cornerPoints[0].y = y0;

        this.cornerPoints[1].x = x1;
        this.cornerPoints[1].y = y1;

        this.cornerPoints[2].x = x2;
        this.cornerPoints[2].y = y2;

        this.cornerPoints[3].x = x3;
        this.cornerPoints[3].y = y3;

        this.updateTransform();
    }

    /**
     * Set if the surface should be hidden
     *
     * @param hidden
     */
    public void setHide(boolean hidden) {
        this.hidden = hidden;
    }

    /**
     * Set the surfaces ID
     *
     * @param id
     */
    public void setId(int id) {
        this.surfaceId = id;
    }

    /**
     * Set if the surface is locked
     *
     * @param isLocked
     */
    public void setLocked(boolean isLocked) {
        this.isLocked = isLocked;
    }

    public void setMaskFile(File maskFile) {
        this.maskFile = maskFile;
    }

    /**
     * Set surface to calibration mode
     */
    public void setModeCalibrate() {
        this.MODE = this.MODE_CALIBRATE;
    }

    /**
     * Set surface to render mode
     */
    public void setModeRender() {
        this.MODE = this.MODE_RENDER;
    }

    /**
     * Set if the surface is selected
     *
     * @param selected
     */
    public void setSelected(boolean selected) {
        this.isSelected = selected;
    }

    /**
     * Set target bezier control to selected
     *
     * @param selectedBezierControl
     */
    public void setSelectedBezierControl(int selectedBezierControl) {
        this.selectedBezierControl = selectedBezierControl;
    }

    /**
     * Set target corner to selected
     *
     * @param selectedCorner
     */
    public void setSelectedCorner(int selectedCorner) {
        this.selectedCorner = selectedCorner;
    }

    /**
     * Set parameters for shaking the surface. Strength == max Z-displacement, Speed == vibration speed, FallOfSpeed 1-1000 == how fast strength is diminished
     *
     * @param strength
     * @param speed
     * @param fallOfSpeed
     */
    public void setShake(int strength, int speed, int fallOfSpeed) {
        if (fallOfSpeed < 1) fallOfSpeed = 1;
        if (fallOfSpeed > 1000) fallOfSpeed = 1000;
        shaking = true;
        this.shakeStrength = strength;
        this.shakeSpeed = speed;
        this.fallOfSpeed = 1000 - fallOfSpeed;
        shakeAngle = 0;
    }

    public void setSketch(Sketch sketch) {
        this.sketch = sketch;
    }

    public void setSurfaceMask(PImage mask) {
        surfaceMask = mask;
        maskedTex = new PImage(parent.width, parent.height);
    }

    public void setSurfaceName(String surfaceName) {
        this.surfaceName = surfaceName;
    }

    /**
     * Manually set coordinates for mapping the texture. This allows for easy
     * cropping and enables a single texture to span more than one surface.
     * Use normalized values for the values! (i.e 0-1)
     */
    public void setTextureWindow(float x, float y, float width, float height) {
        x = (x > 0) ? x : 0;
        x = (x < 1) ? x : 1;
        y = (y > 0) ? y : 0;
        y = (y < 1) ? y : 1;

        width = (width > 0) ? width : 0;
        width = (width < 1) ? width : 1;
        height = (height > 0) ? height : 0;
        height = (height < 1) ? height : 1;

        textureWindow[0] = new PVector(x, y);
        textureWindow[1] = new PVector(width, height);
    }

    /**
     * Set Z-displacement for all coordinates of surface
     *
     * @param currentZ
     */
    public void setZ(float currentZ) {
        this.currentZ = currentZ;
    }
    
    public int getSketchIndex() {
		return sketchIndex;
	}

	public void setSketchIndex(int sketchIndex) {
		this.sketchIndex = sketchIndex;
	}    

    /**
     * Tells surface to shake (will only do something if setShake has been called quite recently)
     */
    public void shake() {
        if (shaking) {
            shakeAngle += (float) (shakeSpeed / 1000);
            shakeStrength *= ((float) this.fallOfSpeed / 1000);
            float shakeZ = (float) (Math.sin(shakeAngle) * shakeStrength);
            this.setZ(shakeZ);
            if (shakeStrength < 1) {
                shaking = false;
            }
        }
    }

    /**
     * Toggle if surface is locked
     */
    public void toggleLocked() {
        this.isLocked = !this.isLocked;
    }

    /**
     * Toggle surface mode
     */
    public void toggleMode() {
        if (this.MODE == this.MODE_RENDER) {
            this.MODE = this.MODE_CALIBRATE;
        } else {
            this.MODE = this.MODE_RENDER;
        }
    }

    private void updateBlendScreen() {
        if (blendScreen == null) {
            blendScreen = parent.createGraphics(512, 512);
        }

        blendScreen.beginDraw();

        if (this.isBlendLeft()) {
            blendScreen.noStroke();
            blendScreen.beginShape(PApplet.QUADS);
            blendScreen.texture(edgeBlendTex);
            blendScreen.vertex(-2, 0, 0, 0);
            blendScreen.vertex(blendScreen.width * this.getBlendLeftSize(), 0, edgeBlendTex.width, 0);
            blendScreen.vertex(blendScreen.width * this.getBlendLeftSize(), blendScreen.height, edgeBlendTex.width, edgeBlendTex.height);
            blendScreen.vertex(-2, blendScreen.height, 0, edgeBlendTex.height);
            blendScreen.endShape(PApplet.CLOSE);
        }
        if (this.isBlendRight()) {
            blendScreen.noStroke();
            blendScreen.beginShape(PApplet.QUADS);
            blendScreen.texture(edgeBlendTex);
            blendScreen.vertex(blendScreen.width - (blendScreen.width * this.getBlendRightSize()), 0, edgeBlendTex.width, 0);
            blendScreen.vertex(blendScreen.width + 2, 0, 0, 0);
            blendScreen.vertex(blendScreen.width + 2, blendScreen.height, 0, edgeBlendTex.height);
            blendScreen.vertex(blendScreen.width - (blendScreen.width * this.getBlendRightSize()), blendScreen.height, edgeBlendTex.width, edgeBlendTex.height);
            blendScreen.endShape(PApplet.CLOSE);
        }

        blendScreen.endDraw();
    }

    /**
     * Recalculates all coordinates of the surface.
     * Must be called whenever any change has been done to the surface.
     */
    public void updateTransform() {

        for (int i = 0; i <= GRID_RESOLUTION; i++) {
            for (int j = 0; j <= GRID_RESOLUTION; j++) {

                float start_x = parent.bezierPoint(cornerPoints[0].x, bezierPoints[0].x, bezierPoints[7].x, cornerPoints[3].x, (float) j / GRID_RESOLUTION);
                float end_x = parent.bezierPoint(cornerPoints[1].x, bezierPoints[3].x, bezierPoints[4].x, cornerPoints[2].x, (float) j / GRID_RESOLUTION);

                float start_y = parent.bezierPoint(cornerPoints[0].y, bezierPoints[0].y, bezierPoints[7].y, cornerPoints[3].y, (float) j / GRID_RESOLUTION);
                float end_y = parent.bezierPoint(cornerPoints[1].y, bezierPoints[3].y, bezierPoints[4].y, cornerPoints[2].y, (float) j / GRID_RESOLUTION);

                float x = parent.bezierPoint(start_x, ((bezierPoints[1].x - bezierPoints[6].x) * (1.0f - (float) j / GRID_RESOLUTION)) + bezierPoints[6].x, ((bezierPoints[2].x - bezierPoints[5].x) * (1.0f - (float) j / GRID_RESOLUTION)) + bezierPoints[5].x, end_x, (float) i / GRID_RESOLUTION);
                float y = parent.bezierPoint(start_y, ((bezierPoints[1].y - bezierPoints[6].y) * (1.0f - (float) j / GRID_RESOLUTION)) + bezierPoints[6].y, ((bezierPoints[2].y - bezierPoints[5].y) * (1.0f - (float) j / GRID_RESOLUTION)) + bezierPoints[5].y, end_y, (float) i / GRID_RESOLUTION);

                //the formula for Orthographic Projection
                //x = cos(latitude) * sin(longitude-referenceLongitude);
                //y = cos(referenceLatitude)*sin(latitude)-sin(referenceLatitude)*cos(latitude)*cos(longitude-referenceLongitude);
                //http://mathworld.wolfram.com/OrthographicProjection.html

                float pi1 = (float) ((Math.PI) / GRID_RESOLUTION);

                float xfix = (float) (Math.cos((j - (GRID_RESOLUTION / 2)) * pi1) * Math.sin((i * pi1) - ((float) (GRID_RESOLUTION / 2) * pi1))) * horizontalForce;
                float yfix = (float) (Math.cos((float) (GRID_RESOLUTION / 2) * pi1) * Math.sin(j * pi1) - Math.sin((float) (GRID_RESOLUTION / 2) * pi1) * Math.cos(j * pi1) * Math.cos((i * pi1) - ((float) (GRID_RESOLUTION / 2) * pi1))) * verticalForce;

                vertexPoints[i][j] = new Point3D(x + xfix, y + yfix, 0);
            }
        }

        poly = new Polygon();
        for (int w = 0; w < 4; w++) {
            for (int i = 0; i < GRID_RESOLUTION; i++) {
                switch (w) {
                    case 0:
                        poly.addPoint((int) vertexPoints[i][0].x, (int) vertexPoints[i][0].y);
                        break;

                    case 1:
                        poly.addPoint((int) vertexPoints[GRID_RESOLUTION][i].x, (int) vertexPoints[GRID_RESOLUTION][i].y);
                        break;

                    case 2:
                        poly.addPoint((int) vertexPoints[GRID_RESOLUTION - i][GRID_RESOLUTION].x, (int) vertexPoints[GRID_RESOLUTION - i][GRID_RESOLUTION].y);
                        break;

                    case 3:
                        poly.addPoint((int) vertexPoints[0][GRID_RESOLUTION - i].x, (int) vertexPoints[0][GRID_RESOLUTION - i].y);
                        break;
                }
            }
        }
    }
}