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

package pp.battleship.model;

import pp.battleship.notification.GameEvent;
import pp.battleship.notification.GameEventBroker;
import pp.battleship.notification.ItemAddedEvent;

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

/**
 * Represents a rectangular map that holds ships and registers shots fired.
 * 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 ShipMap {
    /**
     * A list of items (ships, shots, 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 ShipMap(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 ship or a shot) 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(new ItemAddedEvent(item, this));
    }

    /**
     * Adds a battleship to the map and triggers an item addition event.
     *
     * @param ship the battleship to be added to the map
     */
    public void add(Battleship ship) {
        addItem(ship);
    }

    /**
     * Registers a shot on the map, updates the state of the affected ship (if any),
     * and triggers an item addition event.
     *
     * @param shot the shot to be registered on the map
     */
    public void add(Shot shot) {
        final Battleship ship = findShipAt(shot);
        if (ship != null)
            ship.hit(shot);
        addItem(shot);
    }

    /**
     * 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(new ItemAddedEvent(item, this));
    }

    /**
     * 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 a stream of all battleships currently on the map.
     *
     * @return a stream of battleships
     */
    public Stream<Battleship> getShips() {
        return getItems(Battleship.class);
    }

    /**
     * Returns a list of all remaining battleships that have not been destroyed.
     *
     * @return a list of remaining battleships
     */
    public List<Battleship> getRemainingShips() {
        return getShips().filter(s -> !s.isDestroyed()).toList();
    }

    /**
     * Returns a stream of all shots fired on the map.
     *
     * @return a stream of shots
     */
    public Stream<Shot> getShots() {
        return getItems(Shot.class);
    }

    /**
     * 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;
    }

    /**
     * Checks if the given ship is in a valid position (within the map bounds and non-colliding with other ships).
     *
     * @param ship the battleship to validate
     * @return true if the ship's position is valid, false otherwise
     */
    public boolean isValid(Battleship ship) {
        return isValid(ship.getMinX(), ship.getMinY()) &&
               isValid(ship.getMaxX(), ship.getMaxY()) &&
               getShips().filter(s -> s != ship).noneMatch(ship::collidesWith);
    }

    /**
     * Finds a battleship at the specified coordinates.
     *
     * @param x the x-coordinate of the position
     * @param y the y-coordinate of the position
     * @return the ship at the specified coordinates, or null if none is found
     */
    public Battleship findShipAt(int x, int y) {
        return getShips().filter(ship -> ship.contains(x, y))
                         .findAny()
                         .orElse(null);
    }

    /**
     * Finds a battleship at the specified position. This is a convenience method.
     *
     * @param position the position within the map
     * @return the ship at the specified position, or null if none is found
     */
    public Battleship findShipAt(IntPosition position) {
        return findShipAt(position.getX(), position.getY());
    }

    /**
     * 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 ship map.
     *
     * @return a string representation of the ship map
     */
    @Override
    public String toString() {
        return "ShipMap{" + 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);
    }
}
