/*
 * Copyright 2019-2024 MicroEJ Corp. All rights reserved.
 * This library is provided in source code for use, modification and test, subject to license terms.
 * Any modification of the source code will break MicroEJ Corp. warranties on the whole library.
 */
package ej.mwt.style;

import ej.annotation.Nullable;
import ej.microui.display.Font;
import ej.mwt.style.background.Background;
import ej.mwt.style.dimension.Dimension;
import ej.mwt.style.outline.Outline;
import ej.mwt.util.Alignment;

/**
 * An editable style is a simple implementation of a style. It allows to set all the attributes of a style.
 */
public class EditableStyle implements Style {

	/**
	 * The maximum number of extra fields.
	 */
	public static final int MAX_EXTRA_FIELDS = 7;

	private static final Object[] EMPTY_EXTRA_FIELDS = new Object[0];

	private Dimension dimension;
	private byte horizontalAlignment;
	private byte verticalAlignment;
	private Outline margin;
	private Outline border;
	private Outline padding;
	private Background background;
	private int color;
	private Font font;
	private Object[] extraFields;

	/**
	 * Creates an editable style from the default style.
	 */
	public EditableStyle() {
		this.dimension = DefaultStyle.DIMENSION;
		this.horizontalAlignment = DefaultStyle.HORIZONTAL_ALIGNMENT;
		this.verticalAlignment = DefaultStyle.VERTICAL_ALIGNMENT;
		this.margin = DefaultStyle.MARGIN;
		this.border = DefaultStyle.BORDER;
		this.padding = DefaultStyle.PADDING;
		this.background = DefaultStyle.BACKGROUND;
		this.color = DefaultStyle.COLOR;
		this.font = Font.getDefaultFont();
		this.extraFields = EMPTY_EXTRA_FIELDS;
	}

	/**
	 * Creates an editable style from an other one.
	 *
	 * @param style
	 *            the editable style to copy.
	 */
	public EditableStyle(EditableStyle style) {
		this.dimension = style.dimension;
		this.horizontalAlignment = style.horizontalAlignment;
		this.verticalAlignment = style.verticalAlignment;
		this.margin = style.margin;
		this.border = style.border;
		this.padding = style.padding;
		this.background = style.background;
		this.color = style.color;
		this.font = style.font;
		this.extraFields = (style.extraFields == EMPTY_EXTRA_FIELDS ? EMPTY_EXTRA_FIELDS : style.extraFields.clone());
	}

	@Override
	public Dimension getDimension() {
		return this.dimension;
	}

	@Override
	public int getHorizontalAlignment() {
		return this.horizontalAlignment;
	}

	@Override
	public int getVerticalAlignment() {
		return this.verticalAlignment;
	}

	@Override
	public Outline getMargin() {
		return this.margin;
	}

	@Override
	public Outline getBorder() {
		return this.border;
	}

	@Override
	public Outline getPadding() {
		return this.padding;
	}

	@Override
	public Background getBackground() {
		return this.background;
	}

	@Override
	public int getColor() {
		return this.color;
	}

	@Override
	public Font getFont() {
		return this.font;
	}

	@Override
	public <T> T getExtraObject(int fieldId, Class<T> clazz, T defaultValue) {
		validateExtraFieldId(fieldId);

		Object[] extraFields = this.extraFields;
		if (fieldId < extraFields.length) {
			Object value = extraFields[fieldId];
			if (value != null) {
				T castedValue = clazz.cast(value);
				assert (castedValue != null);
				return castedValue;
			}
		}

		return defaultValue;
	}

	@Override
	public int getExtraInt(int fieldId, int defaultValue) {
		validateExtraFieldId(fieldId);

		Object[] extraFields = this.extraFields;
		if (fieldId < extraFields.length) {
			Object value = extraFields[fieldId];
			if (value != null) {
				return ((Integer) value).intValue();
			}
		}

		return defaultValue;
	}

	@Override
	public float getExtraFloat(int fieldId, float defaultValue) {
		validateExtraFieldId(fieldId);

		Object[] extraFields = this.extraFields;
		if (fieldId < extraFields.length) {
			Object value = extraFields[fieldId];
			if (value != null) {
				return ((Float) value).floatValue();
			}
		}

		return defaultValue;
	}

	/**
	 * Sets the dimension.
	 *
	 * @param dimension
	 *            the dimension.
	 */
	public void setDimension(Dimension dimension) {
		this.dimension = dimension;
	}

	/**
	 * Sets the horizontal alignment.
	 *
	 * The horizontal alignment must be equal to one of the horizontal constants ({@link Alignment#LEFT},
	 * {@link Alignment#HCENTER} or {@link Alignment#RIGHT}).
	 *
	 * @param horizontalAlignment
	 *            the horizontal alignment.
	 * @throws IllegalArgumentException
	 *             if the horizontal alignment is not valid.
	 * @see Alignment#validateHorizontalAlignment(int)
	 */
	public void setHorizontalAlignment(int horizontalAlignment) {
		Alignment.validateHorizontalAlignment(horizontalAlignment);
		this.horizontalAlignment = (byte) horizontalAlignment;
	}

	/**
	 * Sets the vertical alignment.
	 *
	 * The vertical alignment must be equal to one of the vertical constants ({@link Alignment#TOP},
	 * {@link Alignment#VCENTER} or {@link Alignment#BOTTOM}).
	 *
	 * @param verticalAlignment
	 *            the vertical alignment.
	 * @throws IllegalArgumentException
	 *             if the vertical alignment is not valid.
	 * @see Alignment#validateVerticalAlignment(int)
	 */
	public void setVerticalAlignment(int verticalAlignment) {
		Alignment.validateVerticalAlignment(verticalAlignment);
		this.verticalAlignment = (byte) verticalAlignment;
	}

	/**
	 * Sets the margin.
	 *
	 * @param margin
	 *            the margin.
	 */
	public void setMargin(Outline margin) {
		this.margin = margin;
	}

	/**
	 * Sets the border.
	 *
	 * @param border
	 *            the border.
	 */
	public void setBorder(Outline border) {
		this.border = border;
	}

	/**
	 * Sets the padding.
	 *
	 * @param padding
	 *            the padding.
	 */
	public void setPadding(Outline padding) {
		this.padding = padding;
	}

	/**
	 * Sets the background.
	 *
	 * @param background
	 *            the background.
	 */
	public void setBackground(Background background) {
		this.background = background;
	}

	/**
	 * Sets the color.
	 *
	 * @param color
	 *            the color.
	 */
	public void setColor(int color) {
		this.color = color;
	}

	/**
	 * Sets the font.
	 *
	 * @param font
	 *            the font.
	 */
	public void setFont(Font font) {
		this.font = font;
	}

	/**
	 * Sets an extra field to the given object value.
	 *
	 * @param fieldId
	 *            the ID of the extra field.
	 * @param fieldValue
	 *            the value of the extra field.
	 * @throws IllegalArgumentException
	 *             if the given ID is not within the [0, 6] interval.
	 */
	public void setExtraObject(int fieldId, Object fieldValue) {
		validateExtraFieldId(fieldId);
		this.extraFields = setExtraField(this.extraFields, fieldId, fieldValue);
	}

	/**
	 * Sets an extra field to the given int value.
	 *
	 * @param fieldId
	 *            the ID of the extra field.
	 * @param fieldValue
	 *            the value of the extra field.
	 * @throws IllegalArgumentException
	 *             if the given ID is not within the [0, 6] interval.
	 */
	public void setExtraInt(int fieldId, int fieldValue) {
		setExtraObject(fieldId, Integer.valueOf(fieldValue));
	}

	/**
	 * Sets an extra field to the given float value.
	 *
	 * @param fieldId
	 *            the ID of the extra field.
	 * @param fieldValue
	 *            the value of the extra field.
	 * @throws IllegalArgumentException
	 *             if the given ID is not within the [0, 6] interval.
	 */
	public void setExtraFloat(int fieldId, float fieldValue) {
		setExtraObject(fieldId, Float.valueOf(fieldValue));
	}

	/**
	 * Validates that the extra field ID is correct.
	 *
	 * @param fieldId
	 *            the ID to validate.
	 * @throws IllegalArgumentException
	 *             if the given ID is not within the [0, 6] interval.
	 */
	protected static void validateExtraFieldId(int fieldId) {
		if (fieldId < 0 || fieldId >= MAX_EXTRA_FIELDS) {
			throw new IllegalArgumentException();
		}
	}

	private static Object[] setExtraField(Object[] extraFields, int fieldId, Object fieldValue) {
		if (fieldId >= extraFields.length) {
			Object[] newExtraFields = new Object[fieldId + 1];
			System.arraycopy(extraFields, 0, newExtraFields, 0, extraFields.length);
			extraFields = newExtraFields;
		}
		extraFields[fieldId] = fieldValue;
		return extraFields;
	}

	private static boolean identityEquals(Object[] array1, Object[] array2) {
		if (array1.length != array2.length) {
			return false;
		}
		for (int i = 0; i < array1.length; i++) {
			if (array1[i] != array2[i]) {
				return false;
			}
		}
		return true;
	}

	@Override
	public boolean equals(@Nullable Object obj) {
		if (obj == this) {
			return true;
		} else if (!(obj instanceof EditableStyle)) {
			return false;
		} else {
			EditableStyle other = (EditableStyle) obj;
			return this.dimension.equals(other.dimension) && this.horizontalAlignment == other.horizontalAlignment
					&& this.verticalAlignment == other.verticalAlignment && this.margin.equals(other.margin)
					&& this.border.equals(other.border) && this.padding.equals(other.padding)
					&& this.background.equals(other.background) && this.color == other.color && this.font == other.font
					&& identityEquals(this.extraFields, other.extraFields);
		}
	}

	@Override
	public int hashCode() {
		// don't call this.font.hashCode() to avoid embedding the hashCode() method of every class (M0081MEJA-1240)
		return this.dimension.hashCode() - this.horizontalAlignment + this.verticalAlignment - this.margin.hashCode()
				+ this.border.hashCode() - this.padding.hashCode() + this.background.hashCode() - this.color
				+ this.extraFields.length;
	}
}
