import ij.*;
import ij.process.*;
import ij.gui.*;
import java.awt.*;
import java.awt.event.*;
import ij.plugin.filter.*;

import java.awt.image.*;

import java.util.*;

/**
 *      updated 5/9/2014: Uses the correct dimensions for the image and preserves slice labels.
 *
 *      updated 5/8/2014: When the output image is created it loops through all of the images in the
 *      image stack, not just all of the slices.
 *
  *     A plugin for collecting a rectangular region by cropping
  *     and rotating the region.  For RGB images bilinear interpolation
  *     is used, for Anything else, the default interpolation is used.
  *
  *     @Author: Matthew B. Smith
  *     @Url: http://orangepalantir.org
  *     @Version: 0.9
  *     @Date: 5/9/2014
  *
  **/
public class Mouse_X extends MouseAdapter implements PlugInFilter {

    ImagePlus imp,drawplus;
    ImageCanvas drawcanvas;
    ImageProcessor drawproc,bufferproc;

    ArrayList<int[]> points;
    int state;


    public int setup(String arg, ImagePlus imp) {
        this.imp = imp;
        return DOES_ALL;
    }

    /**
      *     This will create a new image plus to draw on so that you can
      *     click on the canvas to select the four points necessary
      *
      **/
    public void run(ImageProcessor ip) {
        ImagePlus drawplus = new ImagePlus("Click to select mid-points",ip.convertToRGB().duplicate());

        drawplus.show();
        drawcanvas = drawplus.getCanvas();

        drawcanvas.addMouseListener(this);
        drawcanvas.addMouseMotionListener(this);

        drawproc = drawplus.getProcessor();
        state = 0;

        points = new ArrayList<int[]>();
	}

    public void mouseClicked(MouseEvent evt){


        int x = drawcanvas.offScreenX(evt.getX());
        int y = drawcanvas.offScreenY(evt.getY());

        //gets the various points
        switch(state){
            case 0:
                drawproc.setColor(Color.BLUE);
                drawproc.drawOval(x -10, y - 10, 20, 20);
                addPoint(x,y);
                break;
            case 1:
                addPoint(x,y);
                drawproc.setColor(Color.BLUE);
                drawproc.drawOval(x -10, y - 10, 20, 20);
                drawproc.setColor(Color.GREEN);
                drawproc.drawLine(points.get(0)[0],points.get(0)[1],points.get(1)[0],points.get(1)[1]);
                break;
            case 2:
                addPoint(x,y);
                drawproc.setColor(Color.BLUE);
                drawproc.drawOval(x -10, y - 10, 20, 20);
                bufferproc = drawproc.duplicate();
                break;
            case 3:
                addPoint(x,y);
                drawproc.setColor(Color.BLUE);
                drawproc.drawOval(x -10, y - 10, 20, 20);
                drawproc.setColor(Color.GREEN);
                drawproc.drawLine(points.get(2)[0],points.get(2)[1],points.get(3)[0],points.get(3)[1]);
                break;
            default:
            }

        state = points.size();

        if(state==4)
            createOvula();

        drawcanvas.update(drawcanvas.getGraphics());

    }

    private void addPoint(int x, int y){
        int[] p = new int[2];
        p[0] = x;
        p[1] = y;
        points.add(p);
    }


    /**
      *     During 'state 3' we draw the bounding rectangle
      *     to show the area that will be cropped and rotated.
      *
      **/
    public void mouseMoved(MouseEvent evt){

        if(state==3){
            int x = drawcanvas.offScreenX(evt.getX());
            int y = drawcanvas.offScreenY(evt.getY());
            drawproc.copyBits(bufferproc,0,0,Blitter.COPY);

            int[] p1,p2,p3,p4;

            p1 = points.get(0);
            p2 = points.get(1);
            p3 = points.get(2);
            p4 = new int[] {x, y};
            //principle axis
            int lx = p2[0] - p1[0];
            int ly = p2[1] - p1[1];

            double lx_sq = Math.pow(lx,2);
            double ly_sq = Math.pow(ly,2);
            double length = Math.sqrt(lx_sq+ly_sq);

            int width = (int)length;        //integer value for creating new image dimensions

            //secondary axis
            double hx = p4[0] - p3[0];
            double hy = p4[1] - p3[1];

            double hx_sq = Math.pow(hx,2);
            double hy_sq = Math.pow(hy,2);

            //Finds the length of height_d by finding the projection of the chosen line along the image r1xr2 = |r1||r2|sin(theta)
            double height_d = Math.abs((hx*ly - hy*lx)/length);


            int height = (int)height_d;         //integer value for new image dimensions

            //angle that the principle axis makes with the horizontal positive is ccw trig functions only

            double sintheta = ly/length;
            double costheta = lx/length;

            int startx = (int)(p1[0] - sintheta*height_d/2);
            int starty = (int)(p1[1] + costheta*height_d/2);

            int endx = (int)(p1[0] + sintheta*height_d/2);
            int endy = (int)(p1[1] - costheta*height_d/2);


            drawproc.drawLine(startx,starty, endx, endy);
            drawproc.drawLine(endx,endy,endx + lx, endy + ly);
            drawproc.drawLine(endx + lx, endy + ly, startx + lx, starty + ly);
            drawproc.drawLine(startx + lx, starty + ly, startx, starty);

            drawcanvas.update(drawcanvas.getGraphics());


        }

    }



    /**
      *     This sizes out the rectangle and then gets the pixel values in the image then plots them onto a new image stack
      **/
    public void createOvula(){

        int[] p1,p2,p3,p4;

        p1 = points.get(0);
        p2 = points.get(1);
        p3 = points.get(2);
        p4 = points.get(3);
        //principle axis
        double lx = p2[0] - p1[0];
        double ly = p2[1] - p1[1];

        double lx_sq = Math.pow(lx,2);
        double ly_sq = Math.pow(ly,2);
        double length = Math.sqrt(lx_sq+ly_sq);

        int width = (int)length;        //integer value for creating new image dimensions

        //secondary axis
        double hx = p4[0] - p3[0];
        double hy = p4[1] - p3[1];

        double hx_sq = Math.pow(hx,2);
        double hy_sq = Math.pow(hy,2);

        //Finds the length of height_d by finding the projection of the chosen line along the image r1xr2 = |r1||r2|sin(theta)
        double height_d = Math.abs((hx*ly - hy*lx)/length);


        int height = (int)height_d;         //integer value for new image dimensions

        //angle that the principle axis makes with the horizontal positive is ccw trig functions only

        double sintheta = ly/length;
        double costheta = lx/length;

        double startx = p1[0] - sintheta*height_d/2;
        double starty = p1[1] + costheta*height_d/2;

        double[][] cnet_map = new double[height*width][2];



        //int c = Color.RED.getRGB();
        int c = 150<<16;
        for(int i = 0; i<height; i++){
            for(int j = 0; j<width; j++){

                //creates a map
                cnet_map[i*width + j][0] = startx + j*costheta + i*sintheta;
                cnet_map[i*width + j][1] = starty + j*sintheta - i*costheta;

                //Creates a tinted box of the pixels used some pixelation occurs here because the points are drawn as ints
                int x = (int)cnet_map[i*width + j][0];
                int y = (int)cnet_map[i*width + j][1];
                drawproc.putPixel(x,y,(drawproc.getPixel(x,y)|c));
            }
        }
        if(imp.getType()==ImagePlus.COLOR_RGB)
            createCroppedImageRGB(width,height, cnet_map);
        else
            createCroppedImage(width,height, cnet_map);



    }

    /**
     *      Creates the rotated image by mapping the values located in
     *      cnet_map to a new rectangular image.
     *      @param width width of new image
     *      @param height of new image
     *      @param cnet_map coordinate map.
     **/
    public void createCroppedImage(int width, int height, double[][] cnet_map){
        //old stack for getting processors
        ImageStack AB = imp.getStack();

        //new stack for creating new imageplus
        ImageStack outstack = new ImageStack(width,height);
        ImageProcessor cp;


        for(int k = 1; k<=AB.getSize(); k++){

            cp = AB.getProcessor(k);
            ImageProcessor np = cp.createProcessor(width,height);

            //This is the rotation algorithm, it maps values from the rotated rectangle to a non-rotated.
            for(int i = 0; i < height; i++){
                for(int j = 0; j < width; j++){
                    np.putPixelValue(j,height - 1 - i,cp.getInterpolatedPixel(cnet_map[i*width + j][0],cnet_map[i*width + j][1]));
                }

            }

            String label = AB.getSliceLabel(k);
            outstack.addSlice(label,np);
        }


        createOutputPlus(outstack);
        points = new ArrayList<int[]>();

    }

    void createOutputPlus(ImageStack outstack){
        ImagePlus showme = new ImagePlus("cropped and rotated",outstack);
        showme.setDimensions(imp.getNChannels(), imp.getNSlices(), imp.getNFrames());
        int dims = 0;
        if(imp.getNChannels()>1) dims++;
        if(imp.getNSlices()>1) dims++;
        if(imp.getNFrames()>1) dims++;

        if(dims>1) showme.setOpenAsHyperStack(true);
        showme.show();
    }

    /**
     *
     *  RGB version that interpolates each pixel of the int array
     *  separately, this uses bi-linear interpolation...I hope
     *  @param width width of new image
     *  @param height of new image
     *  @param cnet_map coordinate map.
     * */
    public void createCroppedImageRGB(int width, int height, double[][] cnet_map){
        //old stack for getting processors
        ImageStack AB = imp.getStack();

        //new stack for creating new imageplus
        ImageStack outstack = new ImageStack(width,height);
        ImageProcessor cp;


        for(int k = 1; k<=AB.getSize(); k++){

            cp = AB.getProcessor(k);
            ImageProcessor np = cp.createProcessor(width,height);

            int[] px = new int[3];
            int[] pxi = new int[3];
            int[] pxj = new int[3];
            int[] pxij = new int[3];

            int[] npx = new int[3];
            double tx, ty;
            //This is the rotation algorithm, it maps values from the rotated rectangle to a non-rotated.
            for(int i = 0; i < height; i++){
                for(int j = 0; j < width; j++){

                    /** begin interpolating pixel, need to supply bound check */
                    int x = (int)cnet_map[i*width + j][0];
                    int y = (int)cnet_map[i*width + j][1];

                    tx = cnet_map[i*width + j][0] - x;
                    ty = cnet_map[i*width + j][1] - y;

                    px = cp.getPixel(x,y,px);
                    pxi = cp.getPixel(x+1,y,pxi);
                    pxj = cp.getPixel(x,y+1,pxj);
                    pxij = cp.getPixel(x+1,y+1,pxij);

                    for(int l = 0; l < 3; l++)
                        npx[l] = (int)(

                            (px[l] + tx*(pxi[l] - px[l]))*(1-ty) + (pxj[l] + (pxij[l] - pxj[l])*tx)*ty
                        );


                    np.putPixel(j,height - 1 - i,npx);
                }

            }

            String label = AB.getSliceLabel(k);
            outstack.addSlice(label,np);
        }

        createOutputPlus(outstack);

        points = new ArrayList<int[]>();

    }

}