package sim.lib.memory.hex;

import java.awt.*;
import java.awt.event.*;

import gjt.BulletinLayout;
import javax.swing.JScrollPane;

import sim.util.BufferedContainer;

public class HexArea extends BufferedContainer implements MouseListener, MouseMotionListener, FocusListener, KeyListener {

	private Dimension box;
	private WindowCursor cursor;
	private Font current;
	private int dataWidth;
	private int busSize;
	private int addressSpace;
	private int addressWidth;
	private int canFitOnRow;
	private char[][] content;
	private int oldAddress;
	private boolean isEditing;
	private Color backHighlight;
	private Color brushHighlight;
	private ScrollPane parent;

	public HexArea(Font f, char[][] memory, int buses, int addresses, ScrollPane scroller, boolean isSimulating) {
		this.isEditing = !isSimulating;
		this.oldAddress = 0;
		this.backHighlight = Color.white;
		this.brushHighlight = Color.black;

		if (this.isEditing) {
			this.addMouseListener(this);
			this.addMouseMotionListener(this);
			this.addFocusListener(this);
			this.addKeyListener(this);
		}

		this.content = memory;
		this.busSize = buses;
		this.addressSpace = addresses;
		this.current = f;
		this.dataWidth = Integer.toHexString((int) Math.pow(2, this.busSize) - 1).length();
		this.addressWidth = Integer.toHexString(this.addressSpace - 1).length();

		this.old = new char[this.dataWidth];

		this.setLayout(new BulletinLayout());
		this.setSize(1, 1);

		FontMetrics fm = this.getFontMetrics(f);
		this.box = new Dimension(fm.stringWidth("M"), fm.getHeight() + 3);
		this.cursor = new WindowCursor(f);
		this.cursor.setVisible(false);
		this.cursor.deselect();
		this.add(this.cursor);

		this.parent = scroller;
		this.parent.getVAdjustable().setUnitIncrement(this.box.height);
	}

	public char[][] getContent() {
		return this.content;
	}

	public void setContent(char[][] newContent) {
		this.content = newContent;
	}

	public void deselectCursor() {
		this.cursor.deselect();
		this.cursor.setVisible(false);
	}

	public void skipToAddress(int address) {
		this.moveCursorTo(address, 0);
		this.parent.setScrollPosition(0, this.box.height * (1 + address / this.canFitOnRow));
	}

	public boolean needNewBuffer(int availableWidth) {
		int addressBox = this.addressWidth + 3;
		int dataBox = this.dataWidth + 1;
		int fits = (availableWidth - addressBox * this.box.width) / (dataBox * this.box.width);

		return (this.canFitOnRow != fits);
	}

	public void paintBakground(int availableWidth) {
		int addressBox = this.addressWidth + 3;
		int dataBox = this.dataWidth + 1;
		this.canFitOnRow = (availableWidth - addressBox * this.box.width) / (dataBox * this.box.width);

		if (this.canFitOnRow <= 0) {
			this.canFitOnRow = 1;
		}

		this.setSize(availableWidth, ((int) Math.ceil((double) this.addressSpace / (double) this.canFitOnRow) + 2) * this.box.height);
		this.updateBuffers();
		Graphics g = this.getBuffer();

		g.setColor(Color.white);
		g.fillRect(0, 0, this.getSize().width, this.getSize().height);

		g.setColor(Color.lightGray);
		g.fillRect(0, 0, addressBox * this.box.width, this.getSize().height);

		g.setColor(Color.black);
		g.drawLine(addressBox * this.box.width, 0, addressBox * this.box.width, this.getSize().height);
		g.drawLine(0, this.getSize().height - 1, availableWidth, this.getSize().height - 1);

		g.setFont(this.current);

		String addressString;
		String toDraw;

		FontMetrics fm = g.getFontMetrics(this.current);
		int index, w;
		int h = fm.getAscent() + this.box.height;

		// for each row
		for (int address = 0; address < this.addressSpace; address = address + this.canFitOnRow) {
			// draw address

			addressString = Integer.toHexString(address);

			for (index = addressString.length(); index < this.addressWidth; index++) {
				addressString = "0" + addressString;
			}

			w = box.width;
			g.drawString("$", w + (this.box.width - fm.stringWidth("$")) / 2, h);

			for (index = 0; index < this.addressWidth; index++) {
				toDraw = (new Character(addressString.charAt(index))).toString();
				w = w + box.width;
				g.drawString(toDraw, w + (this.box.width - fm.stringWidth(toDraw)) / 2, h);
			}

			w = w + 2 * box.width;

			// draw data
			for (index = 0; (index < this.canFitOnRow) && ((address + index) < this.addressSpace); index++) {
				w = w + box.width;

				for (dataBox = 0; dataBox < this.dataWidth; dataBox++) {
					toDraw = (new Character(this.content[address + index][this.dataWidth - 1 - dataBox])).toString();
					g.drawString(toDraw, w + (this.box.width - fm.stringWidth(toDraw)) / 2, h);
					w = w + box.width;
				}
			}

			//go to next raw
			h = h + this.box.height;
		}

		g.dispose();

		if (this.cursor.isVisible()) {
			this.cursor.setVisible(false);
			this.moveCursorTo(this.cursor.getAddress(), this.cursor.getBitPosition());
		}

		if (!this.isEditing) {
			this.highlightData(this.oldAddress, this.backHighlight, this.brushHighlight);
		}
	}

	public void repaintBakground() {
		int addressBox = this.addressWidth + 3;
		int dataBox = this.dataWidth + 1;

		Graphics g = this.getBuffer();

		g.setColor(Color.white);
		g.fillRect(0, 0, this.getSize().width, this.getSize().height);

		g.setColor(Color.lightGray);
		g.fillRect(0, 0, addressBox * this.box.width, this.getSize().height);

		g.setColor(Color.black);
		g.drawLine(addressBox * this.box.width, 0, addressBox * this.box.width, this.getSize().height);
		g.drawLine(0, this.getSize().height - 1, this.getSize().width, this.getSize().height - 1);

		g.setFont(this.current);

		String addressString;
		String toDraw;

		FontMetrics fm = g.getFontMetrics(this.current);
		int index, w;
		int h = fm.getAscent() + this.box.height;

		// for each row
		for (int address = 0; address < this.addressSpace; address = address + this.canFitOnRow) {
			// draw address

			addressString = Integer.toHexString(address);

			for (index = addressString.length(); index < this.addressWidth; index++) {
				addressString = "0" + addressString;
			}

			w = box.width;
			g.drawString("$", w + (this.box.width - fm.stringWidth("$")) / 2, h);

			for (index = 0; index < this.addressWidth; index++) {
				toDraw = (new Character(addressString.charAt(index))).toString();
				w = w + box.width;
				g.drawString(toDraw, w + (this.box.width - fm.stringWidth(toDraw)) / 2, h);
			}

			w = w + 2 * box.width;

			// draw data
			for (index = 0; (index < this.canFitOnRow) && ((address + index) < this.addressSpace); index++) {
				w = w + box.width;

				for (dataBox = 0; dataBox < this.dataWidth; dataBox++) {
					toDraw = (new Character(this.content[address + index][this.dataWidth - 1 - dataBox])).toString();
					g.drawString(toDraw, w + (this.box.width - fm.stringWidth(toDraw)) / 2, h);
					w = w + box.width;
				}
			}

			//go to next raw
			h = h + this.box.height;
		}

		g.dispose();
	}

	public void changeParameters(char[][] memory, int buses, int addresses) {
		this.deselectCursor();

		this.content = memory;
		this.busSize = buses;
		this.addressSpace = addresses;
		this.dataWidth = Integer.toHexString((int) Math.pow(2, this.busSize) - 1).length();
		this.addressWidth = Integer.toHexString(this.addressSpace - 1).length();
		this.old = new char[this.dataWidth];

		this.paintBakground(this.getSize().width);
	}

	public void highlightData(int address, Color back, Color brush) {
		Graphics g = this.getBuffer();
		g.setFont(this.current);
		FontMetrics fm = g.getFontMetrics(this.current);

		int row = (1 + address / this.canFitOnRow) * this.box.height;
		int col = (this.addressWidth + 4 + (this.dataWidth + 1) * (address % this.canFitOnRow)) * this.box.width;

		Rectangle clip = new Rectangle(col, row, this.dataWidth * this.box.width, this.box.height);

		g.setColor(back);
		g.fillRect(clip.x, clip.y, clip.width, clip.height);
		g.setColor(brush);

		String toDraw;

		for (int index = 0; index < this.dataWidth; index++) {
			toDraw = (new Character(this.content[address][this.dataWidth - 1 - index])).toString();
			g.drawString(toDraw, col + (this.box.width - fm.stringWidth(toDraw)) / 2, row + fm.getAscent());
			col = col + box.width;
		}

		g.dispose();
		this.buffers.blitWorkplaceToScreen(clip);

		this.oldAddress = address;

		this.backHighlight = back;
		this.brushHighlight = brush;
	}

	public void eliminateHighlights() {
		this.highlightData(this.oldAddress, Color.white, Color.black);
	}

	public void showAddress(int address) {
		this.parent.setScrollPosition(0, this.box.height * (1 + address / this.canFitOnRow));
	}

	public void prepareForSimulation() {
		if (this.cursor.isSelected()) {
			this.updateMemoryContent(this.cursor.getAddress());
		}

		this.isEditing = false;
		this.oldAddress = 0;

		this.removeMouseListener(this);
		this.removeMouseMotionListener(this);
		this.removeFocusListener(this);
		this.removeKeyListener(this);
	}

	public void prepareForEditing() {
		this.isEditing = true;

		this.addMouseListener(this);
		this.addMouseMotionListener(this);
		this.addFocusListener(this);
		this.addKeyListener(this);

		this.highlightData(this.oldAddress, Color.white, Color.black);
	}
	/* ==================================================================
	 Handle cursor
	 ================================================================= */
	private char old[];
	private boolean isModified = false;
	private boolean isDragging = false;

	private void moveCursorTo(int address, int position) {
		int loop;
		int row = (1 + address / this.canFitOnRow) * this.box.height;
		int col = (this.addressWidth + 4 + (this.dataWidth + 1) * (address % this.canFitOnRow) + position) * this.box.width;

		if (this.cursor.isSelected() && (address != this.cursor.getAddress())) {
			if (this.isModified) {
				this.isModified = false;
				this.updateMemoryContent(this.cursor.getAddress());
			}

			for (loop = 0; loop < this.dataWidth; loop++) {
				this.old[loop] = this.content[address][loop];
			}
		} else if (!this.cursor.isSelected()) {
			this.isModified = false;

			for (loop = 0; loop < this.dataWidth; loop++) {
				this.old[loop] = this.content[address][loop];
			}
		}

		if (this.cursor.isVisible()) {
			this.cursor.deselect();
			this.paintComponent(this.cursor);
		} else {
			this.cursor.setVisible(true);
		}

		this.cursor.setLocation(col, row);
		this.cursor.setCharacter(this.old[this.dataWidth - 1 - position]);
		this.cursor.select(address, position);
		this.paintComponent(this.cursor);

		this.requestFocus();
	}

	private void updateMemoryContent(int address) {
		int index;
		String value = (new Character(this.old[this.dataWidth - 1])).toString();
		this.cursor.deselect();

		for (index = 1; index < this.dataWidth; index++) {
			value = value + this.old[this.dataWidth - 1 - index];
		}

		try {
			index = Integer.valueOf(value, 16).intValue();

			if ((index >= 0) && (index < Math.pow(2, this.busSize))) {
				for (index = 0; index < this.dataWidth; index++) {
					this.content[address][index] = this.old[index];
				}

				this.paintComponent(this.cursor);
			} else {
				for (index = 0; index < this.dataWidth; index++) {
					this.content[address][index] = '-';
				}

				this.highlightData(address, Color.white, Color.black);
			}
		} catch (NumberFormatException e) {
			for (index = 0; index < this.dataWidth; index++) {
				this.content[address][index] = '-';
			}

			this.highlightData(address, Color.white, Color.black);
		}

		this.cursor.setVisible(false);
	}

	/* ==================================================================
	 Mouse listener
	 ================================================================= */
	private Point getPositionAt(Point pressed) {
		Point result = new Point(-1, -1);

		int row = pressed.y / this.box.height;
		int col = pressed.x / this.box.width;

		row = row - 1;
		col = col - this.addressWidth - 4;

		if ((col / (this.dataWidth + 1) < this.canFitOnRow) && (row >= 0) && (col >= 0)) {
			result.x = row * this.canFitOnRow + col / (this.dataWidth + 1);
			result.y = col % (this.dataWidth + 1);
		}

		return result;
	}

	public void mousePressed(MouseEvent event) {
		this.isDragging = true;
		Point where = this.getPositionAt(event.getPoint());

		if ((where.x >= 0) && (where.x < this.addressSpace) && (where.y >= 0) && (where.y < this.dataWidth)) {
			this.moveCursorTo(where.x, where.y);
		}
	}

	public void mouseReleased(MouseEvent event) {
		this.isDragging = false;
	}

	public void mouseClicked(MouseEvent event) {
	}

	public void mouseMoved(MouseEvent event) {
	}

	public void mouseDragged(MouseEvent event) {
		Point where = this.getPositionAt(event.getPoint());

		if ((where.x >= 0) && (where.x < this.addressSpace) && (where.y >= 0) && (where.y < this.dataWidth)) {
			this.moveCursorTo(where.x, where.y);
		}
	}

	public void mouseEntered(MouseEvent e) {
	}

	public void mouseExited(MouseEvent e) {
	}

	/* ==================================================================
	 Focus handling
	 ================================================================= */
	public void focusGained(FocusEvent e) {
	}

	public void focusLost(FocusEvent e) {
		if (this.cursor.isSelected()) {
			// deselect
		}
	}

	/* ==================================================================
	 Key handling
	 ================================================================= */
	public void keyPressed(KeyEvent e) {
	}

	public void keyReleased(KeyEvent e) {
		char input;

		if ((!this.isDragging) && (this.cursor.isSelected())) {
			switch (e.getKeyCode()) {
				case KeyEvent.VK_ESCAPE:
					this.highlightData(this.cursor.getAddress(), Color.white, Color.black);
					this.cursor.deselect();
					break;
				case KeyEvent.VK_HOME:
					this.moveCursorTo(this.cursor.getAddress() - this.cursor.getAddress() % this.canFitOnRow, 0);
					break;
				case KeyEvent.VK_END:
					if (this.cursor.getAddress() - this.cursor.getAddress() % this.canFitOnRow - 1 + this.canFitOnRow < this.addressSpace) {
						this.moveCursorTo(this.cursor.getAddress() - this.cursor.getAddress() % this.canFitOnRow - 1 + this.canFitOnRow, 0);
					} else {
						this.moveCursorTo(this.addressSpace - 1, 0);
					}

					break;
				case KeyEvent.VK_ENTER:
					this.updateMemoryContent(this.cursor.getAddress());
					break;
				case KeyEvent.VK_LEFT:
					if (this.cursor.getBitPosition() != 0) {
						this.moveCursorTo(this.cursor.getAddress(), this.cursor.getBitPosition() - 1);
					} else if (this.cursor.getAddress() != 0) {
						this.moveCursorTo(this.cursor.getAddress() - 1, this.dataWidth - 1);
					}

					break;
				case KeyEvent.VK_RIGHT:
					if (this.cursor.getBitPosition() != (this.dataWidth - 1)) {
						this.moveCursorTo(this.cursor.getAddress(), this.cursor.getBitPosition() + 1);
					} else if (this.cursor.getAddress() != (this.addressSpace - 1)) {
						this.moveCursorTo(this.cursor.getAddress() + 1, 0);
					}

					break;
				case KeyEvent.VK_UP:
					if (this.cursor.getAddress() - this.canFitOnRow >= 0) {
						this.moveCursorTo(this.cursor.getAddress() - this.canFitOnRow, this.cursor.getBitPosition());
					}

					break;
				case KeyEvent.VK_DOWN:
					if (this.cursor.getAddress() + this.canFitOnRow < this.addressSpace) {
						this.moveCursorTo(this.cursor.getAddress() + this.canFitOnRow, this.cursor.getBitPosition());
					}

					break;

				default:
					input = e.getKeyChar();

					if (input != KeyEvent.CHAR_UNDEFINED) {
						input = Character.toLowerCase(input);

						this.old[this.dataWidth - 1 - this.cursor.getBitPosition()] = input;
						this.isModified = true;
						this.cursor.setCharacter(input);

						if (this.cursor.getBitPosition() != (this.dataWidth - 1)) {
							this.moveCursorTo(this.cursor.getAddress(), this.cursor.getBitPosition() + 1);
						} else if (this.cursor.getAddress() != (this.addressSpace - 1)) {
							this.moveCursorTo(this.cursor.getAddress() + 1, 0);
						} else {
							this.updateMemoryContent(this.cursor.getAddress());
						}
					}
			}
		}

	}

	public void keyTyped(KeyEvent e) {
	}

	/* ==================================================================
	 Cursor
	 ================================================================= */
	private class WindowCursor extends Component {

		private Font current;
		private FontMetrics fm;
		private int address;
		private int position;
		private String toDraw;
		private boolean selected;

		public WindowCursor(Font f) {
			super();

			this.current = f;

			this.fm = this.getFontMetrics(f);
			this.setSize(this.fm.stringWidth("M"), this.fm.getHeight() + 3);

			this.selected = false;
		}

		public void select(int where, int bit) {
			this.selected = true;

			this.address = where;
			this.position = bit;
		}

		public void select() {
			this.selected = true;
		}

		public int getAddress() {
			return this.address;
		}

		public int getBitPosition() {
			return this.position;
		}

		public void deselect() {
			this.selected = false;
		}

		public boolean isSelected() {
			return this.selected;
		}

		public void setCharacter(char c) {
			this.toDraw = (new Character(c)).toString();
		}

		public char getCharacter() {
			return this.toDraw.charAt(0);
		}

		public void paint(Graphics g) {
			if (this.isVisible()) {
				g.setFont(this.current);

				if (this.selected) {
					g.setColor(Color.blue.darker());
					g.fillRect(0, 0, this.getSize().width, this.getSize().height);
					g.setColor(Color.white);
				} else {
					g.setColor(Color.white);
					g.fillRect(0, 0, this.getSize().width, this.getSize().height);
					g.setColor(Color.black);
				}

				g.drawString(this.toDraw, (this.getSize().width - this.fm.stringWidth(this.toDraw)) / 2, this.fm.getAscent());
			}
		}
	}
}
