/*************************************************************************
 *  Compilation:  javac StdDraw.java
 *  Execution:    java StdDraw
 *
 *  Standard graphics library.
 *
 *  For documentation, see http://www.cs.princeton.edu/introcs/22inout 
 *
 *  Todo
 *  ----
 *    -  Add support for CubicCurve2D or QudarticCurve2D or Arc2D
 *    -  Add support for gradient fill, etc.
 *    -  Block when user clicks mouse in window; unblock on next click
 *
 *  Remarks
 *  -------
 *    -  don't use AffineTransform for rescaling since it inverts
 *       images and strings
 *    -  careful using setFont in inner loop within an animation -
 *       it can cause flicker
 *
 *************************************************************************/

import java.io.*;
import java.net.*;
import java.awt.*;
import java.awt.geom.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.swing.*;
import javax.imageio.ImageIO;


public class StdDraw implements ActionListener {

    // pre-defined colors
    public final static Color BLACK      = Color.BLACK;
    public final static Color BLUE       = Color.BLUE;
    public final static Color CYAN       = Color.CYAN;
    public final static Color DARK_GRAY  = Color.DARK_GRAY;
    public final static Color GRAY       = Color.GRAY;
    public final static Color GREEN      = Color.GREEN;
    public final static Color LIGHT_GRAY = Color.LIGHT_GRAY;
    public final static Color MAGENTA    = Color.MAGENTA;
    public final static Color ORANGE     = Color.ORANGE;
    public final static Color PINK       = Color.PINK;
    public final static Color RED        = Color.RED;
    public final static Color WHITE      = Color.WHITE;
    public final static Color YELLOW     = Color.YELLOW;

    // default colors
    public final static Color DEFAULT_PEN_COLOR   = BLACK;
    public final static Color DEFAULT_CLEAR_COLOR = WHITE;

    // current pen color
    private static Color penColor;

    // default canvas size is SIZE-by-SIZE
    public final static int SIZE = 512;
    private static int width  = SIZE;
    private static int height = SIZE;

    // default pen radius
    private final static double DEFAULT_PEN_RADIUS = 0.002;

    // current pen radius
    private static double penRadius;

    // show we draw immediately or wait until next show?
    private static boolean defer = false;

    // boundary of drawing canvas
    private final static double BORDER = 0.05;       // 5 % border
    private final static double DEFAULT_XMIN = 0.0;
    private final static double DEFAULT_XMAX = 1.0;
    private final static double DEFAULT_YMIN = 0.0;
    private final static double DEFAULT_YMAX = 1.0;
    private static double xmin, ymin, xmax, ymax;

    // font
    private final static Font DEFAULT_FONT = new Font("Serif", Font.PLAIN, 16);
    private static Font font;

    // double buffered graphics
    private static BufferedImage offscreenImage, onscreenImage;
    private static Graphics2D offscreen, onscreen;
    private static EpsGraphics2D eps;

    // singleton for callbacks
    // solely used to avoid generating extra .class files from nested classes
    private static StdDraw std = new StdDraw();

    // not instantiable
    private StdDraw() { }

    // the frame for drawing to the screen
    private static JFrame frame = new JFrame();


    // static initializer
    static 
    {
        init();
    }

    public static void setCanvasSize(int w, int h) {
        width = w;
        height = h;
        init();
    }

    // init 
    private static void init() {
        offscreenImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        onscreenImage  = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        offscreen = (Graphics2D) offscreenImage.getGraphics();
        onscreen  = (Graphics2D) onscreenImage.getGraphics();
        eps = new EpsGraphics2D();
        setXscale();
        setYscale();
        offscreen.setColor(DEFAULT_CLEAR_COLOR);
        offscreen.fillRect(0, 0, width, height);
        eps.setColor(DEFAULT_CLEAR_COLOR);
        eps.fillRect(0, 0, width, height);
        setPenColor();
        setPenRadius();
        setFont();
        clear();

        // add antialiasing
        RenderingHints hints = new RenderingHints(RenderingHints.KEY_ANTIALIASING,
                                                  RenderingHints.VALUE_ANTIALIAS_ON);
        hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        offscreen.addRenderingHints(hints);

        // frame stuff
        ImageIcon icon = new ImageIcon(onscreenImage);
        frame.setContentPane(new JLabel(icon));
        frame.setResizable(false);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);            // closes all windows
        // frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);      // closes only current window
        frame.setTitle("Standard Draw");
        frame.setJMenuBar(createMenuBar());
        frame.pack();
        frame.setVisible(true);
    }

    // create the menu bar
    public static JMenuBar createMenuBar() {
        JMenuBar menuBar = new JMenuBar();
        JMenu menu = new JMenu("File");
        menuBar.add(menu);
        JMenuItem menuItem1 = new JMenuItem(" Save...   ");
        menuItem1.addActionListener(std);
        menuItem1.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S,
                                Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
        menu.add(menuItem1);
        return menuBar;
    }



    // change the user coordinate system
    public static void setXscale() { setXscale(DEFAULT_XMIN, DEFAULT_XMAX); }
    public static void setYscale() { setYscale(DEFAULT_YMIN, DEFAULT_YMAX); }
    public static void setXscale(double min, double max) {
        double size = max - min;
        xmin = min - BORDER * size;
        xmax = max + BORDER * size;
    }
    public static void setYscale(double min, double max) {
        double size = max - min;
        ymin = min - BORDER * size;
        ymax = max + BORDER * size;
    }

    // helper functions that scale from user coordinates to screen coordinates
    private static double scaleX(double x)  { return width  * (x - xmin) / (xmax - xmin); }
    private static double scaleY(double y)  { return height * (ymax - y) / (ymax - ymin); }
    private static double factorX(double w) { return w * width  / Math.abs(xmax - xmin);  }
    private static double factorY(double h) { return h * height / Math.abs(ymax - ymin);  }

    // clear the screen with given color
    public static void clear() { clear(DEFAULT_CLEAR_COLOR); }
    public static void clear(Color color) {
        offscreen.setColor(color);
        offscreen.fillRect(0, 0, width, height);
        offscreen.setColor(penColor);
        eps.setColor(color);
        eps.fillRect(0, 0, width, height);
        eps.setColor(penColor);
        show();
    }
   
    // set the pen size
    public static void setPenRadius() { setPenRadius(DEFAULT_PEN_RADIUS); }
    public static void setPenRadius(double r) {
        penRadius = r * SIZE;
        BasicStroke stroke = new BasicStroke((float) penRadius);
        offscreen.setStroke(stroke);
        eps.setStroke(stroke);
    }

    // set the pen color
    public static void setPenColor() { setPenColor(DEFAULT_PEN_COLOR); }
    public static void setPenColor(Color color) {
        penColor = color;
        offscreen.setColor(penColor);
        eps.setColor(penColor);
    }

    // write the given string in the current font
    public static void setFont() { setFont(DEFAULT_FONT); }
    public static void setFont(Font f) { font = f; }

    // draw a line from (x0, y0) to (x1, y1)
    public static void line(double x0, double y0, double x1, double y1) {
        offscreen.draw(new Line2D.Double(scaleX(x0), scaleY(y0), scaleX(x1), scaleY(y1)));
        eps.draw(new Line2D.Double(scaleX(x0), scaleY(y0), scaleX(x1), scaleY(y1)));
        show();
    }

    // draw one pixel at (x, y)
    private static void pixel(double x, double y) {
        offscreen.fillRect((int) Math.round(scaleX(x)), (int) Math.round(scaleY(y)), 1, 1);
        eps.fillRect((int) Math.round(scaleX(x)), (int) Math.round(scaleY(y)), 1, 1);
    }

    // draw point at (x, y)
    public static void point(double x, double y) {
        double xs = scaleX(x);
        double ys = scaleY(y);
        double r = penRadius;
        //double ws = factorX(2*r);
        //double hs = factorY(2*r);
        //if (ws <= 1 && hs <= 1) pixel(x, y);
        if (r <= 1) pixel(x, y);
        else offscreen.fill(new Ellipse2D.Double(xs - r/2, ys - r/2, r, r));
        eps.fill(new Ellipse2D.Double(xs - r/2, ys - r/2, r, r));
        show();
    }

    // draw circle of radius r, centered on (x, y); degenerate to pixel if small
    public static void circle(double x, double y, double r) {
        double xs = scaleX(x);
        double ys = scaleY(y);
        double ws = factorX(2*r);
        double hs = factorY(2*r);
        if (ws <= 1 && hs <= 1) pixel(x, y);
        else offscreen.draw(new Ellipse2D.Double(xs - ws/2, ys - hs/2, ws, hs));
        eps.draw(new Ellipse2D.Double(xs - ws/2, ys - hs/2, ws, hs));
        show();
    }


    // draw filled circle of radius r, centered on (x, y); degenerate to pixel if small
    public static void filledCircle(double x, double y, double r) {
        double xs = scaleX(x);
        double ys = scaleY(y);
        double ws = factorX(2*r);
        double hs = factorY(2*r);
        if (ws <= 1 && hs <= 1) pixel(x, y);
        else offscreen.fill(new Ellipse2D.Double(xs - ws/2, ys - hs/2, ws, hs));
        eps.fill(new Ellipse2D.Double(xs - ws/2, ys - hs/2, ws, hs));
        show();
    }

    // draw squared of side length 2r, centered on (x, y); degenerate to pixel if small
    public static void square(double x, double y, double r) {
        // screen coordinates
        double xs = scaleX(x);
        double ys = scaleY(y);
        double ws = factorX(2*r);
        double hs = factorY(2*r);
        if (ws <= 1 && hs <= 1) pixel(x, y);
        else offscreen.draw(new Rectangle2D.Double(xs - ws/2, ys - hs/2, ws, hs));
        eps.draw(new Rectangle2D.Double(xs - ws/2, ys - hs/2, ws, hs));
        show();
    }

    // draw squared of side length 2r, centered on (x, y); degenerate to pixel if small
    public static void filledSquare(double x, double y, double r) {
        // screen coordinates
        double xs = scaleX(x);
        double ys = scaleY(y);
        double ws = factorX(2*r);
        double hs = factorY(2*r);
        if (ws <= 1 && hs <= 1) pixel(x, y);
        else offscreen.fill(new Rectangle2D.Double(xs - ws/2, ys - hs/2, ws, hs));
        eps.fill(new Rectangle2D.Double(xs - ws/2, ys - hs/2, ws, hs));
        show();
    }

    // draw a polygon with the given (x[i], y[i]) coordinates
    public static void polygon(double[] x, double[] y) {
        int N = x.length;
        GeneralPath path = new GeneralPath();
        path.moveTo((float) scaleX(x[0]), (float) scaleY(y[0]));
        for (int i = 0; i < N; i++)
            path.lineTo((float) scaleX(x[i]), (float) scaleY(y[i]));
        path.closePath();
        offscreen.draw(path);
        eps.draw(path);
        show();
    }

    // draw a filled polygon with the given (x[i], y[i]) coordinates
    public static void filledPolygon(double[] x, double[] y) {
        int N = x.length;
        GeneralPath path = new GeneralPath();
        path.moveTo((float) scaleX(x[0]), (float) scaleY(y[0]));
        for (int i = 0; i < N; i++)
            path.lineTo((float) scaleX(x[i]), (float) scaleY(y[i]));
        path.closePath();
        offscreen.fill(path);
        eps.fill(path);
        show();
    }


    // get an image from the given filename
    private static Image getImage(String filename) {

        // to read from file
        ImageIcon icon = new ImageIcon(filename);

        // try to read from URL
        if ((icon == null) || (icon.getImageLoadStatus() != MediaTracker.COMPLETE)) {
            try {
                URL url = new URL(filename);
                icon = new ImageIcon(url);
            } catch(Exception e) { }
        }

        // in case file is inside a .jar
        if ((icon == null) || (icon.getImageLoadStatus() != MediaTracker.COMPLETE)) {
            URL url = StdDraw.class.getResource(filename);
            if (url == null) throw new RuntimeException("image " + filename + " not found");
            icon = new ImageIcon(url);
        }

        return icon.getImage();
    }

    // draw picture (gif, jpg, or png) centered on (x, y)
    public static void picture(double x, double y, String s) {
        Image image = getImage(s);
        double xs = scaleX(x);
        double ys = scaleY(y);
        int ws = image.getWidth(null);
        int hs = image.getHeight(null);
        if (ws < 0 || hs < 0) throw new RuntimeException("image " + s + " is corrupt"); 

        offscreen.drawImage(image, (int) (xs - ws/2.0), (int) (ys - hs/2.0), null);
        eps.drawImage(image, (int) (xs - ws/2.0), (int) (ys - hs/2.0), null);
        show();
    }

    // write the given text string in the current font, center on (x, y)
    public static void text(double x, double y, String s) {
        offscreen.setFont(font);
        eps.setFont(font);
        FontMetrics metrics = offscreen.getFontMetrics();
        double xs = scaleX(x);
        double ys = scaleY(y);
        int ws = metrics.stringWidth(s);
        int hs = metrics.getDescent();
        offscreen.drawString(s, (float) (xs - ws/2.0), (float) (ys + hs));
        eps.drawString(s, (float) (xs - ws/2.0), (float) (ys + hs));
        show();
    }

    // display on screen and pause for t miliseconds
    public static void show(int t) {
        defer = true;
        onscreen.drawImage(offscreenImage, 0, 0, null);
        frame.repaint();
        try { Thread.currentThread().sleep(t); }
        catch (InterruptedException e) { }
    }


    // view on-screen, creating new frame if necessary
    public static void show() {
        if (!defer) onscreen.drawImage(offscreenImage, 0, 0, null);
        if (!defer) frame.repaint();
    }

    // save to file - suffix can be png, jpg, or gif
    public static void save(String filename) {
        File file = new File(filename);
        String suffix = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();

        if (suffix.equals("eps")) {
            PrintWriter out = null;
            try                     { out = new PrintWriter(new FileOutputStream(filename), true);  }
            catch(IOException ioe)  { ioe.printStackTrace();                                        }
            out.println(eps);
            out.close();
        }

        else if (suffix.equals("png") || suffix.equals("jpeg") || suffix.equals("jpg")) {
            try { ImageIO.write(offscreenImage, suffix, file); }
            catch (IOException e) { e.printStackTrace(); }
        }
        else {
            System.out.println("Illegal file extension: " + filename);
        }
    }


    // open a save dialog when the user selects "Save As" from the menu
    public void actionPerformed(ActionEvent e) {
        FileDialog chooser = new FileDialog(StdDraw.frame, "Use a .png, .jpg, or .eps extension", FileDialog.SAVE);
        chooser.setVisible(true);
        String filename = chooser.getFile();
        if (filename != null) {
            StdDraw.save(chooser.getDirectory() + File.separator + chooser.getFile());
        }
    }


    // test client
    public static void main(String args[]) {
        StdDraw.square(.2, .8, .1); 
        StdDraw.filledSquare(.8, .8, .2); 
        StdDraw.circle(.8, .2, .2); 

        // draw a blue diamond
        StdDraw.setPenColor(StdDraw.BLUE);
        double[] x = { .1, .2, .3, .2 };
        double[] y = { .2, .3, .2, .1 };
        StdDraw.filledPolygon(x, y);

        // text
        StdDraw.text(0.8, 0.2, "centered");
    }

}

