package sim.lib.memory.hex;

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

import gjt.BulletinLayout;

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 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());
			}
		}
	}
}