/* 
 * Simple application that reproduces a possible bug in Java. 
 * 
 * Author: Phil Burk 2019 
 * License: Free for all uses without restriction and without warranty of any kind. 
 */ 

// package com.mobileer.bugbusypaint; 

import java.awt.Color; 
import java.awt.Dimension; 
import java.awt.Graphics; 
import java.awt.event.ActionEvent; 
import java.awt.event.ActionListener; 
import java.awt.event.MouseAdapter; 
import java.awt.event.MouseEvent; 
import java.util.concurrent.CopyOnWriteArrayList; 

import javax.swing.BoxLayout; 
import javax.swing.JDesktopPane; 
import javax.swing.JFrame; 
import javax.swing.JInternalFrame; 
import javax.swing.JPanel; 
import javax.swing.SwingUtilities; 


public class BugBusyPaint 
{ 
    private final static int NUM_ROWS = 8; 
    private final static int NUM_COLUMNS = 16; 
    private final static int PATCH_WIDTH = 600; 
    private final static int PATCH_HEIGHT = 500; 

    private JFrame mainFrame; 
    private JDesktopPane desktop; 
    private Thread thread; 
    BitMaskJackView[] maskViews = new BitMaskJackView[NUM_ROWS]; 

    public static void main(String[] args) { 
    	BugBusyPaint app = new BugBusyPaint(); 
        app.execute(); 
    } 

    private void execute() { 
        init(); 
        start(); 
    } 

    private void init() { 

        mainFrame = new JFrame("Test"); 
        mainFrame.setLocation(40, 50); 
        mainFrame.setSize(800, 600); 

        // Create a desktop to contain the internal frames. 
        desktop = new JDesktopPane(); 
        desktop.setBackground(Color.ORANGE); 
        desktop.setOpaque(true); 
        mainFrame.setContentPane(desktop); 

        // Create a patch frame with custom check boxes. 
        JInternalFrame patch1 = new JInternalFrame("patch1", 
        	false, // resizable 
        	false, // closable 
        	false, // maximizable 
        	false);// iconifiable 
        patch1.setLocation(50, 30); 
        patch1.setSize(PATCH_WIDTH, PATCH_HEIGHT); 
        patch1.setBackground(Color.CYAN); 
        // Turning off shadows does not help. 
        // patch1.putClientProperty("JInternalFrame.frameType", "normal"); 
        desktop.add(patch1); 

        // Fill the first patch with check boxes 
        JPanel panel = new JPanel(); 
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS)); 
        for (int i = 0; i < maskViews.length; i++) { 
        	maskViews[i] = new BitMaskJackView(NUM_COLUMNS); 
        	panel.add(maskViews[i]); 
        } 
        patch1.add( panel ); 

        // Create a second patch to partially cover the first one. 
        JInternalFrame patch2 = new JInternalFrame("patch2", 
        	false, // resizable 
        	false, // closable 
        	false, // maximizable 
        	false);// iconifiable 
        patch2.setLocation(100, 30); 
        patch2.setSize(PATCH_WIDTH, PATCH_HEIGHT); 
        patch2.setBackground(Color.DARK_GRAY); 
        // Turning off shadows does not help. 
        // patch2.putClientProperty("JInternalFrame.frameType", "normal"); 
        desktop.add(patch2); 

        patch1.setVisible(true); 
        patch2.setVisible(true); 

        mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
    } 


    private void start() { 
        mainFrame.setVisible(true); 
        startSequencer(); 
    } 

private void startSequencer() { 
thread = new Thread() { 
            @Override 
            public void run() { 
                try { 
                	int column = 0; 
                    // Process commands from queue. 
                    while (true) { 
                    	sleep(200); 
                    	final int j = column; 
                        SwingUtilities.invokeLater(new Runnable() { 
                            @Override 
                            public void run() { 
                            	for (BitMaskJackView maskView : maskViews) { 
                        	maskView.indexChanged(j); 
                            	} 
                            } 
                        }); 
                    	if (++column >= 16) column = 0; 
                    } 
                } catch (InterruptedException e) { 
                    e.printStackTrace(); 
                } 
            } 
        }; 
        thread.start(); 
} 

} 

// ===================================================================== 

@SuppressWarnings("serial") 
class BitMaskJackView extends JPanel { 
    private BooleanBox[] mBoxes; 
    private int previousIndex; 

    public BitMaskJackView(int numColumns) { 
        BoxLayout layout = new BoxLayout(this, BoxLayout.X_AXIS); 
        setLayout(layout); 
        mBoxes = new BooleanBox[numColumns]; 
        for (int i = 0; i < numColumns; i++) { 
            final BooleanBox checkbox = new BooleanBox(); 
            add(checkbox); 
            mBoxes[i] = checkbox; 
        } 
    } 

    public void indexChanged(int index) { 
        mBoxes[previousIndex].setCurrent(false); 
        mBoxes[index].setCurrent(true); 
        if (mBoxes[previousIndex].isVisible()) { 
            mBoxes[previousIndex].repaint(); 
        } 
        if (mBoxes[index].isVisible()) { 
            mBoxes[index].repaint(); 
        } 
        previousIndex = index; 
    } 
} 

//===================================================================== 

class BooleanBox extends JPanel { 
    private boolean mSelected; 
    private boolean mCurrent; 
    private CopyOnWriteArrayList<ActionListener> mActionListeners = new CopyOnWriteArrayList<ActionListener>(); 
    private static final int WIDTH = 20; 
    private static final int HEIGHT = 20; 
    private static final int MARGIN = 4; 

    private static int mPaintCounter = 0; 

    public BooleanBox() { 
        addMouseListener(new MyMouseListener()); 
        Dimension dim = new Dimension(WIDTH, HEIGHT); 
        setMinimumSize(dim); 
        setPreferredSize(dim); 
    } 

    @Override 
    public void paintComponent(Graphics g) { 
        super.paintComponent(g); 

        // Draw background based on whether is it a currently active box. 
        Color backColor = (mCurrent) ? Color.ORANGE : getBackground(); 
        g.setColor(backColor); 
        g.fillRect(0, 0, getWidth(), getHeight()); 

        // Draw a circle. 
        int x = MARGIN / 2; 
        int y = MARGIN / 2; 
        int w = getWidth() - MARGIN; 
        int h = getHeight() - MARGIN; 

        int diameter = (w < h) ? w : h; // min 
        if (mSelected) { 
            g.setColor(Color.CYAN); 
            g.fillOval(x, y, diameter, diameter); 
        } 
        g.setColor(Color.BLACK); 
        g.drawOval(x, y, diameter, diameter); 
        // System.out.println("paint BooleanBox #" + mPaintCounter++); 
    } 

    private class MyMouseListener extends MouseAdapter { 
        @Override 
        public void mouseClicked(MouseEvent e) { 
            setSelected(!isSelected()); // toggle 
            fireActionListeners(); 
        } 
    } 

    public boolean isSelected() { 
        return mSelected; 
    } 

    public boolean isCurrent() { 
        return mCurrent; 
    } 

    public void setCurrent(boolean current) { 
        this.mCurrent = current; 
    } 

    public void setSelected(boolean selected) { 
        this.mSelected = selected; 
        repaint(); 
    } 

    public void fireActionListeners() { 
        ActionEvent event = new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "toggle"); 
        for (ActionListener actionListener : mActionListeners) { 
            actionListener.actionPerformed(event); 
        } 
    } 

    public void addActionListener(ActionListener actionListener) { 
        mActionListeners.add(actionListener); 
    } 

    public void removeActionListener(ActionListener actionListener) { 
        mActionListeners.remove(actionListener); 
    } 
} 