/* 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.analyze.gui;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.MouseEvent;
import java.util.List;

import javax.swing.JPanel;

import com.cburch.logisim.analyze.model.AnalyzerModel;
import com.cburch.logisim.analyze.model.OutputExpressionsEvent;
import com.cburch.logisim.analyze.model.OutputExpressionsListener;
import com.cburch.logisim.analyze.model.Entry;
import com.cburch.logisim.analyze.model.Implicant;
import com.cburch.logisim.analyze.model.TruthTable;
import com.cburch.logisim.analyze.model.TruthTableEvent;
import com.cburch.logisim.analyze.model.TruthTableListener;
import com.cburch.logisim.analyze.model.VariableList;
import com.cburch.logisim.util.GraphicsUtil;

class KarnaughMapPanel extends JPanel implements TruthTablePanel {
	private static final Font HEAD_FONT = new Font("Serif", Font.BOLD, 14);
	private static final Font BODY_FONT = new Font("Serif", Font.PLAIN, 14);
	private static final Color[] IMP_COLORS = new Color[] {
		new Color(255, 0, 0, 128),
		new Color(0, 150, 0, 128),
		new Color(0, 0, 255, 128),
		new Color(255, 0, 255, 128),
	};

	private static final int MAX_VARS = 4;
	
	private static final int[] ROW_VARS = { 0, 0, 1, 1, 2 };
	private static final int[] COL_VARS = { 0, 1, 1, 2, 2 };
	private static final int CELL_HORZ_SEP = 10;
	private static final int CELL_VERT_SEP = 10;
	private static final int IMP_INSET = 4;
	private static final int IMP_RADIUS = 5;
	
	private class MyListener
			implements OutputExpressionsListener, TruthTableListener {
		public void expressionChanged(OutputExpressionsEvent event) {
			if (event.getType() == OutputExpressionsEvent.OUTPUT_MINIMAL
					&& event.getVariable().equals(output)) {
				repaint();
			}
		}
		
		public void cellsChanged(TruthTableEvent event) {
			repaint();
		}

		public void structureChanged(TruthTableEvent event) {
			computePreferredSize();
		}
		
	}

	private MyListener myListener = new MyListener();
	private AnalyzerModel model;
	private String output;
	private int headHeight;
	private int cellWidth = 1;
	private int cellHeight = 1;
	private int tableWidth;
	private int tableHeight;
	private int provisionalX;
	private int provisionalY;
	private Entry provisionalValue = null;
	
	public KarnaughMapPanel(AnalyzerModel model) {
		this.model = model;
		model.getOutputExpressions().addOutputExpressionsListener(myListener);
		model.getTruthTable().addTruthTableListener(myListener);
		setToolTipText(" ");
	}
	
	public void setOutput(String value) {
		boolean recompute = (output == null || value == null) && output != value;
		output = value;
		if (recompute) computePreferredSize();
		else repaint();
	}
	
	public TruthTable getTruthTable() {
		return model.getTruthTable();
	}
	
	public int getRow(MouseEvent event) {
		TruthTable table = model.getTruthTable();
		int inputs = table.getInputColumnCount();
		if (inputs >= ROW_VARS.length) return -1;
		int left = computeMargin(getWidth(), tableWidth);
		int top = computeMargin(getHeight(), tableHeight);
		int x = event.getX() - left - headHeight - cellWidth;
		int y = event.getY() - top - headHeight - cellHeight;
		if (x < 0 || y < 0) return -1;
		int row = y / cellHeight;
		int col = x / cellWidth;
		int rows = 1 << ROW_VARS[inputs];
		int cols = 1 << COL_VARS[inputs];
		if (row >= rows || col >= cols) return -1;
		return getTableRow(row, col, rows, cols); 
	}
	
	public int getOutputColumn(MouseEvent event) {
		return model.getOutputs().indexOf(output);
	}
	
	public void setEntryProvisional(int y, int x, Entry value) {
		provisionalY = y;
		provisionalX = x;
		provisionalValue = value;
		repaint();
	}
	
	@Override
	public String getToolTipText(MouseEvent event) {
		TruthTable table = model.getTruthTable();
		int row = getRow(event);
		int col = getOutputColumn(event);
		Entry entry = table.getOutputEntry(row, col);
		return entry.getErrorMessage();
	}
	
	void localeChanged() {
		computePreferredSize();
		repaint();
	}
	
	private void computePreferredSize() {
		Graphics g = getGraphics();
		TruthTable table = model.getTruthTable();
		
		String message = null;
		if (output == null) {
			message = Strings.get("karnaughNoOutputError");
		} else if (table.getInputColumnCount() > MAX_VARS) {
			message = Strings.get("karnaughTooManyInputsError");
		}
		if (message != null) {
			if (g == null) {
				tableHeight = 15;
				tableWidth = 100;
			} else {
				FontMetrics fm = g.getFontMetrics(BODY_FONT);
				tableHeight = fm.getHeight();
				tableWidth = fm.stringWidth(message);
			}
			setPreferredSize(new Dimension(tableWidth, tableHeight));
			repaint();
			return;
		}
		
		if (g == null) {
			headHeight = 16;
			cellHeight = 16;
			cellWidth = 24;
		} else {
			FontMetrics headFm = g.getFontMetrics(HEAD_FONT);
			headHeight = headFm.getHeight();
			
			FontMetrics fm = g.getFontMetrics(BODY_FONT);
			cellHeight = fm.getAscent() + CELL_VERT_SEP;
			cellWidth = fm.stringWidth("00") + CELL_HORZ_SEP;
		}
		
		int rows = 1 << ROW_VARS[table.getInputColumnCount()];
		int cols = 1 << COL_VARS[table.getInputColumnCount()];
		tableWidth = headHeight + cellWidth * (cols + 1); 
		tableHeight = headHeight + cellHeight * (rows + 1);
		setPreferredSize(new Dimension(tableWidth, tableHeight));
		invalidate();
		repaint();
	}

	@Override
	public void paintComponent(Graphics g) {
		super.paintComponent(g);
		
		TruthTable table = model.getTruthTable();
		int inputCount = table.getInputColumnCount();
		Dimension sz = getSize();
		String message = null;
		if (output == null) {
			message = Strings.get("karnaughNoOutputError");
		} else if (inputCount > MAX_VARS) {
			message = Strings.get("karnaughTooManyInputsError");
		}
		if (message != null) {
			g.setFont(BODY_FONT);
			GraphicsUtil.drawCenteredText(g, message, sz.width / 2, sz.height / 2);
			return;
		}

		int left = computeMargin(sz.width, tableWidth);
		int top = computeMargin(sz.height, tableHeight);
		int x = left;
		int y = top;
		int rowVars = ROW_VARS[inputCount];
		int colVars = COL_VARS[inputCount];
		int rows = 1 << rowVars;
		int cols = 1 << colVars;
		
		g.setFont(HEAD_FONT);
		FontMetrics headFm = g.getFontMetrics();
		String rowHeader = header(0, rowVars);
		String colHeader = header(rowVars, rowVars + colVars);
		int xoffs = (tableWidth + headHeight + cellWidth - headFm.stringWidth(colHeader)) / 2;
		g.drawString(colHeader, x + xoffs, y + headFm.getAscent());
		int headerWidth = headFm.stringWidth(rowHeader);
		if (headerWidth <= headHeight) {
			int headX = x + (headHeight - headerWidth) / 2;
			int headY = y + (tableHeight + headHeight + cellHeight + headFm.getAscent()) / 2;
			g.drawString(rowHeader, headX, headY);
		} else if (g instanceof Graphics2D){
			Graphics2D g2 = (Graphics2D) g.create();
			int yoffs = (tableHeight + headHeight + cellHeight + headerWidth) / 2;
			int headX = x + headFm.getAscent();
			int headY = y + yoffs;
			g2.rotate(-Math.PI / 2.0);
			g2.drawString(rowHeader, -headY, headX);
			g2.dispose();
		}
		
		x += headHeight;
		y += headHeight;
		g.setFont(BODY_FONT);
		FontMetrics fm = g.getFontMetrics();
		int dy = (cellHeight + fm.getAscent()) / 2;
		for (int i = 0; i < cols; i++) {
			String label = label(i, cols);
			g.drawString(label,
				x + (i + 1) * cellWidth + (cellWidth - fm.stringWidth(label)) / 2,
				y + dy);
		}
		for (int i = 0; i < rows; i++) {
			String label = label(i, rows);
			g.drawString(label,
				x + (cellWidth - fm.stringWidth(label)) / 2,
				y + (i + 1) * cellHeight + dy);
		}
		
		int outputColumn = table.getOutputIndex(output);
		x += cellWidth;
		y += cellHeight;
		g.setColor(ERROR_COLOR);
		for (int i = 0; i < rows; i++) {
			for (int j = 0; j < cols; j++) {
				int row = getTableRow(i, j, rows, cols);
				Entry entry = table.getOutputEntry(row, outputColumn);
				if (provisionalValue != null && row == provisionalY
						&& outputColumn == provisionalX) entry = provisionalValue;
				if (entry.isError()) {
					g.fillRect(x + j * cellWidth, y + i * cellHeight, cellWidth, cellHeight);
				}
			}
		}
		
		List<Implicant> implicants = model.getOutputExpressions().getMinimalImplicants(output);
		if (implicants != null) {
			int index = 0;
			for (Implicant imp : implicants) {
				g.setColor(IMP_COLORS[index % IMP_COLORS.length]);
				paintImplicant(g, imp, x, y, rows, cols);
				index++;
			}
		}
		
		g.setColor(Color.GRAY);
		if (cols > 1 || inputCount == 0) g.drawLine(x, y, left + tableWidth, y);
		if (rows > 1 || inputCount == 0) g.drawLine(x, y, x, top + tableHeight);
		if (outputColumn < 0) return;
		
		g.setColor(Color.BLACK);
		for (int i = 0; i < rows; i++) {
			for (int j = 0; j < cols; j++) {
				int row = getTableRow(i, j, rows, cols);
				if (provisionalValue != null && row == provisionalY
						&& outputColumn == provisionalX) {
					String text = provisionalValue.getDescription();
					g.setColor(Color.GREEN);
					g.drawString(text,
							x + j * cellWidth + (cellWidth - fm.stringWidth(text)) / 2,
							y + i * cellHeight + dy);
					g.setColor(Color.BLACK);
				} else {
					Entry entry = table.getOutputEntry(row, outputColumn);
					String text = entry.getDescription();
					g.drawString(text,
							x + j * cellWidth + (cellWidth - fm.stringWidth(text)) / 2,
							y + i * cellHeight + dy);
				}
			}
		}
	}

	private void paintImplicant(Graphics g, Implicant imp, int x, int y,
			int rows, int cols) {
		int rowMax = -1;
		int rowMin = rows;
		int colMax = -1;
		int colMin = cols;
		boolean oneRowFound = false;
		int count = 0;
		for (Implicant sq : imp.getTerms()) {
			int tableRow = sq.getRow();
			int row = getRow(tableRow, rows, cols);
			int col = getCol(tableRow, rows, cols);
			if (row == 1) oneRowFound = true;
			if (row > rowMax) rowMax = row;
			if (row < rowMin) rowMin = row;
			if (col > colMax) colMax = col;
			if (col < colMin) colMin = col;
			++count;
		}
		
		int numCols = colMax - colMin + 1;
		int numRows = rowMax - rowMin + 1;
		int covered = numCols * numRows;
		int d = 2 * IMP_RADIUS;
		if (covered == count) {
			g.fillRoundRect(x + colMin * cellWidth + IMP_INSET,
					y + rowMin * cellHeight + IMP_INSET,
					numCols * cellWidth - 2 * IMP_INSET,
					numRows * cellHeight - 2 * IMP_INSET,
					d, d);
		} else if (covered == 16) {
			if (count == 4) {
				int w = cellWidth - IMP_INSET;
				int h = cellHeight - IMP_INSET;
				int x1 = x + 3 * cellWidth + IMP_INSET;
				int y1 = y + 3 * cellHeight + IMP_INSET;
				g.fillRoundRect(x,  y,  w, h, d, d);
				g.fillRoundRect(x1, y,  w, h, d, d);
				g.fillRoundRect(x,  y1, w, h, d, d);
				g.fillRoundRect(x1, y1, w, h, d, d);
			} else if (oneRowFound) { // first and last columns
				int w = cellWidth - IMP_INSET;
				int h = 4 * cellHeight - 2 * IMP_INSET;
				int x1 = x + 3 * cellWidth + IMP_INSET;
				g.fillRoundRect(x,  y + IMP_INSET, w, h, d, d);
				g.fillRoundRect(x1, y + IMP_INSET, w, h, d, d);
			} else { // first and last rows
				int w = 4 * cellWidth - 2 * IMP_INSET;
				int h = cellHeight - IMP_INSET;
				int y1 = y + 3 * cellHeight + IMP_INSET;
				g.fillRoundRect(x + IMP_INSET, y,  w, h, d, d);
				g.fillRoundRect(x + IMP_INSET, y1, w, h, d, d);
			}
		} else if (numCols == 4) {
			int top = y + rowMin * cellHeight + IMP_INSET;
			int w = cellWidth - IMP_INSET;
			int h = numRows * cellHeight - 2 * IMP_INSET;
			// handle half going off left edge
			g.fillRoundRect(x, top, w, h, d, d);
			// handle half going off right edge
			g.fillRoundRect(x + 3 * cellWidth + IMP_INSET, top, w, h, d, d);
			/* This is the proper way, with no rounded rectangles along
			 * the table's edge; but I found that the different regions were
			 * liable to overlap, particularly the arcs with the rectangles.
			 * (Plus, I was too lazy to figure this out for the 16 case.)
			int y0 = y + rowMin * cellHeight + IMP_INSET;
			int y1 = y + rowMax * cellHeight + cellHeight - IMP_INSET;
			int dy = y1 - y0;
			int x0 = x + cellWidth - IMP_INSET;
			int x1 = x + 3 * cellWidth + IMP_INSET;
			
			// half going off left edge
			g.fillRect(x,               y0, cellWidth - IMP_INSET - IMP_RADIUS, dy);
			g.fillRect(x0 - IMP_RADIUS, y0 + IMP_RADIUS, IMP_RADIUS, dy - d);
			g.fillArc(x0 - d, y0, d, d, 0, 90);
			g.fillArc(x0 - d, y1 - d, d, d, 0, -90);
			
			// half going off right edge
			g.fillRect(x1 + IMP_RADIUS, y0, cellWidth - IMP_INSET - IMP_RADIUS, dy);
			g.fillRect(x1, y0 + IMP_RADIUS, IMP_RADIUS, dy - d);
			g.fillArc(x1, y0, d, d, 180, 90);
			g.fillArc(x1, y1 - d, d, d, 180, -90);
			*/
		} else { // numRows == 4
			int left = x + colMin * cellWidth + IMP_INSET;
			int w = numCols * cellWidth - 2 * IMP_INSET;
			int h = cellHeight - IMP_INSET;
			// handle half going off top edge
			g.fillRoundRect(left, y, w, h, d, d);
			// handle half going off right edge
			g.fillRoundRect(left, y + 3 * cellHeight + IMP_INSET, w, h, d, d);
		}
	}
	
	private String header(int start, int stop) {
		if (start >= stop) return "";
		VariableList inputs = model.getInputs();
		StringBuilder ret = new StringBuilder(inputs.get(start));
		for (int i = start + 1; i < stop; i++) {
			ret.append(", ");
			ret.append(inputs.get(i));
		}
		return ret.toString();
	}
	
	private String label(int row, int rows) {
		switch (rows) {
		case 2: return "" + row;
		case 4:
			switch (row) {
			case 0: return "00";
			case 1: return "01";
			case 2: return "11";
			case 3: return "10";
			}
		default: return "";
		}
	}
	
	private int getTableRow(int row, int col, int rows, int cols) {
		return toRow(row, rows) * cols + toRow(col, cols);
	}
	
	private int toRow(int row, int rows) {
		if (rows == 4) {
			switch (row) {
			case 2: return 3;
			case 3: return 2;
			default: return row;
			}
		} else {
			return row;
		}
	}
	
	private int getRow(int tableRow, int rows, int cols) {
		int ret = tableRow / cols;
		switch (ret) {
		case 2: return 3;
		case 3: return 2;
		default: return ret;
		}
	}
	
	private int getCol(int tableRow, int rows, int cols) {
		int ret = tableRow % cols;
		switch (ret) {
		case 2: return 3;
		case 3: return 2;
		default: return ret;
		}
	}
	
	private int computeMargin(int compDim, int tableDim) {
		int ret = (compDim - tableDim) / 2;
		return ret >= 0 ? ret : Math.max(-headHeight, compDim - tableDim);
	}

}
