package cz.jzitnik.game.ui;

import cz.jzitnik.game.Game;
import cz.jzitnik.game.entities.items.InventoryItem;
import cz.jzitnik.game.entities.items.Item;
import cz.jzitnik.tui.utils.SpriteCombiner;
import cz.jzitnik.tui.SpriteList;
import cz.jzitnik.tui.utils.Numbers;
import lombok.Getter;
import lombok.Setter;
import org.jline.terminal.Terminal;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

/**
 * Represents a player's inventory including hotbar, crafting table, and item management.
 */
@Getter
public class Inventory {
    public static final int INVENTORY_SIZE_PX = 470;
    public static final int COLUMN_AMOUNT = 5;
    public static final int ROW_AMOUNT = 4;

    private final InventoryItem[] items = new InventoryItem[20];
    private final InventoryItem[] hotbar = new InventoryItem[9];

    private transient SmallCraftingTable smallCraftingTable = new SmallCraftingTable(this);

    @Setter
    private int itemInhHandIndex = 0;
    @Setter
    private int selectedItemInv = -1;
    @Setter
    private boolean rightClick = false;

    /**
     * Returns the currently held item in the hotbar's selected hand index.
     *
     * @return Optional containing the held Item if present, otherwise empty.
     */
    public Optional<Item> getItemInHand() {
        if (hotbar[itemInhHandIndex] == null) {
            return Optional.empty();
        }
        return Optional.of(hotbar[itemInhHandIndex].getItem().getLast());
    }

    /**
     * Decreases the amount of the item in hand by one, removing it if the amount reaches zero.
     */
    public void decreaseItemInHand() {
        if (hotbar[itemInhHandIndex] == null) {
            return;
        }

        if (hotbar[itemInhHandIndex].getAmount() == 1) {
            hotbar[itemInhHandIndex] = null;
            return;
        }

        hotbar[itemInhHandIndex].decrease();
    }


    /**
     * Attempts to place a new item into the inventory or hotbar.
     * If inventory and hotbar are full, the item is lost.
     *
     * @param item The item to place.
     */
    private void placeItem(Item item) {
        var inventoryItem = new InventoryItem(item);

        // Try to place to hotbar
        for (int i = 0; i < hotbar.length; i++) {
            if (hotbar[i] == null) {
                hotbar[i] = inventoryItem;
                return;
            }
        }

        // Try to place to inventory
        for (int i = 0; i < items.length; i++) {
            if (items[i] == null) {
                items[i] = inventoryItem;
                break;
            }
        }

        // If inventory is full the item is lost
    }

    /**
     * Adds an InventoryItem's amount of items into the inventory.
     *
     * @param item InventoryItem to add.
     */
    public void addItem(InventoryItem item) {
        for (int i = 0; i < item.getAmount(); i++) {
            addItem(item.getItem().get(i));
        }
    }

    /**
     * Adds a list of Items to the inventory.
     *
     * @param item List of Items to add.
     */
    public void addItem(List<Item> item) {
        for (Item i : item) {
            addItem(i);
        }
    }

    /**
     * Adds a single Item to the inventory, stacking if possible.
     *
     * @param item Item to add.
     */
    public void addItem(Item item) {
        if (!item.isStackable()) {
            placeItem(item);
            return;
        }

        // Try to stack in hotbar
        for (InventoryItem value : hotbar) {
            if (value != null && value.getItem().getFirst().equals(item) && value.getAmount() < item.getStackAmount()) {
                value.add(item);
                return;
            }
        }

        // Try to stack in inventory
        for (InventoryItem inventoryItem : items) {
            if (inventoryItem != null && inventoryItem.getItem().getFirst().equals(item)
                    && inventoryItem.getAmount() < item.getStackAmount()) {
                inventoryItem.add(item);
                return;
            }
        }

        // If stacking didn't work try to add it to inventory
        placeItem(item);
    }

    /**
     * Returns a string representing the background for the hotbar slots.
     *
     * @return Background sprite string.
     */
    private String getHotbarBackground() {
        StringBuilder sprite = new StringBuilder();
        for (int i = 0; i < 25; i++) {
            sprite.append("\033[38;5;245;48;5;245m▓".repeat(50));
            sprite.append("\n");
        }
        return sprite.toString();
    }

    /**
     * Renders the hotbar UI onto the given buffer.
     *
     * @param buffer     StringBuilder buffer to append rendering output.
     * @param spriteList SpriteList used for rendering item sprites.
     * @param terminal   Terminal object for screen dimensions.
     * @param isFull     Whether full inventory mode is active.
     * @param game       Current game instance.
     */
    public void renderHotbar(StringBuilder buffer, SpriteList spriteList, Terminal terminal, boolean isFull, Game game) {
        int termWidth = terminal.getWidth();
        int startLeft = (termWidth / 2) - (INVENTORY_SIZE_PX / 2);

        List<String> sprites = getSprites(hotbar, spriteList, (isFull ? selectedItemInv - 20 : itemInhHandIndex), game);

        for (int i = 0; i < 26; i++) {
            // Empty left space
            buffer.append("\033[0m ".repeat(Math.max(0, startLeft)));

            if (i == 0 || i == 25) {
                buffer.append("\033[38;5;231;48;5;231m▓".repeat(INVENTORY_SIZE_PX - 2));
            } else {
                for (int j = 0; j < 9; j++) {
                    buffer.append("\033[38;5;231;48;5;231m▓".repeat(2));
                    if (sprites.get(j) == null) {
                        buffer.append("\033[0m ".repeat(50));
                        continue;
                    }

                    String x = sprites.get(j).split("\n")[i];
                    buffer.append(x);
                }
            }
            buffer.append("\033[38;5;231;48;5;231m▓".repeat(2));
            buffer.append("\033[0m\n");
        }
    }

    /**
     * Renders the full inventory UI including items and crafting table.
     *
     * @param buffer          StringBuilder buffer to append rendering output.
     * @param terminal        Terminal object for screen dimensions.
     * @param spriteList      SpriteList used for rendering item sprites.
     * @param includeCrafting Whether to include the crafting UI.
     * @param moveTopCustom   Optional vertical offset.
     * @param game            Current game instance.
     */
    public void renderFull(StringBuilder buffer, Terminal terminal, SpriteList spriteList, boolean includeCrafting,
                           Optional<Integer> moveTopCustom, Game game) {
        int widthPixels = COLUMN_AMOUNT * (50 + 4) + 2;
        int heightPixels = ROW_AMOUNT * (25 + 1);

        int moveLeft = Math.max(0, (terminal.getWidth() / 2) - (widthPixels / 2));
        int moveTop = moveTopCustom.orElse((terminal.getHeight() / 2) - (heightPixels / 2));

        List<String> sprites = getSprites(items, spriteList, selectedItemInv, game);

        // Top center
        buffer.append("\n".repeat(Math.max(0, moveTopCustom.isPresent() ? 0 : moveTop)));

        buffer.append("\033[0m ".repeat(moveLeft));
        buffer.append("\033[38;5;231;48;5;231m▓".repeat(widthPixels)).append("\033[0m\n");

        String[] craftingTable = smallCraftingTable.render(spriteList, game).toString().split("\n");
        int spacesFromTop = (ROW_AMOUNT - SmallCraftingTable.ROW_AMOUNT) / 2;

        for (int i = 0; i < ROW_AMOUNT; i++) {
            for (int j = 0; j < 26; j++) {
                buffer.append("\033[0m ".repeat(moveLeft));
                if (j < 25) {
                    buffer.append("\033[38;5;231;48;5;231m▓");
                    for (int k = 0; k < COLUMN_AMOUNT; k++) {
                        buffer.append("\033[38;5;231;48;5;231m▓".repeat(2));
                        var item = items[i * COLUMN_AMOUNT + k];
                        if (item == null) {
                            buffer.append("\033[0m ".repeat(50));
                            buffer.append("\033[38;5;231;48;5;231m▓".repeat(2));
                            continue;
                        }
                        var sprite = sprites.get(i * COLUMN_AMOUNT + k).split("\n");
                        buffer.append(sprite[j]);
                        buffer.append("\033[38;5;231;48;5;231m▓".repeat(2));
                    }
                    buffer.append("\033[38;5;231;48;5;231m▓");
                } else {
                    buffer.append("\033[38;5;231;48;5;231m▓".repeat(widthPixels));
                    if (i == 0 && includeCrafting) {
                        buffer.append("\033[0m").append(" ".repeat(20));
                        buffer.append("\033[38;5;231;48;5;231m▓".repeat(106));
                    }
                }

                if (i + 1 > spacesFromTop && includeCrafting) {
                    int craftingIndex = ((i - spacesFromTop) * 26) + j;

                    if (craftingIndex < craftingTable.length) {
                        buffer.append("\033[0m").append(" ".repeat(20));
                        buffer.append(craftingTable[craftingIndex]);
                    }
                }

                buffer.append("\n");
            }
        }

        buffer.append("\n".repeat(10));

        renderHotbar(buffer, spriteList, terminal, true, game);
    }

    /**
     * Generates a list of sprite strings for the given inventory items, highlighting selected.
     * @param items Array of InventoryItem objects.
     * @param spriteList SpriteList used to retrieve sprites.
     * @param selectedItem Index of the selected item.
     * @param game Current game instance.
     * @return List of sprite strings representing each inventory slot.
     */
    public List<String> getSprites(InventoryItem[] items, SpriteList spriteList, int selectedItem, Game game) {
        List<String> sprites = new ArrayList<>();

        for (int i = 0; i < items.length; i++) {
            var item = items[i];

            if (item == null) {
                if (i == selectedItem) {
                    sprites.add(getHotbarBackground());
                    continue;
                }

                sprites.add(null);
                continue;
            }

            StringBuilder stringBuilder = new StringBuilder();
            if (item.getItem().getFirst().getDurability() != 0
                    || item.getItem().getFirst().getDurability() == item.getItem().getFirst().getMaxDurability()) {
                stringBuilder.append(("\033[0m ".repeat(50) + "\n").repeat(21));

                stringBuilder.append("\033[0m ".repeat(4));
                int width = (int) (((double) item.getItem().getFirst().getDurability()
                        / item.getItem().getFirst().getMaxDurability()) * 42);

                stringBuilder.append("\033[38;5;231;48;5;231m▓".repeat(width));
                stringBuilder.append("\033[40m ".repeat(42 - width));
                stringBuilder.append("\033[0m ".repeat(4));
                stringBuilder.append("\n");

                stringBuilder.append(("\033[0m ".repeat(50) + "\n").repeat(3));
            }

            String sprite;

            if (i == selectedItem) {
                sprite = SpriteCombiner
                        .combineTwoSprites(getHotbarBackground(),
                                SpriteCombiner.combineTwoSprites(
                                        item.getItem().getFirst().getSpriteState().isPresent()
                                                ? spriteList.getSprite(item.getItem().getFirst().getSprite())
                                                .getSprite(item.getItem().getFirst().getSpriteState()
                                                        .get())
                                                : spriteList.getSprite(item.getItem().getFirst().getSprite())
                                                .getSprite(),
                                        Numbers.getNumberSprite(item.getAmount(), game)));
            } else {
                sprite = SpriteCombiner.combineTwoSprites(
                        item.getItem().getFirst().getSpriteState().isPresent()
                                ? spriteList.getSprite(item.getItem().getFirst().getSprite())
                                .getSprite(item.getItem().getFirst().getSpriteState().get())
                                : spriteList.getSprite(item.getItem().getFirst().getSprite()).getSprite(),
                        Numbers.getNumberSprite(item.getAmount(), game));
            }

            if (item.getItem().getFirst().getDurability() == 0
                    || item.getItem().getFirst().getDurability() == item.getItem().getFirst().getMaxDurability()) {
                sprites.add(sprite);
            } else {
                sprites.add(SpriteCombiner.combineTwoSprites(sprite, stringBuilder.toString()));
            }
        }

        return sprites;
    }

    /**
     * Retrieves an InventoryDTO wrapping the item array and index for the given absolute index.
     * @param index Index of the item in inventory/hotbar/crafting table.
     * @param i Optional custom InventoryItem array for special inventories.
     * @return InventoryDTO representing the selected inventory slot.
     */
    public InventoryDTO getItem(int index, Optional<InventoryItem[]> i) {
        if (index < 20) {
            // Normal inventory
            return new InventoryDTO(items, index);
        } else if (index >= 50) {
            return new InventoryDTO(i.get(), index - 50);
        } else if (index >= 29) {
            // Small crafting table
            return new InventoryDTO(smallCraftingTable.getItems(), index - 29);
        } else {
            // Hotbar
            return new InventoryDTO(hotbar, index - 20);
        }
    }

    /**
     * Returns the InventoryItem at the selected index without modifying inventory.
     * @param i Optional custom InventoryItem array.
     * @return InventoryItem at the selected index or null if none.
     */
    public InventoryItem getSelectedItemNo(Optional<InventoryItem[]> i) {
        InventoryDTO data = getItem(selectedItemInv, i);
        if (selectedItemInv == -1) {
            return null;
        }
        return data.getObj()[data.getIndex()];
    }

    public InventoryItem getSelectedItem(Optional<InventoryItem[]> i) {
        InventoryItem temp;
        InventoryDTO data = getItem(selectedItemInv, i);
        temp = data.getObj()[data.getIndex()];
        data.getObj()[data.getIndex()] = null;

        selectedItemInv = -1;
        rightClick = false;

        return temp;
    }

    public InventoryItem getOne(Optional<InventoryItem[]> i) {
        InventoryDTO inventoryItem = getItem(selectedItemInv, i);
        if (inventoryItem.getIndex() == -1) {
            return null;
        }
        InventoryItem item = inventoryItem.getObj()[inventoryItem.getIndex()];
        if (item.getAmount() == 1) {
            return getSelectedItem(i);
        }

        rightClick = false;

        var ij = new InventoryItem(1, item.getItem().getLast());
        item.decrease();

        return ij;
    }

    public InventoryItem getHalf(Optional<InventoryItem[]> i) {
        InventoryDTO inventoryItem = getItem(selectedItemInv, i);
        InventoryItem item = inventoryItem.getObj()[inventoryItem.getIndex()];
        if (item.getAmount() == 1) {
            return getSelectedItem(i);
        }

        int half = item.getAmount() / 2;

        var inv = new InventoryItem();

        for (int j = 0; j < half; j++) {
            inv.add(item.getItem().getLast());
            item.decrease();
        }

        selectedItemInv = -1;
        rightClick = false;

        return inv;
    }

    public boolean hasSelectedItem() {
        return selectedItemInv != -1;
    }

    public boolean mergeItems(int indexFrom, int indexTo, Optional<InventoryItem[]> i) {
        InventoryDTO fromData = getItem(indexFrom, i);
        InventoryDTO toData = getItem(indexTo, i);

        InventoryItem fromItem = fromData.getObj()[fromData.getIndex()];
        InventoryItem toItem = toData.getObj()[toData.getIndex()];

        if (fromItem == null || toItem == null) {
            return false; // Nothing to merge
        }

        if (!fromItem.getItem().getFirst().isStackable() || !toItem.getItem().getFirst().isStackable()) {
            return false;
        }

        if (!fromItem.getItem().getFirst().equals(toItem.getItem().getFirst())) {
            return false; // Different items cannot be merged
        }

        while (true) {
            if (toItem.getAmount() == 64) {
                break;
            }

            if (fromItem.getAmount() == 0) {
                fromData.getObj()[fromData.getIndex()] = null;
                break;
            }

            toItem.add(fromItem.getItem().getLast());
            fromItem.decrease();
        }

        selectedItemInv = -1;

        return true;
    }

    public void mergeOne(int fromIndex, int toIndex, Optional<InventoryItem[]> i) {
        InventoryDTO fromSlot = getItem(fromIndex, i);
        InventoryDTO toSlot = getItem(toIndex, i);

        InventoryItem fromItem = fromSlot.getObj()[fromSlot.getIndex()];
        InventoryItem toItem = toSlot.getObj()[toSlot.getIndex()];

        if (fromItem == null || !fromItem.getItem().getFirst().equals(toItem.getItem().getFirst())
                || toItem.getAmount() >= fromItem.getItem().getFirst().getStackAmount()) {
            return; // Can't merge
        }

        // Move only one item
        toItem.add(fromItem.getItem().getLast());
        fromItem.decrease();

        // Remove source item if empty
        if (fromItem.getAmount() <= 0) {
            fromSlot.getObj()[fromSlot.getIndex()] = null;
            selectedItemInv = -1;
        }
    }

    public void exit() {
        // Put all items from crafting to inv
        for (int i = 0; i < smallCraftingTable.getItems().length; i++) {
            InventoryItem item = smallCraftingTable.getItems()[i];
            if (item == null) {
                continue;
            }

            addItem(item);
            smallCraftingTable.getItems()[i] = null;
        }
    }
}
