/* Copyright (c) 2010, Carl Burch. License information is located in the
 * com.cburch.logisim.Main source code and at www.cburch.com/logisim/. */

package com.cburch.logisim.gui.generic;

import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.image.MemoryImageSource;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.Arrays;

public class GridPainter {
	public static final String ZOOM_PROPERTY = "zoom";
	public static final String SHOW_GRID_PROPERTY = "showgrid";
	
	private static final int GRID_DOT_COLOR = 0xFF777777;
	private static final int GRID_DOT_ZOOMED_COLOR = 0xFFCCCCCC;

	private static final Color GRID_ZOOMED_OUT_COLOR = new Color(210, 210, 210);
	
	private class Listener implements PropertyChangeListener {
		public void propertyChange(PropertyChangeEvent event) {
			String prop = event.getPropertyName();
			Object val = event.getNewValue();
			if (prop.equals(ZoomModel.ZOOM)) {
				setZoomFactor(((Double) val).doubleValue());
				destination.repaint();
			} else if (prop.equals(ZoomModel.SHOW_GRID)) {
				setShowGrid(((Boolean) val).booleanValue());
				destination.repaint();
			}
		}
	}

	private Component destination;
	private PropertyChangeSupport support;
	private Listener listener;
	private ZoomModel zoomModel;
	private boolean showGrid;
	private int gridSize;
	private double zoomFactor;
	private Image gridImage;
	private int gridImageWidth;
	
	public GridPainter(Component destination) {
		this.destination = destination;
		support = new PropertyChangeSupport(this);
		showGrid = true;
		gridSize = 10;
		zoomFactor = 1.0;
		updateGridImage(gridSize, zoomFactor);
	}
	
	public void addPropertyChangeListener(String prop,
			PropertyChangeListener listener) {
		support.addPropertyChangeListener(prop, listener);
	}
	
	public void removePropertyChangeListener(String prop,
			PropertyChangeListener listener) {
		support.removePropertyChangeListener(prop, listener);
	}
	
	public boolean getShowGrid() {
		return showGrid;
	}
	
	public void setShowGrid(boolean value) {
		if (showGrid != value) {
			showGrid = value;
			support.firePropertyChange(SHOW_GRID_PROPERTY, !value, value);
		}
	}
	
	public double getZoomFactor() {
		return zoomFactor;
	}
	
	public void setZoomFactor(double value) {
		double oldValue = zoomFactor;
		if (oldValue != value) {
			zoomFactor = value;
			updateGridImage(gridSize, value);
			support.firePropertyChange(ZOOM_PROPERTY, Double.valueOf(oldValue),
					Double.valueOf(value));
		}
	}
	
	public ZoomModel getZoomModel() {
		return zoomModel;
	}
	
	public void setZoomModel(ZoomModel model) {
		ZoomModel old = zoomModel;
		if (model != old) {
			if (listener == null) {
				listener = new Listener();
			}
			if (old != null) {
				old.removePropertyChangeListener(ZoomModel.ZOOM, listener);
				old.removePropertyChangeListener(ZoomModel.SHOW_GRID, listener);
			}
			zoomModel = model;
			if (model != null) {
				model.addPropertyChangeListener(ZoomModel.ZOOM, listener);
				model.addPropertyChangeListener(ZoomModel.SHOW_GRID, listener);
			}
			setShowGrid(model.getShowGrid());
			setZoomFactor(model.getZoomFactor());
			destination.repaint();
		}
	}

	public void paintGrid(Graphics g) {
		Rectangle clip = g.getClipBounds();
		Component dest = destination;
		double zoom = zoomFactor;
		int size = gridSize;

		if (!showGrid) return;

		Image img = gridImage;
		int w = gridImageWidth;
		if (img == null) {
			paintGridOld(g, size, zoom, clip);
			return;
		}
		int x0 = (clip.x / w) * w; // round down to multiple of w
		int y0 = (clip.y / w) * w;
		for (int x = 0; x < clip.width + w; x += w) {
			for (int y = 0; y < clip.height + w; y += w) {
				g.drawImage(img, x0 + x, y0 + y, dest);
			}
		}
	}
	
	private void paintGridOld(Graphics g, int size, double f, Rectangle clip) {
		g.setColor(Color.GRAY);
		if (f == 1.0) {
			int start_x = ((clip.x + 9) / size) * size;
			int start_y = ((clip.y + 9) / size) * size;
			for (int x = 0; x < clip.width; x += size) {
				for (int y = 0; y < clip.height; y += size) {
					g.fillRect(start_x + x, start_y + y, 1, 1);
				}
			}
		} else {
			/* Kevin Walsh of Cornell suggested the code below instead. */
			int x0 = size * (int) Math.ceil(clip.x / f / size);
			int x1 = x0 + (int) (clip.width / f);
			int y0 = size * (int) Math.ceil(clip.y / f / size);
			int y1 = y0 + (int) (clip.height / f);
			if (f <= 0.5) g.setColor(GRID_ZOOMED_OUT_COLOR);
			for (double x = x0; x < x1; x += size) {
				for (double y = y0; y < y1; y += size) {
					int sx = (int) Math.round(f * x);
					int sy = (int) Math.round(f * y);
					g.fillRect(sx, sy, 1, 1);
				}
			}
			if (f <= 0.5) { // make every 5th pixel darker
				int size5 = 5 * size;
				g.setColor(Color.GRAY);
				x0 = size5 * (int) Math.ceil(clip.x / f / size5);
				y0 = size5 * (int) Math.ceil(clip.y / f / size5);
				for (double x = x0; x < x1; x += size5) {
					for (double y = y0; y < y1; y += size5) {
						int sx = (int) Math.round(f * x);
						int sy = (int) Math.round(f * y);
						g.fillRect(sx, sy, 1, 1);
					}
				}
			}               
			
			/* Original code by Carl Burch
			int x0 = 10 * (int) Math.ceil(clip.x / f / 10);
			int x1 = x0 + (int)(clip.width / f);
			int y0 = 10 * (int) Math.ceil(clip.y / f / 10);
			int y1 = y0 + (int) (clip.height / f);
			int s = f > 0.5 ? 1 : f > 0.25 ? 2 : 3;
			int i0 = s - ((x0 + 10*s - 1) % (s * 10)) / 10 - 1;
			int j0 = s - ((y1 + 10*s - 1) % (s * 10)) / 10 - 1;
			for (int i = 0; i < s; i++) {
				for (int x = x0+i*10; x < x1; x += s*10) {
					for (int j = 0; j < s; j++) {
						g.setColor(i == i0 && j == j0 ? Color.gray : GRID_ZOOMED_OUT_COLOR);
						for (int y = y0+j*10; y < y1; y += s*10) {
							int sx = (int) Math.round(f * x);
							int sy = (int) Math.round(f * y);
							g.fillRect(sx, sy, 1, 1);
						}
					}
				}
			}
			*/
		}
	}

	//
	// creating the grid image
	//
	private void updateGridImage(int size, double f) {
		double ww = f * size * 5;
		while (2 * ww < 150) ww *= 2;
		int w = (int) Math.round(ww);
		int[] pix = new int[w * w];
		Arrays.fill(pix, 0xFFFFFF);

		if (f == 1.0) {
			int lineStep = size * w;
			for (int j = 0; j < pix.length; j += lineStep) {
				for (int i = 0; i < w; i += size) {
					pix[i + j] = GRID_DOT_COLOR;
				}
			}
		} else {
			int off0 = 0;
			int off1 = 1;
			if (f >= 2.0) { // we'll draw several pixels for each grid point
				int num = (int) (f + 0.001);
				off0 = -(num / 2);
				off1 = off0 + num; 
			}
			
			int dotColor = f <= 0.5 ? GRID_DOT_ZOOMED_COLOR : GRID_DOT_COLOR;
			for (int j = 0; true; j += size) {
				int y = (int) Math.round(f * j);
				if (y + off0 >= w) break;
				
				for (int yo = y + off0; yo < y + off1; yo++) {
					if (yo >= 0 && yo < w) {
						int base = yo * w;
						for (int i = 0; true; i += size) {
							int x = (int) Math.round(f * i);
							if (x + off0 >= w) break;
							for (int xo = x + off0; xo < x + off1; xo++) {
								if (xo >= 0 && xo < w) {
									pix[base + xo] = dotColor;
								}
							}
						}
					}
				}
			}
			if (f <= 0.5) { // repaint over every 5th pixel so it is darker
				int size5 = size * 5;
				for (int j = 0; true; j += size5) {
					int y = (int) Math.round(f * j);
					if (y >= w) break;
					y *= w;
					
					for (int i = 0; true; i += size5) {
						int x = (int) Math.round(f * i);
						if (x >= w) break;
						pix[y + x] = GRID_DOT_COLOR;
					}
				}
			}
		}
		gridImage = destination.createImage(new MemoryImageSource(w, w, pix, 0, w));
		gridImageWidth = w;
	}
}
