//
// Created by BONNe
// Copyright - 2021
//


package world.bentobox.bentobox.api.panels;


import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.function.BiFunction;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;

import world.bentobox.bentobox.BentoBox;
import world.bentobox.bentobox.api.panels.builders.PanelItemBuilder;
import world.bentobox.bentobox.api.panels.builders.TemplatedPanelBuilder;
import world.bentobox.bentobox.api.panels.reader.ItemTemplateRecord;
import world.bentobox.bentobox.api.panels.reader.PanelTemplateRecord;
import world.bentobox.bentobox.api.user.User;


/**
 * This class creates a new Panel from the template record.
 * @author BONNe
 * @since 1.17.3
 */
public class TemplatedPanel extends Panel
{
    /**
     * TemplatedPanel constructor class which generates functional panel.
     * @param builder Builder that contains all information about the panel that must be generated.
     */
    public TemplatedPanel(@NonNull TemplatedPanelBuilder builder)
    {
        this.user = builder.getUser();
        this.setWorld(builder.getWorld());
        this.setListener(builder.getListener());

        this.panelTemplate = builder.getPanelTemplate();
        // Init type creators
        this.typeCreators = new HashMap<>(builder.getObjectCreatorMap());
        this.typeIndex = new HashMap<>(builder.getObjectCreatorMap().size());
        this.typeSlotMap = new HashMap<>(builder.getObjectCreatorMap().size());

        this.parameters = builder.getParameters().toArray(new String[0]);

        if (this.panelTemplate == null)
        {
            BentoBox.getInstance().logError("Cannot generate panel because template is not loaded.");
        }
        else
        {
            this.generatePanel();
        }
    }


    /**
     * This method generates the panel from the template.
     */
    private void generatePanel()
    {
        Map<Integer, PanelItem> items = switch (this.panelTemplate.type())
                {
                case INVENTORY -> this.populateInventoryPanel();
                case HOPPER -> this.populateHopperPanel();
                case DROPPER -> this.populateDropperPanel();
                };

                super.makePanel(this.user.getTranslation(this.panelTemplate.title(), this.parameters),
                        items,
                        items.keySet().stream().max(Comparator.naturalOrder()).orElse(9),
                        this.user,
                        this.getListener().orElse(null),
                        this.panelTemplate.type());
    }

    /**
     * This method creates map with item indexes and their icons that will be added into
     * Inventory Panel.
     * @return Map that contains indexes linked to the correct panel item.
     */
    @NonNull
    private Map<Integer, PanelItem> populateInventoryPanel() {
        PanelItem[][] itemArray = new PanelItem[6][9];
        processItemData(itemArray);
        removeEmptyLines(itemArray);
        fillBorder(itemArray);
        fillBackground(itemArray);
        return createItemMap(itemArray);
    }

    private void processItemData(PanelItem[][] itemArray) {
        // Analyze the GUI button layout a bit.
        for (int i = 0; i < panelTemplate.content().length; i++) {
            for (int k = 0; k < panelTemplate.content()[i].length; k++) {
                ItemTemplateRecord rec = panelTemplate.content()[i][k];
                if (rec != null && rec.dataMap().containsKey("type")) {
                    String type = String.valueOf(rec.dataMap().get("type"));
                    int counter = typeSlotMap.computeIfAbsent(type, key -> 0);
                    typeSlotMap.put(type, counter + 1);
                }
                // Make buttons for the GUI
                itemArray[i][k] = makeButton(panelTemplate.content()[i][k]);
            }
        }
    }

    private void removeEmptyLines(PanelItem[][] itemArray) {
        boolean[] showLine = panelTemplate.forcedRows();
        for (int i = 0; i < panelTemplate.content().length; i++) {
            boolean emptyLine = true;
            for (int k = 0; emptyLine && k < panelTemplate.content()[i].length; k++) {
                emptyLine = itemArray[i][k] == null;
            }
            showLine[i] = showLine[i] || !emptyLine;
        }
    }

    private void fillBorder(PanelItem[][] itemArray) {
        if (panelTemplate.border() == null) {
            return;
        }
        PanelItem template = makeTemplate(panelTemplate.border());
        int numRows = itemArray.length;
        int numCols = itemArray[0].length;

        for (int i = 0; i < numRows; i++) {
            if (i == 0 || i == numRows - 1) {
                // Fill first and last row completely with border.
                for (int k = 0; k < numCols; k++) {
                    if (itemArray[i][k] == null) {
                        itemArray[i][k] = template;
                    }
                }
            } else {
                // Fill first and last element in row with border.
                if (itemArray[i][0] == null) {
                    itemArray[i][0] = template;
                }
                if (itemArray[i][numCols - 1] == null) {
                    itemArray[i][numCols - 1] = template;
                }
            }
        }
        panelTemplate.forcedRows()[0] = true;
        panelTemplate.forcedRows()[numRows - 1] = true;
    }

    private void fillBackground(PanelItem[][] itemArray) {
        if (panelTemplate.background() != null) {
            PanelItem template = makeTemplate(panelTemplate.background());
            for (int i = 0; i < 6; i++) {
                for (int k = 0; k < 9; k++) {
                    if (itemArray[i][k] == null) {
                        itemArray[i][k] = template;
                    }
                }
            }
        }
    }

    private Map<Integer, PanelItem> createItemMap(PanelItem[][] itemArray) {
        Map<Integer, PanelItem> itemMap = new HashMap<>(6 * 9);
        int correctIndex = 0;
        for (int i = 0; i < itemArray.length; i++) {
            final boolean iterate = panelTemplate.forcedRows()[i];
            for (int k = 0; iterate && k < itemArray[i].length; k++) {
                if (itemArray[i][k] != null) {
                    itemMap.put(correctIndex, itemArray[i][k]);
                }
                correctIndex++;
            }
        }
        return itemMap;
    }

    /**
     * This method creates map with item indexes and their icons that will be added into
     * hopper Panel.
     * @return Map that contains indexes linked to the correct panel item.
     */
    @NonNull
    private Map<Integer, PanelItem> populateHopperPanel()
    {
        // Init item array with the max available size.
        PanelItem[] itemArray = new PanelItem[5];

        // Analyze the template
        for (int i = 0; i < 5; i++)
        {
            ItemTemplateRecord rec = this.panelTemplate.content()[0][i];

            if (rec != null && rec.dataMap().containsKey("type"))
            {
                String type = String.valueOf(rec.dataMap().get("type"));

                int counter = this.typeSlotMap.computeIfAbsent(type, key -> 0);
                this.typeSlotMap.put(type, counter + 1);
            }
        }

        // Make buttons
        for (int i = 0; i < 5; i++)
        {
            itemArray[i] = this.makeButton(this.panelTemplate.content()[0][i]);
        }

        // Now fill the background.
        if (this.panelTemplate.background() != null)
        {
            PanelItem template = this.makeTemplate(this.panelTemplate.background());

            for (int i = 0; i < 5; i++)
            {
                if (itemArray[i] == null)
                {
                    itemArray[i] = template;
                }
            }
        }

        // Now place all panel items with their indexes into item map.
        Map<Integer, PanelItem> itemMap = new HashMap<>(5);

        int correctIndex = 0;

        for (PanelItem panelItem : itemArray)
        {
            if (panelItem != null)
            {
                itemMap.put(correctIndex, panelItem);
            }

            correctIndex++;
        }

        return itemMap;
    }


    /**
     * This method creates map with item indexes and their icons that will be added into
     * dropper Panel.
     * @return Map that contains indexes linked to the correct panel item.
     */
    @NonNull
    private Map<Integer, PanelItem> populateDropperPanel()
    {
        // Analyze the template
        for (int i = 0; i < 3; i++)
        {
            for (int k = 0; k < 3; k++)
            {
                ItemTemplateRecord rec = this.panelTemplate.content()[i][k];

                if (rec != null && rec.dataMap().containsKey("type"))
                {
                    String type = String.valueOf(rec.dataMap().get("type"));

                    int counter = this.typeSlotMap.computeIfAbsent(type, key -> 0);
                    this.typeSlotMap.put(type, counter + 1);
                }
            }
        }

        // Init item array with the max available size.
        PanelItem[][] itemArray = new PanelItem[3][3];

        // Make buttons
        for (int i = 0; i < 3; i++)
        {
            for (int k = 0; k < 3; k++)
            {
                itemArray[i][k] = this.makeButton(this.panelTemplate.content()[i][k]);
            }
        }

        // Now fill the background.
        if (this.panelTemplate.background() != null)
        {
            PanelItem template = this.makeTemplate(this.panelTemplate.background());

            for (int i = 0; i < 3; i++)
            {
                for (int k = 0; k < 3; k++)
                {
                    if (itemArray[i][k] == null)
                    {
                        itemArray[i][k] = template;
                    }
                }
            }
        }

        // Init item map with the max available size.
        Map<Integer, PanelItem> itemMap = new HashMap<>(9);

        int correctIndex = 0;

        for (int i = 0; i < 3; i++)
        {
            for (int k = 0; k < 3; k++)
            {
                if (itemArray[i][k] != null)
                {
                    itemMap.put(correctIndex, itemArray[i][k]);
                }

                correctIndex++;
            }
        }

        return itemMap;
    }


    /**
     * This method passes button creation from given record template.
     * @param rec Template of the button that must be created.
     * @return PanelItem of the template, otherwise null.
     */
    @Nullable
    private PanelItem makeButton(@Nullable ItemTemplateRecord rec)
    {
        if (rec == null)
        {
            // Immediate exit if record is null.
            return null;
        }

        if (rec.dataMap().containsKey("type"))
        {
            // If dataMap is not null, and it is not empty, then pass button to the object creator function.

            return this.makeAddonButton(rec);
        }
        else
        {
            PanelItemBuilder itemBuilder = new PanelItemBuilder();

            if (rec.icon() != null)
            {
                itemBuilder.icon(rec.icon().clone());
            }

            if (rec.title() != null)
            {
                itemBuilder.name(this.user.getTranslation(rec.title()));
            }

            if (rec.description() != null)
            {
                itemBuilder.description(this.user.getTranslation(rec.description()));
            }

            // If there are generic click handlers that could be added, then this is a place
            // where to process them.

            // Click Handlers are managed by custom addon buttons.
            return itemBuilder.build();
        }
    }


    /**
     * This method passes button to the type creator, if that exists.
     * @param rec Template of the button that must be created.
     * @return PanelItem of the button created by typeCreator, otherwise null.
     */
    @Nullable
    private PanelItem makeAddonButton(@NonNull ItemTemplateRecord rec)
    {
        // Get object type.
        String type = String.valueOf(rec.dataMap().getOrDefault("type", ""));

        if (!this.typeCreators.containsKey(type))
        {
            // There are no object with a given type.
            return this.makeFallBack(rec.fallback());
        }

        BiFunction<ItemTemplateRecord, ItemSlot, PanelItem> buttonBuilder = this.typeCreators.get(type);

        // Get next slot index.
        ItemSlot itemSlot = this.typeIndex.containsKey(type) ?
                this.typeIndex.get(type) :
                    new ItemSlot(0, this.typeSlotMap);
        this.typeIndex.put(type, itemSlot.nextItemSlot());

        // Try to get next object.
        PanelItem item = buttonBuilder.apply(rec, itemSlot);
        return item == null ? this.makeFallBack(rec.fallback()) : item;
    }


    /**
     * This method creates a fall back button for given record.
     * @param rec Record which fallback must be created.
     * @return PanelItem if fallback was creates successfully, otherwise null.
     */
    @Nullable
    private PanelItem makeFallBack(@Nullable ItemTemplateRecord rec)
    {
        return rec == null ? null : this.makeButton(rec.fallback());
    }


    /**
     * This method translates template record into a panel item.
     * @param rec Record that must be translated.
     * @return PanelItem that contains all information from the record.
     */
    private PanelItem makeTemplate(PanelTemplateRecord.TemplateItem rec)
    {
        PanelItemBuilder itemBuilder = new PanelItemBuilder();

        // Read icon only if it is not null.
        if (rec.icon() != null)
        {
            itemBuilder.icon(rec.icon().clone());
        }

        // Read title only if it is not null.
        if (rec.title() != null)
        {
            itemBuilder.name(this.user.getTranslation(rec.title()));
        }

        // Read description only if it is not null.
        if (rec.description() != null)
        {
            itemBuilder.description(this.user.getTranslation(rec.description()));
        }

        // Click Handlers are managed by custom addon buttons.
        return itemBuilder.build();
    }


    // ---------------------------------------------------------------------
    // Section: Classes
    // ---------------------------------------------------------------------


    /**
     * This record contains current slot object and map that links types with a number of slots in
     * panel with it.
     * Some buttons need information about all types, like previous/next.
     * @param slot Index of object in current panel.
     * @param amountMap Map that links types with number of objects in panel.
     */
    public record ItemSlot(int slot, Map<String, Integer> amountMap)
    {
        /**
         * This method returns new record object with iterative slot index.
         * @return New ItemSlot object that has increased slot index by 1.
         */
        ItemSlot nextItemSlot()
        {
            return new ItemSlot(this.slot() + 1, this.amountMap());
        }
    }


    // ---------------------------------------------------------------------
    // Section: Variables
    // ---------------------------------------------------------------------


    /**
     * The GUI template record.
     */
    private final PanelTemplateRecord panelTemplate;

    /**
     * The user who opens the GUI.
     */
    private final User user;

    /**
     * This map links custom types with their info object.
     */
    private final Map<String, BiFunction<ItemTemplateRecord, ItemSlot, PanelItem>> typeCreators;

    /**
     * Stores the item slot information for each type.
     */
    private final Map<String, ItemSlot> typeIndex;

    /**
     * Stores the number of items with given type in whole panel.
     */
    private final Map<String, Integer> typeSlotMap;

    /**
     * Stores the parameters for panel title object.
     * @since 1.20.0
     */
    private final String[] parameters;
}
