/* ###
 * IP: GHIDRA
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*
 * BytesFieldFactory.java
 *
 * Created on June 18, 2001, 11:01 AM
 */

package ghidra.app.util.viewer.field;

import java.awt.Color;
import java.math.BigInteger;

import org.apache.commons.lang3.ArrayUtils;

import docking.widgets.fieldpanel.field.*;
import docking.widgets.fieldpanel.support.FieldLocation;
import docking.widgets.fieldpanel.support.RowColLocation;
import ghidra.app.util.ListingHighlightProvider;
import ghidra.app.util.viewer.format.FieldFormatModel;
import ghidra.app.util.viewer.proxy.ProxyObj;
import ghidra.framework.options.Options;
import ghidra.framework.options.ToolOptions;
import ghidra.program.model.data.DataType;
import ghidra.program.model.data.Structure;
import ghidra.program.model.listing.*;
import ghidra.program.util.BytesFieldLocation;
import ghidra.program.util.ProgramLocation;
import ghidra.util.HelpLocation;

/**
  *  Generates Bytes Fields.
  */
public class BytesFieldFactory extends FieldFactory {
	private static final int CHARS_IN_BYTE = 2;
	public static final String FIELD_NAME = "Bytes";
	public final static String GROUP_TITLE = "Bytes Field";
	public final static String MAX_DISPLAY_LINES_MSG =
		GROUP_TITLE + Options.DELIMITER + "Maximum Lines To Display";
	public final static String DELIMITER_MSG = GROUP_TITLE + Options.DELIMITER + "Delimiter";
	public final static String BYTE_GROUP_SIZE_MSG =
		GROUP_TITLE + Options.DELIMITER + "Byte Group Size";
	public final static String DISPLAY_UCASE_MSG =
		GROUP_TITLE + Options.DELIMITER + "Display in Upper Case";
	public final static String REVERSE_INSTRUCTION_BYTE_ORDERING =
		GROUP_TITLE + Options.DELIMITER + "Reverse Instruction Byte Ordering";
	public final static String DISPLAY_STRUCTURE_ALIGNMENT_BYTES_MSG =
		GROUP_TITLE + Options.DELIMITER + "Display Structure Alignment Bytes";

	private String delim = " ";
	private int maxDisplayLines;
	/** number of bytes (not chars) displayed without whitespace; usually 1*/
	private int byteGroupSize;
	private boolean displayUpperCase;
	private boolean reverseInstByteOrdering;
	private boolean displayStructureAlignmentBytes;

	/**
	 * Default Constructor
	 */
	public BytesFieldFactory() {
		super(FIELD_NAME);
	}

	/**
	 * Constructor
	 * @param model the model that the field belongs to.
	 * @param hlProvider the HightLightStringProvider.
	 * @param displayOptions the Options for display properties.
	 * @param fieldOptions the Options for field specific properties.
	 */
	private BytesFieldFactory(FieldFormatModel model, ListingHighlightProvider hlProvider,
			Options displayOptions, Options fieldOptions) {
		super(FIELD_NAME, model, hlProvider, displayOptions, fieldOptions);

		HelpLocation hl = new HelpLocation("CodeBrowserPlugin", "Bytes_Field");
		fieldOptions.getOptions(GROUP_TITLE).setOptionsHelpLocation(hl);

		fieldOptions.registerOption(DELIMITER_MSG, " ", hl,
			"String used to separate groups of bytes in the bytes field.");
		fieldOptions.registerOption(MAX_DISPLAY_LINES_MSG, 3, hl,
			"The maximum number of lines used to display bytes.");
		fieldOptions.registerOption(BYTE_GROUP_SIZE_MSG, 1, hl,
			"The number of bytes to group together without delimeters in the bytes field.");
		fieldOptions.registerOption(DISPLAY_UCASE_MSG, false, hl,
			"Displays the hex digits in upper case in the bytes field");
		fieldOptions.registerOption(REVERSE_INSTRUCTION_BYTE_ORDERING, false, hl,
			"Reverses the normal order of the bytes in the bytes field." +
				"  Only used for instructions in Little Endian format");
		fieldOptions.registerOption(DISPLAY_STRUCTURE_ALIGNMENT_BYTES_MSG, true, hl,
			"Display trailing alignment bytes in structures.");

		delim = fieldOptions.getString(DELIMITER_MSG, " ");
		maxDisplayLines = fieldOptions.getInt(MAX_DISPLAY_LINES_MSG, 3);
		byteGroupSize = fieldOptions.getInt(BYTE_GROUP_SIZE_MSG, 1);
		displayUpperCase = fieldOptions.getBoolean(DISPLAY_UCASE_MSG, false);
		reverseInstByteOrdering = fieldOptions.getBoolean(REVERSE_INSTRUCTION_BYTE_ORDERING, false);
		displayStructureAlignmentBytes =
			fieldOptions.getBoolean(DISPLAY_STRUCTURE_ALIGNMENT_BYTES_MSG, false);
	}

	@Override
	public void fieldOptionsChanged(Options options, String optionName, Object oldValue,
			Object newValue) {

		if (optionName.equals(MAX_DISPLAY_LINES_MSG)) {
			setDisplayLines(((Integer) newValue).intValue(), options);
			model.update();
		}
		else if (optionName.equals(DELIMITER_MSG)) {
			setDelim((String) newValue, options);
			model.update();
		}
		else if (optionName.equals(BYTE_GROUP_SIZE_MSG)) {
			setGroupSize(((Integer) newValue).intValue(), options);
			model.update();
		}
		else if (optionName.equals(DISPLAY_UCASE_MSG)) {
			displayUpperCase = ((Boolean) newValue).booleanValue();
			model.update();
		}
		else if (optionName.equals(REVERSE_INSTRUCTION_BYTE_ORDERING)) {
			reverseInstByteOrdering = ((Boolean) newValue).booleanValue();
			model.update();
		}
		else if (optionName.equals(DISPLAY_STRUCTURE_ALIGNMENT_BYTES_MSG)) {
			displayStructureAlignmentBytes = ((Boolean) newValue).booleanValue();
			model.update();
		}
	}

	private void setGroupSize(int n, Options options) {
		if (n < 1) {
			n = 1;
			options.setInt(BYTE_GROUP_SIZE_MSG, 1);
		}
		byteGroupSize = n;
	}

	private void setDisplayLines(int n, Options options) {
		if (n < 1) {
			n = 1;
			options.setInt(MAX_DISPLAY_LINES_MSG, 1);
		}
		maxDisplayLines = n;
	}

	private void setDelim(String s, Options options) {
		if (s == null) {
			s = " ";
			options.setString(DELIMITER_MSG, s);
		}
		delim = s;
	}

	@Override
	public ListingField getField(ProxyObj<?> proxy, int varWidth) {
		if (!enabled || !(proxy.getObject() instanceof CodeUnit cu)) {
			return null;
		}

		// Instructions: use prototype length so we display all bytes for length-override case
		// Consider all parsed bytes even if the overlap the next instruction due to the
		// use of an instruction length-override.
		final int arrLength =
			cu instanceof Instruction ins ? ins.getParsedLength() : Math.min(cu.getLength(), 100);

		byte[] bytes = new byte[arrLength];
		final int length = cu.getBytes(bytes, 0);

		if (length == 0) {
			return null;
		}

		if ((cu instanceof Instruction) && reverseInstByteOrdering && !cu.isBigEndian()) {
			ArrayUtils.reverse(bytes);
		}

		int groupLength = length / byteGroupSize;
		int residual = length % byteGroupSize;
		if (residual != 0) {
			groupLength++;
		}

		boolean wasTruncated = length != cu.getLength();
		byte[] alignmentBytes = getAlignmentBytes(cu, wasTruncated);
		int extraLength = getLengthForAlignmentBytes(alignmentBytes, residual);
		FieldElement[] elements = new FieldElement[groupLength + extraLength];
		boolean addDelimiter = extraLength != 0;
		buildAttributedByteValues(elements, 0, bytes, length, 0, ListingColors.BYTES, addDelimiter);
		if (addDelimiter) {
			buildAttributedByteValues(elements, groupLength, alignmentBytes, alignmentBytes.length,
				residual, ListingColors.BYTES_ALIGNMENT, false);
		}

		return ListingTextField.createPackedTextField(this, proxy, elements, startX + varWidth,
			width, maxDisplayLines, hlProvider);
	}

	private int getLengthForAlignmentBytes(byte[] alignmentBytes, int residual) {
		if (alignmentBytes == null) {
			return 0;
		}
		int firstGroup = byteGroupSize - residual;
		int extraBytes = alignmentBytes.length - firstGroup;
		if (extraBytes < 0) {
			return 1;
		}
		int alignmentLength = (extraBytes / byteGroupSize) + 1;
		if (extraBytes % byteGroupSize != 0) {
			alignmentLength++;
		}
		return alignmentLength;
	}

	private byte[] getAlignmentBytes(CodeUnit cu, boolean wasTruncated) {
		if ((cu instanceof Data) && displayStructureAlignmentBytes && !wasTruncated) {
			return getStructureComponentAlignmentBytes((Data) cu);
		}
		return null;
	}

	private int buildAttributedByteValues(FieldElement[] elements, int pos, byte[] bytes, int size,
			int residual, Color color, boolean addDelimiter) {
		StringBuilder buffer = new StringBuilder();
		int groupSize = byteGroupSize - residual;
		int tempGroupSize = 0;
		for (int i = 0; i < size; ++i) {
			if (bytes[i] >= 0x00 && bytes[i] <= 0x0F) {
				buffer.append("0");
			}
			String bStr = Integer.toHexString(bytes[i] & 0x000000FF);
			if (bStr.length() > 2) {
				bStr = bStr.substring(bStr.length() - 2);
			}
			if (displayUpperCase) {
				bStr = bStr.toUpperCase();
			}
			buffer.append(bStr);
			++tempGroupSize;
			if (tempGroupSize == groupSize) {
				tempGroupSize = 0;
				groupSize = byteGroupSize;
				if (i < size - 1 || addDelimiter) {
					buffer.append(delim);
				}
				AttributedString as = new AttributedString(buffer.toString(), color, getMetrics());
				elements[pos] = new TextFieldElement(as, pos, 0);
				pos++;
				buffer = new StringBuilder();
			}
		}

		// append incomplete byte group...
		if (tempGroupSize > 0) {
			AttributedString as = new AttributedString(buffer.toString(), color, getMetrics());
			elements[pos] = new TextFieldElement(as, pos, 0);
		}
		return tempGroupSize;
	}

	private byte[] getStructureComponentAlignmentBytes(Data data) {

		Data parent = data.getParent();
		if (parent == null) {
			return null;
		}
		DataType baseDataType = parent.getBaseDataType();
		if (!(baseDataType instanceof Structure)) {
			return null; // e.g., union
		}
		Structure struct = (Structure) baseDataType;
		if (!struct.isPackingEnabled()) {
			return null;
		}

		int alignSize = 0;
		int ordinal = data.getComponentIndex();
		if (ordinal == (struct.getNumComponents() - 1)) {
			alignSize = (int) (parent.getMaxAddress().subtract(data.getMaxAddress()));
		}
		else {
			Data nextComponent = parent.getComponent(ordinal + 1);
			if (nextComponent == null) {
				return null; // this should never happen
			}
			alignSize = (int) (nextComponent.getMinAddress().subtract(data.getMaxAddress())) - 1;
		}
		if (alignSize <= 0) {
			return null;
		}

		int alignmentOffset = data.getParentOffset() + data.getLength();
		byte[] bytes = new byte[alignSize];
		parent.getBytes(bytes, alignmentOffset);
		return bytes;
	}

	@Override
	public ProgramLocation getProgramLocation(int row, int col, ListingField bf) {
		Object obj = bf.getProxy().getObject();
		if (!(obj instanceof CodeUnit) || row < 0 || col < 0) {
			return null;
		}

		CodeUnit cu = (CodeUnit) obj;
		int[] cpath = null;
		if (cu instanceof Data) {
			cpath = ((Data) cu).getComponentPath();
		}

		ListingTextField btf = (ListingTextField) bf;
		RowColLocation fieldLoc = btf.screenToDataLocation(row, col);
		int tokenIndex = fieldLoc.row();
		int tokenCharPos = fieldLoc.col();

		// compute tokens associated with code unit bytes (excluding trailing alignment bytes)
		int size = cu.getLength();
		int len = size / byteGroupSize;
		int residual = size % byteGroupSize;
		if (residual != 0) {
			len++;
		}

		// compensate for split group containing optional alignment bytes
		if (tokenIndex >= len && residual != 0) {
			if (tokenIndex == len) {
				tokenCharPos += residual * CHARS_IN_BYTE;
			}
			--tokenIndex;
		}

		int byteIndex = tokenIndex * byteGroupSize + getByteIndexInToken(tokenCharPos);
		int charOffset = computeCharOffset(tokenCharPos);
		return new BytesFieldLocation(cu.getProgram(), cu.getMinAddress(),
			cu.getMinAddress().add(byteIndex), cpath, charOffset);
	}

	/**
	 * Computes how many bytes the given column position represents. Normally this is just the
	 * column position / 2 (since each byte consists of two chars).  There is a special case when
	 * the col position is just past the last char of the token.  In this case, we want to return
	 * the number of bytes in a token - 1;
	 */
	private int getByteIndexInToken(int col) {
		if (col >= byteGroupSize * CHARS_IN_BYTE) {
			return byteGroupSize - 1;
		}
		return col / CHARS_IN_BYTE;
	}

	/**
	 * Computes the character offset for a BytesFieldLocation based on the character column the
	 * cursor is at in the token.  BytesFieldLocation character offsets are always as if the group
	 * size is 1. So for all positions except the last byte, it is just the column modulo 2.  For
	 * the last byte, we have to account for any columns past the last char.  In this case, we have
	 * to subtract off 2 for every byte before the last byte.
	 */
	private int computeCharOffset(int col) {
		if (col >= byteGroupSize * CHARS_IN_BYTE) {
			return col - ((byteGroupSize - 1) * CHARS_IN_BYTE);
		}
		return col % CHARS_IN_BYTE;
	}

	@Override
	public FieldLocation getFieldLocation(ListingField bf, BigInteger index, int fieldNum,
			ProgramLocation loc) {

		if (!(loc instanceof BytesFieldLocation)) {
			return null;
		}
		Object obj = bf.getProxy().getObject();
		if (!(obj instanceof CodeUnit)) {
			return null;
		}

		CodeUnit cu = (CodeUnit) obj;
		BytesFieldLocation bytesLoc = (BytesFieldLocation) loc;
		int byteIndex = bytesLoc.getByteIndex();
		int columnInByte = bytesLoc.getColumnInByte();

		int size = cu.getLength();
		int residual = size % byteGroupSize;

		if (!displayStructureAlignmentBytes && byteIndex >= size) {
			byteIndex = size - 1;
			columnInByte = 2;
		}

		int tokenIndex = byteIndex / byteGroupSize;
		int tokenOffset = (byteIndex % byteGroupSize) * 2 + columnInByte;

		// compensate for split group containing optional alignment bytes
		if (byteIndex >= size && residual != 0) {
			if ((byteIndex - size) < (byteGroupSize - residual)) {
				tokenOffset -= (residual * CHARS_IN_BYTE);
			}
			++tokenIndex;
		}

		ListingTextField btf = (ListingTextField) bf;
		RowColLocation rcl = btf.dataToScreenLocation(tokenIndex, tokenOffset);
		if (hasSamePath(bf, loc)) {
			return new FieldLocation(index, fieldNum, rcl.row(), rcl.col());
		}
		return null;
	}

	@Override
	public boolean acceptsType(int category, Class<?> proxyObjectClass) {
		if (!CodeUnit.class.isAssignableFrom(proxyObjectClass)) {
			return false;
		}
		return (category == FieldFormatModel.INSTRUCTION_OR_DATA ||
			category == FieldFormatModel.OPEN_DATA);
	}

	@Override
	public FieldFactory newInstance(FieldFormatModel formatModel, ListingHighlightProvider provider,
			ToolOptions displayOptions, ToolOptions fieldOptions) {
		return new BytesFieldFactory(formatModel, provider, displayOptions, fieldOptions);
	}

}
