/*
 * Copyright (c) 1997, 2006, Oracle and/or its affiliates. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 *   - Neither the name of Oracle nor the names of its
 *     contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/*
 */

import java.applet.Applet;
import java.awt.event.*;
import java.awt.*;
import java.awt.image.ColorModel;
import java.awt.image.MemoryImageSource;

public class DitherTest extends Applet implements Runnable {
    final private static int NOOP = 0;
    final private static int RED = 1;
    final private static int GREEN = 2;
    final private static int BLUE = 3;
    final private static int ALPHA = 4;
    final private static int SATURATION = 5;

    private Thread runner;

    private DitherControls XControls;
    private DitherControls YControls;
    private DitherCanvas canvas;

    public static void main(String args[]) {
        Frame f = new Frame("DitherTest");
        DitherTest ditherTest = new DitherTest();
        ditherTest.init();
        f.add("Center", ditherTest);
        f.pack();
        f.setVisible(true);
        ditherTest.start();
    }

    public void init() {
        String xspec = null, yspec = null;
        int xvals[] = new int[2];
        int yvals[] = new int[2];

        try {
            xspec = getParameter("xaxis");
            yspec = getParameter("yaxis");
        } catch (NullPointerException npe) {
            //only occurs if run as application
        }

        if (xspec == null) {
            xspec = "red";
        }
        if (yspec == null) {
            yspec = "blue";
        }
        int xmethod = colormethod(xspec, xvals);
        int ymethod = colormethod(yspec, yvals);

        setLayout(new BorderLayout());
        XControls = new DitherControls(this, xvals[0], xvals[1],
                                       xmethod, false);
        YControls = new DitherControls(this, yvals[0], yvals[1],
                                       ymethod, true);
        YControls.addRenderButton();
        add("North", XControls);
        add("South", YControls);
        add("Center", canvas = new DitherCanvas());
    }

    private int colormethod(String s, int vals[]) {
        int method = NOOP;
        if (s == null) {
            s = "";
        }
        String lower = s.toLowerCase();
        int len = 0;
        if (lower.startsWith("red")) {
            method = RED;
            lower = lower.substring(3);
        } else if (lower.startsWith("green")) {
            method = GREEN;
            lower = lower.substring(5);
        } else if (lower.startsWith("blue")) {
            method = BLUE;
            lower = lower.substring(4);
        } else if (lower.startsWith("alpha")) {
            method = ALPHA;
            lower = lower.substring(5);
        } else if (lower.startsWith("saturation")) {
            method = SATURATION;
            lower = lower.substring(10);
        }
        if (method == NOOP) {
            vals[0] = 0;
            vals[1] = 0;
            return method;
        }
        int begval = 0;
        int endval = 255;
        try {
            int dash = lower.indexOf('-');
            if (dash < 0) {
                endval = Integer.parseInt(lower);
            } else {
                begval = Integer.parseInt(lower.substring(0, dash));
                endval = Integer.parseInt(lower.substring(dash + 1));
            }
        } catch (NumberFormatException nfe) {
        }

        if (begval < 0) {
            begval = 0;
        } else if (begval > 255) {
            begval = 255;
        }

        if (endval < 0) {
            endval = 0;
        } else if (endval > 255) {
            endval = 255;
        }

        vals[0] = begval;
        vals[1] = endval;
        return method;
    }

    /**
     * Calculates and returns the image.  Halts the calculation and returns
     * null if the Applet is stopped during the calculation.
     */
    private Image calculateImage() {
        Thread me = Thread.currentThread();

        int width = canvas.getSize().width;
        int height = canvas.getSize().height;
        int xvals[] = new int[2];
        int yvals[] = new int[2];
        int xmethod = XControls.getParams(xvals);
        int ymethod = YControls.getParams(yvals);
        int pixels[] = new int[width * height];
        int c[] = new int[4];   //temporarily holds R,G,B,A information
        int index = 0;
        for (int j = 0; j < height; j++) {
            for (int i = 0; i < width; i++) {
                c[0] = c[1] = c[2] = 0;
                c[3] = 255;
                if (xmethod < ymethod) {
                    applymethod(c, xmethod, i, width, xvals);
                    applymethod(c, ymethod, j, height, yvals);
                } else {
                    applymethod(c, ymethod, j, height, yvals);
                    applymethod(c, xmethod, i, width, xvals);
                }
                pixels[index++] = ((c[3] << 24) |
                                   (c[0] << 16) |
                                   (c[1] << 8) |
                                   c[2]);
            }

            // Poll once per row to see if we've been told to stop.
            if (runner != me) {
                return null;
            }
        }
        return createImage(new MemoryImageSource(width, height,
                            ColorModel.getRGBdefault(), pixels, 0, width));
    }

    private void applymethod(int c[], int method, int step,
                             int total, int vals[]) {
        if (method == NOOP) {
            return;
        }
        int val = ((total < 2)
                   ? vals[0]
                   : vals[0] + ((vals[1] - vals[0]) * step / (total - 1)));
        switch (method) {
        case RED:
            c[0] = val;
            break;
        case GREEN:
            c[1] = val;
            break;
        case BLUE:
            c[2] = val;
            break;
        case ALPHA:
            c[3] = val;
            break;
        case SATURATION:
            int max = Math.max(Math.max(c[0], c[1]), c[2]);
            int min = max * (255 - val) / 255;
            if (c[0] == 0) {
                c[0] = min;
            }
            if (c[1] == 0) {
                c[1] = min;
            }
            if (c[2] == 0) {
                c[2] = min;
            }
            break;
        }
    }

    public void start() {
        runner = new Thread(this);
        runner.start();
    }

    public void run() {
        canvas.setImage(null);  // Wipe previous image
        Image img = calculateImage();
        if (img != null && runner == Thread.currentThread()) {
            canvas.setImage(img);
        }
    }

    public void stop() {
        runner = null;
    }

    public void destroy() {
        remove(XControls);
        remove(YControls);
        remove(canvas);
    }

    public String getAppletInfo() {
        return "An interactive demonstration of dithering.";
    }

    public String[][] getParameterInfo() {
        String[][] info = {
            {"xaxis", "{RED, GREEN, BLUE, ALPHA, SATURATION}",
             "The color of the Y axis.  Default is RED."},
            {"yaxis", "{RED, GREEN, BLUE, ALPHA, SATURATION}",
             "The color of the X axis.  Default is BLUE."}
        };
        return info;
    }
}

class DitherCanvas extends Canvas {
    private Image img;
    private static String calcString = "Calculating...";

    public void paint(Graphics g) {
        int w = getSize().width;
        int h = getSize().height;
        if (img == null) {
            super.paint(g);
            g.setColor(Color.black);
            FontMetrics fm = g.getFontMetrics();
            int x = (w - fm.stringWidth(calcString)) / 2;
            int y = h / 2;
            g.drawString(calcString, x, y);
        } else {
            g.drawImage(img, 0, 0, w, h, this);
        }
    }

    public void update(Graphics g) {
        paint(g);
    }

    public Dimension getMinimumSize() {
        return new Dimension(20, 20);
    }

    public Dimension getPreferredSize() {
        return new Dimension(200, 200);
    }

    public Image getImage() {
        return img;
    }

    public void setImage(Image img) {
        this.img = img;
        repaint();
    }
}

class DitherControls extends Panel implements ActionListener {
    private CardinalTextField start;
    private CardinalTextField end;
    private Button button;
    private Choice choice;
    private DitherTest applet;

    private static LayoutManager dcLayout = new FlowLayout(FlowLayout.CENTER,
                                                           10, 5);

    public DitherControls(DitherTest app, int s, int e, int type,
                          boolean vertical) {
        applet = app;
        setLayout(dcLayout);
        add(new Label(vertical ? "Vertical" : "Horizontal"));
        add(choice = new Choice());
        choice.addItem("Noop");
        choice.addItem("Red");
        choice.addItem("Green");
        choice.addItem("Blue");
        choice.addItem("Alpha");
        choice.addItem("Saturation");
        choice.select(type);
        add(start = new CardinalTextField(Integer.toString(s), 4));
        add(end = new CardinalTextField(Integer.toString(e), 4));
    }

    /* puts on the button */
    public void addRenderButton() {
        add(button = new Button("New Image"));
        button.addActionListener(this);
    }

    /* retrieves data from the user input fields */
    public int getParams(int vals[]) {
        try {
            vals[0] = scale(Integer.parseInt(start.getText()));
        } catch (NumberFormatException nfe) {
            vals[0] = 0;
        }
        try {
            vals[1] = scale(Integer.parseInt(end.getText()));
        } catch (NumberFormatException nfe) {
            vals[1] = 255;
        }
        return choice.getSelectedIndex();
    }

    /* fits the number between 0 and 255 inclusive */
    private int scale(int number) {
        if (number < 0) {
            number = 0;
        } else if (number > 255) {
            number = 255;
        }
        return number;
    }

    /* called when user clicks the button */
    public void actionPerformed(ActionEvent e) {
        if (e.getSource() == button) {
            applet.start();
        }
    }
}

class CardinalTextField extends TextField {

    String oldText = null;

    public CardinalTextField(String text, int columns) {
        super(text, columns);
        enableEvents(AWTEvent.KEY_EVENT_MASK | AWTEvent.TEXT_EVENT_MASK);
        oldText = getText();
    }

    // Consume non-digit KeyTyped events
    // Note that processTextEvent kind of eliminates the need for this
    // function, but this is neater, since ideally, it would prevent
    // the text from appearing at all.  Sigh.  See bugid 4100317/4114565.
    //
    protected void processEvent(AWTEvent evt) {
        int id = evt.getID();
        if (id != KeyEvent.KEY_TYPED) {
            super.processEvent(evt);
            return;
        }

        KeyEvent kevt = (KeyEvent) evt;
        char c = kevt.getKeyChar();

        // Digits, backspace, and delete are okay
        // Note that the minus sign is not allowed (neither is decimal)
        if (Character.isDigit(c) || (c == '\b') || (c == '\u007f')) {
            super.processEvent(evt);
            return;
        }

        Toolkit.getDefaultToolkit().beep();
        kevt.consume();
    }

    // Should consume TextEvents for non-integer Strings
    // Store away the text in the tf for every TextEvent
    // so we can revert to it on a TextEvent (paste, or
    // legal key in the wrong location) with bad text
    //
    // Note: it would be easy to extend this to an eight-bit
    // TextField (range 0-255), but I'll leave it as-is.
    //
    protected void processTextEvent(TextEvent te) {
        // The empty string is okay, too
        String newText = getText();
        if (newText.equals("") || textIsCardinal(newText)) {
            oldText = newText;
            super.processTextEvent(te);
            return;
        }

        Toolkit.getDefaultToolkit().beep();
        setText(oldText);
    }

    // Returns true for Cardinal (non-negative) numbers
    // Note that the empty string is not allowed
    private boolean textIsCardinal(String textToCheck) {
        int value = -1;

        try {
            value = Integer.parseInt(textToCheck, 10);
            return (value >= 0);
        } catch (NumberFormatException nfe) {
            return false;
        }
    }
}
