/* @(#)PaintBar.java 1.0   2.3.2   11 September 2004
 *
 * Copyright 2004
 * College of Computer and Information Science
 * Northeastern University
 * Boston, MA  02115
 *
 * The Java Power Tools software may be used for educational
 * purposes as long as this copyright notice is retained intact
 * at the top of all source files.
 *
 * To discuss possible commercial use of this software, 
 * contact Richard Rasala at Northeastern University, 
 * College of Computer and Information Science,
 * 617-373-2462 or rasala@ccs.neu.edu.
 *
 * The Java Power Tools software has been designed and built
 * in collaboration with Viera Proulx and Jeff Raab.
 *
 * Should this software be modified, the words "Modified from 
 * Original" must be included as a comment below this notice.
 *
 * All publication rights are retained.  This software or its 
 * documentation may not be published in any media either
 * in whole or in part without explicit permission.
 *
 * This software was created with support from Northeastern 
 * University and from NSF grant DUE-9950829.
 */

package edu.neu.ccs.gui;

import edu.neu.ccs.gui.*;
import edu.neu.ccs.util.*;

import java.awt.*;
import java.awt.geom.*;
import javax.swing.*;
import java.beans.*;

/**
 * <p>Class <code>PaintBar</code> implements a <code>Paintable</code>
 * that paints a one or two dimensional array of disks.
 * The constructor supplies an array of <code>Paint</code> objects,
 * one for each disk.  The user can choose the diameter of the disks
 * and the gap between the disks.</p>
 *
 * <p>As an application, an object of this class may be embedded in
 * a <code>PaintableComponent</code> and used to implement a custom
 * paint selector control.</p>
 *
 * @author  Richard Rasala
 * @version 2.3.2
 * @since   2.3.2
 */
public class PaintBar
    extends PaintableSequenceComposite
{
    /** The number of distinct paint disks. */
    private int number = 0;
    
    /** The diameter of the disks. */
    private int diameter = 0;
    
    /** The gap between the disks. */
    private int gap = 0;
    
    /** The x and y coordinate of the corner of disk (0, 0). */
    private int base = 0;
    
    /** The x and y coordinate of the center of disk (0, 0). */
    private int center = 0;
    
    /** The skip between the disk centers. */
    private int skip = 0;
    
    /** The bounds of the paint bar. */
    private Rectangle2D.Double bounds;
    
    
    /**
     * <p>This contructor builds a <code>Paintable</code> that will paint
     * a sequence of disks using the given one dimensional <code>Paint</code>
     * array, the given disk diameter, the given gap between disks, and the
     * given orientation for the bar (HORIZONTAL or VERTICAL).</p>
     *
     * <p>Throws <code>NullPointerException</code> if the array of paints
     * is <code>null</code> or if all entries in the array of paints are
     * <code>null</code>.  On the other hand, if an individual paint item
     * is <code>null</code>, then its position is simply skipped.</p>
     *
     * <p>If the diameter is less than 5, it is set to 5.</p>
     *
     * <p>If the gap is less than 0, it is set to 0.</p>
     *
     * <p>If the orientation is not equal to HORIZONAL, it is set to
     * VERTICAL.</p>
     *
     * @param paints      the set of paints for the disks in the paint bar
     * @param diameter    the diameter of each disk
     * @param gap         the gap between disks
     * @param orientation the orientation of the paint bar
     */
    public PaintBar(Paint[] paints, int diameter, int gap, int orientation) {
        if (paints == null)
            throw new NullPointerException
                ("PaintBar constructor error: paints array is null");
        
        int length = paints.length; // length of paints
        
        if (length == 0)
            throw new NullPointerException
                ("PaintBar constructor error: paints array has no data");
        
        if (diameter < 5)
            diameter = 5;
        
        if (gap < 0)
            gap = 0;
        
        this.diameter = diameter;
        this.gap = gap;
        
        base   = gap / 2;
        skip   = diameter + gap;
        center = skip / 2;
        
        int x = xCorner(0);     // circle x corner
        int y = yCorner(0);     // circle y corner
        
        for (int i = 0; i < length; i++) {
            if (paints[i] != null) {
                if (orientation == HORIZONTAL)
                    x = xCorner(i);
                else
                    y = yCorner(i);
                
                installPaint(paints[i], x, y, diameter);
            }
        }
        
        if (number == 0)
            throw new NullPointerException
                ("PaintBar constructor error: paints array has only null data");
        
        if (orientation == HORIZONTAL)
            bounds = new Rectangle2D.Double(0, 0, skip * length, skip);
        else
            bounds = new Rectangle2D.Double(0, 0, skip, skip * length);
        
        getPaintableSequence().setDefaultBounds2D(bounds);
    }
    
    
    /**
     * <p>This contructor builds a <code>Paintable</code> that will paint
     * an array of disks using the given two dimensional <code>Paint</code>
     * array, the given disk diameter, and the given gap between disks.</p>
     *
     * <p>Throws <code>NullPointerException</code> if the array of paints
     * is <code>null</code> or if all entries in the array of paints are
     * <code>null</code>.  On the other hand, if an individual paint item
     * is <code>null</code>, then its position is simply skipped.</p>
     *
     * <p>If the diameter is less than 5, it is set to 5.</p>
     *
     * <p>If the gap is less than 0, it is set to 0.</p>
     *
     * @param paints      the set of paints for the disks in the paint bar
     * @param diameter    the diameter of each disk
     * @param gap         the gap between disks
     */
    public PaintBar(Paint[][] paints, int diameter, int gap) {
        if (paints == null)
            throw new NullPointerException
                ("PaintBar constructor error: paints array is null");
        
        int rows = paints.length;   // rows in paints
        
        if (rows == 0)
            throw new NullPointerException
                ("PaintBar constructor error: paints array has no data");
        
        int cols = 0;               // cols in paints
        
        if (diameter < 5)
            diameter = 5;
        
        if (gap < 0)
            gap = 0;
        
        this.diameter = diameter;
        this.gap = gap;
        
        base = gap / 2;
        skip = diameter + gap;
        center = skip / 2;
        
        for (int row = 0; row < rows; row++) {
            if (paints[row] != null) {
                int y = yCorner(row);           // circle y corner
                
                int limit = paints[row].length;
                cols = Math.max(cols, limit);
                
                for (int col = 0; col < limit; col++) {
                    if (paints[row][col] != null) {
                        int x = xCorner(col);   // circle x corner
                        
                        installPaint(paints[row][col], x, y, diameter);
                    }
                }
            }
        }
        
        if (number == 0)
            throw new NullPointerException
                ("PaintBar constructor error: paints array has only null data");
        
        bounds = new Rectangle2D.Double(0, 0, skip * cols, skip * rows);
        
        getPaintableSequence().setDefaultBounds2D(bounds);
    }
    
    
    /**
     * <p>Returns the number of paint disks.</p>
     *
     * @return the number of paint disks
     */
    public int getPaintCount() {
        return number;
    }
    
    
    /**
     * <p>Returns the paint of the disk at the given row and col.</p>
     *
     * <p>If the paint bar is HORIZONTAL, row should be 0.</p>
     *
     * <p>If the paint bar is VERTICAL, col should be 0.</p>
     *
     * <p>Returns <code>null</code> if no paint disk was installed
     * at the given row and col.</p>
     *
     * @param row the row of a paint disk
     * @param col the col of a paint disk
     * @return the paint of the disk at the given row and col
     */
    public Paint getPaint(int row, int col) {
        return hitsPaint(xCenter(col), yCenter(row));
    }
    
    
    /**
     * <p>Sets the paint of the disk at the given row and col.</p>
     *
     * <p>Does nothing if the row and col are out of bounds or the
     * paint is <code>null</code>.</p>
     *
     * <p>This method will install a paint disk at a valid row and
     * col if none was installed at construction.</p>
     *
     * @param row the row of a paint disk
     * @param col the col of a paint disk
     * @param paint the new paint for the disk at the given index
     */
    public void setPaint(int row, int col, Paint paint) {
        if (paint == null)
            return;
        
        int x = xCenter(col);
        int y = yCenter(row);
        
        if (! bounds.contains(x, y))
            return;
        
        Paintable paintable = getPaintableSequence().hits(x, y);
        
        if (paintable == null) {
            x = xCorner(col);
            y = yCorner(row);
            installPaint(paint, x, y, diameter);
        }
        else {
            ShapePaintable shapepaintable = (ShapePaintable) paintable;
            shapepaintable.setFillPaint(paint);
        }
    }
    
    
    /**
     * <p>If the given position is within one of the paint disks,
     * then returns the paint of that disk,
     * otherwise returns <code>null</code>.</p>
     *
     * <p>This method may be used to implement a control.</p>
     *
     * @param x the x-coordinate of the position
     * @param y the y-coordinate of the position
     * @return the paint of the disk at x,y or <code>null</code>
     */
    public Paint hitsPaint(double x, double y) {
        Paintable paintable = getPaintableSequence().hits(x, y);
        
        if (paintable == null)
            return null;
        
        ShapePaintable shapepaintable = (ShapePaintable) paintable;
        
        return shapepaintable.getFillPaint();
    }
    
    
    /**
     * <p>If the given position is within one of the paint disks,
     * then returns the paint of that disk,
     * otherwise returns <code>null</code>.</p>
     *
     * <p>This method may be used to implement a control.</p>
     *
     * @param p the position
     * @return the paint of the disk at p or <code>null</code>
     */
    public Paint hitsPaint(Point2D p) {
        if (p == null)
            return null;
        
        return hitsPaint(p.getX(), p.getY());
    
    }
    
    
    /** Returns the diameter. */
    public int diameter() { return diameter; }
    
    
    /** Returns the gap. */
    public int gap() { return gap; }
    
    
    /** Returns the x-corner of a disk in the given col. */
    public int xCorner(int col) { return base + col * skip; }
    
    
    /** Returns the y-corner of a disk in the given row. */
    public int yCorner(int row) { return base + row * skip; }
    
    
    /** Returns the x-center of a disk in the given col. */
    public int xCenter(int col) { return center + col * skip; }
    
    
    /** Returns the y-center of a disk in the given row. */
    public int yCenter(int row) { return center + row * skip; }
    
    
    /**
     * <p>This protected method returns the <code>ShapePaintable</code>
     * that implements the paint disk at the given index.</p>
     *
     * <p>If the index is out of bounds, returns <code>null</code>.</p>
     *
     * @param index the index of a paint disk
     * @return the shape paintable for the disk at the given index
     */
    protected ShapePaintable getShapePaintable(int index) {
        if ((index < 0) || (index >= getPaintCount()))
            return null;
        
        return (ShapePaintable) getPaintableSequence().getPaintable(index);
    }
    
    
    /** Installs the given paint at the given position with the given diameter. */
    private void installPaint(Paint paint, int x, int y, int diameter) {
        Ellipse2D circle =
            new Ellipse2D.Double(x, y, diameter, diameter);
        
        ShapePaintable paintable =
            new ShapePaintable(circle, PaintMode.FILL, paint);
        
        getPaintableSequence().appendPaintable(paintable);
        
        number++;
    }
    
}
