////////////////////////////////////////
// Programming project code
// UniBw M, 2022, 2023, 2024
// www.unibw.de/inf2
// (c) Mark Minas (mark.minas@unibw.de)
////////////////////////////////////////

package pp.monopoly.model;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;

import pp.monopoly.notification.GameEvent;
import pp.monopoly.notification.GameEventBroker;
import pp.monopoly.notification.ItemAddedEvent;
import pp.monopoly.notification.ItemRemovedEvent;

/**
 * Represents a rectangular map that holds figures and registers houses, hotels
 * It also supports event notification for game state changes such as item addition or removal.
 * Valid positions on this map have x-coordinates in the range of 0 to width-1 and y-coordinates
 * in the range of 0 to height-1.
 *
 * @see #getWidth()
 * @see #getHeight()
 */
public class Board {
    /**
     * A list of items (figure, house, etc.) placed on the map.
     */
    private final List<Item> items = new ArrayList<>();

    /**
     * The broker responsible for notifying registered listeners of events
     * (such as when an item is added or removed from the map).
     * Can be null, in which case no notifications will be sent.
     */
    private final GameEventBroker eventBroker;

    private final int width;
    private final int height;

    /**
     * Constructs an empty map with the given dimensions. The specified event broker
     * will handle the notification of changes in the map state, such as adding or removing items.
     * Passing null as the event broker is allowed, but in that case, no notifications will occur.
     *
     * @param width       the number of columns (width) of the map
     * @param height      the number of rows (height) of the map
     * @param eventBroker the event broker used for notifying listeners, or null if event distribution is not needed
     */
    public Board(int width, int height, GameEventBroker eventBroker) {
        if (width < 1 || height < 1)
            throw new IllegalArgumentException("Invalid map size");
        this.width = width;
        this.height = height;
        this.eventBroker = eventBroker;
    }

    /**
     * Adds an item (e.g., a figure or a house) to the map and triggers the appropriate event.
     *
     * @param item the item to be added to the map
     */
    private void addItem(Item item) {
        items.add(item);
        notifyListeners((GameEvent) new ItemAddedEvent(item, null));
    }

    /**
     * Removes an item from the map and triggers an item removal event.
     *
     * @param item the item to be removed from the map
     */
    public void remove(Item item) {
        items.remove(item);
        notifyListeners((GameEvent) new ItemRemovedEvent(item, null)); // Falls es ein entsprechendes ItemRemovedEvent gibt
    }

    /**
     * Removes all items from the map and triggers corresponding removal events for each.
     */
    public void clear() {
        new ArrayList<>(items).forEach(this::remove);
    }

    /**
     * Returns a stream of items of a specified type (class).
     *
     * @param clazz the class type to filter items by
     * @param <T>   the type of items to return
     * @return a stream of items matching the specified class type
     */
    private <T extends Item> Stream<T> getItems(Class<T> clazz) {
        return items.stream().filter(clazz::isInstance).map(clazz::cast);
    }

    /**
     * Returns an unmodifiable list of all items currently on the map.
     *
     * @return an unmodifiable list of all items
     */
    public List<Item> getItems() {
        return Collections.unmodifiableList(items);
    }

    /**
     * Returns the width (number of columns) of the map.
     *
     * @return the width of the map
     */
    public int getWidth() {
        return width;
    }

    /**
     * Returns the height (number of rows) of the map.
     *
     * @return the height of the map
     */
    public int getHeight() {
        return height;
    }

    /**
     * Validates whether the specified position is within the map boundaries.
     *
     * @param pos the position to validate
     * @return true if the position is within the map, false otherwise
     */
    public boolean isValid(IntPosition pos) {
        return isValid(pos.getX(), pos.getY());
    }

    /**
     * Checks if the specified coordinates are within the map boundaries.
     *
     * @param x the x-coordinate to validate
     * @param y the y-coordinate to validate
     * @return true if the coordinates are valid, false otherwise
     */
    public boolean isValid(int x, int y) {
        return x >= 0 && x < width &&
               y >= 0 && y < height;
    }

    /**
     * Returns a string representation of the map.
     *
     * @return a string representation of the map
     */
    @Override
    public String toString() {
        return "Board:{" + items + '}'; //NON-NLS
    }

    /**
     * Notifies all registered listeners about a game event if the event broker is available.
     *
     * @param event the event to be distributed to listeners
     */
    private void notifyListeners(GameEvent event) {
        if (eventBroker != null)
            eventBroker.notifyListeners(event);
    }
}
