package net.knarcraft.stargate.portal;

import net.knarcraft.stargate.Stargate;
import net.knarcraft.stargate.container.BlockLocation;
import net.knarcraft.stargate.container.RelativeBlockVector;
import org.bukkit.Material;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * A gate describes the physical structure of a stargate
 *
 * <p>While the portal class represents a portal in space, the Gate class represents the physical gate/portal entrance.</p>
 */
public class Gate {

    private final String filename;
    private final GateLayout layout;
    private final Map<Character, Material> characterMaterialMap;
    //Gate materials
    private final Material portalOpenBlock;
    private final Material portalClosedBlock;
    private final Material portalButton;
    //Economy information
    private final int useCost;
    private final int createCost;
    private final int destroyCost;
    private final boolean toOwner;

    /**
     * Instantiates a new gate
     *
     * @param filename             <p>The name of the gate file, including extension</p>
     * @param layout               <p>The gate layout defined in the gate file</p>
     * @param characterMaterialMap <p>The material types the different layout characters represent</p>
     * @param portalOpenBlock      <p>The material to set the opening to when the portal is open</p>
     * @param portalClosedBlock    <p>The material to set the opening to when the portal is closed</p>
     * @param portalButton         <p>The material to use for the portal button</p>
     * @param useCost              <p>The cost of using a portal with this gate layout (-1 to disable)</p>
     * @param createCost           <p>The cost of creating a portal with this gate layout (-1 to disable)</p>
     * @param destroyCost          <p>The cost of destroying a portal with this gate layout (-1 to disable)</p>
     * @param toOwner              <p>Whether any payment should go to the owner of the gate, as opposed to just disappearing</p>
     */
    public Gate(String filename, GateLayout layout, Map<Character, Material> characterMaterialMap, Material portalOpenBlock,
                Material portalClosedBlock, Material portalButton, int useCost, int createCost, int destroyCost,
                boolean toOwner) {
        this.filename = filename;
        this.layout = layout;
        this.characterMaterialMap = characterMaterialMap;
        this.portalOpenBlock = portalOpenBlock;
        this.portalClosedBlock = portalClosedBlock;
        this.portalButton = portalButton;
        this.useCost = useCost;
        this.createCost = createCost;
        this.destroyCost = destroyCost;
        this.toOwner = toOwner;
    }

    /**
     * Gets this gate's layout
     *
     * @return <p>This gate's layout</p>
     */
    public GateLayout getLayout() {
        return layout;
    }

    /**
     * Gets the material type used for this gate's control blocks
     *
     * @return <p>The material type used for control blocks</p>
     */
    public Material getControlBlock() {
        return characterMaterialMap.get(GateHandler.getControlBlockCharacter());
    }

    /**
     * Gets the filename of this gate's file
     *
     * @return <p>The filename of this gate's file</p>
     */
    public String getFilename() {
        return filename;
    }

    /**
     * Gets the block type to use for the opening when a portal using this gate is open
     *
     * @return <p>The block type to use for the opening when open</p>
     */
    public Material getPortalOpenBlock() {
        return portalOpenBlock;
    }

    /**
     * Gets the block type to use for the opening when a portal using this gate is closed
     *
     * @return <p>The block type to use for the opening when closed</p>
     */
    public Material getPortalClosedBlock() {
        return portalClosedBlock;
    }

    /**
     * Gets the material to use for a portal's button if using this gate type
     *
     * @return <p>The material to use for a portal's button if using this gate type</p>
     */
    public Material getPortalButton() {
        return portalButton;
    }

    /**
     * Gets the cost of using a portal with this gate
     *
     * @return <p>The cost of using a portal with this gate</p>
     */
    public int getUseCost() {
        return useCost < 0 ? Stargate.getEconomyConfig().getDefaultUseCost() : useCost;
    }

    /**
     * Gets the cost of creating a portal with this gate
     *
     * @return <p>The cost of creating a portal with this gate</p>
     */
    public Integer getCreateCost() {
        return createCost < 0 ? Stargate.getEconomyConfig().getDefaultCreateCost() : createCost;
    }

    /**
     * Gets the cost of destroying a portal with this gate
     *
     * @return <p>The cost of destroying a portal with this gate</p>
     */
    public Integer getDestroyCost() {
        return destroyCost < 0 ? Stargate.getEconomyConfig().getDefaultDestroyCost() : destroyCost;
    }

    /**
     * Gets whether portal payments go to this portal's owner
     *
     * @return <p>Whether portal payments go to the owner</p>
     */
    public Boolean getToOwner() {
        return toOwner;
    }

    /**
     * Checks whether a portal's gate matches this gate type
     *
     * @param topLeft <p>The top-left block of the portal's gate</p>
     * @param yaw     <p>The yaw when looking directly outwards</p>
     * @return <p>True if this gate matches the portal</p>
     */
    public boolean matches(BlockLocation topLeft, double yaw) {
        return matches(topLeft, yaw, false);
    }

    /**
     * Checks whether a portal's gate matches this gate type
     *
     * <p>If enabling onCreate, opening blocks with materials AIR and WATER will be allowed even if the gate closed
     * material is a different one. If checking and onCreate is not enabled, any inconsistency with opening blocks
     * containing AIR or WATER will cause the gate to not match.</p>
     *
     * @param topLeft  <p>The top-left block of the portal's gate</p>
     * @param yaw      <p>The yaw when looking directly outwards</p>
     * @param onCreate <p>Whether this is used in the context of creating a new gate</p>
     * @return <p>True if this gate matches the portal</p>
     */
    public boolean matches(BlockLocation topLeft, double yaw, boolean onCreate) {
        return verifyGateEntrancesMatch(topLeft, yaw, onCreate) && verifyGateBorderMatches(topLeft, yaw);
    }

    /**
     * Verifies that all border blocks of a portal matches this gate type
     *
     * @param topLeft <p>The top-left block of the portal</p>
     * @param yaw     <p>The yaw when looking directly outwards from the portal</p>
     * @return <p>True if all border blocks of the gate match the layout</p>
     */
    private boolean verifyGateBorderMatches(BlockLocation topLeft, double yaw) {
        Map<Character, Material> characterMaterialMap = new HashMap<>(this.characterMaterialMap);
        for (RelativeBlockVector borderVector : layout.getBorder()) {
            int rowIndex = borderVector.getRight();
            int lineIndex = borderVector.getDown();
            Character key = layout.getLayout()[lineIndex][rowIndex];

            Material materialInLayout = characterMaterialMap.get(key);
            Material materialAtLocation = topLeft.getRelativeLocation(borderVector, yaw).getType();

            if (materialInLayout == null) {
                /* This generally should not happen with proper checking, but just in case a material character is not
                 * recognized, but still allowed in previous checks, verify the gate as long as all such instances of
                 * the character correspond to the same material in the physical gate. All subsequent gates will also
                 * need to match the first verified gate. */
                characterMaterialMap.put(key, materialAtLocation);
                Stargate.debug("Gate::Matches", String.format("Missing layout material in %s. Using %s from the" +
                        " physical portal.", getFilename(), materialAtLocation));
            } else if (materialAtLocation != materialInLayout) {
                Stargate.debug("Gate::Matches", String.format("Block Type Mismatch: %s != %s",
                        materialAtLocation, materialInLayout));
                return false;
            }
        }
        return true;
    }

    /**
     * Verifies that all entrances of a portal gate matches this gate type
     *
     * @param topLeft  <p>The top-left block of this portal</p>
     * @param yaw      <p>The yaw when looking directly outwards</p>
     * @param onCreate <p>Whether this is used in the context of creating a new gate</p>
     * @return <p>Whether this is used in the context of creating a new gate</p>
     */
    private boolean verifyGateEntrancesMatch(BlockLocation topLeft, double yaw, boolean onCreate) {
        Stargate.debug("verifyGateEntrancesMatch", String.valueOf(topLeft));
        for (RelativeBlockVector entranceVector : layout.getEntrances()) {
            Stargate.debug("verifyGateEntrancesMatch", String.valueOf(entranceVector));
            Material type = topLeft.getRelativeLocation(entranceVector, yaw).getType();

            //Ignore entrance if it's air or water, and we're creating a new gate
            if (onCreate && (type == Material.AIR || type == Material.WATER)) {
                continue;
            }

            if (type != portalClosedBlock && type != portalOpenBlock) {
                Stargate.debug("Gate::Matches", "Entrance/Exit Material Mismatch: " + type);
                return false;
            }
        }
        return true;
    }

    /**
     * Saves this gate to a file
     *
     * <p>This method will save the gate to its filename in the given folder.</p>
     *
     * @param gateFolder <p>The folder to save the gate file in</p>
     */
    public void save(String gateFolder) {
        try {
            BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(gateFolder + filename));

            //Save main material names
            writeConfig(bufferedWriter, "portal-open", portalOpenBlock.name());
            writeConfig(bufferedWriter, "portal-closed", portalClosedBlock.name());
            writeConfig(bufferedWriter, "button", portalButton.name());

            //Save the values necessary for economy
            saveEconomyValues(bufferedWriter);

            //Store material types to use for frame blocks
            saveFrameBlockTypes(bufferedWriter);

            bufferedWriter.newLine();

            //Save the gate layout
            layout.saveLayout(bufferedWriter);

            bufferedWriter.close();
        } catch (IOException ex) {
            Stargate.logSevere(String.format("Could not save Gate %s - %s", filename, ex.getMessage()));
        }
    }

    /**
     * Saves current economy related values using a buffered writer
     *
     * @param bufferedWriter <p>The buffered writer to write to</p>
     * @throws IOException <p>If unable to write to the buffered writer</p>
     */
    private void saveEconomyValues(BufferedWriter bufferedWriter) throws IOException {
        //Write use cost if not disabled
        if (useCost != -1) {
            writeConfig(bufferedWriter, "usecost", useCost);
        }
        //Write create cost if not disabled
        if (createCost != -1) {
            writeConfig(bufferedWriter, "createcost", createCost);
        }
        //Write destroy cost if not disabled
        if (destroyCost != -1) {
            writeConfig(bufferedWriter, "destroycost", destroyCost);
        }
        writeConfig(bufferedWriter, "toowner", toOwner);
    }

    /**
     * Saves the types of blocks used for the gate frame/border using a buffered writer
     *
     * @param bufferedWriter <p>The buffered writer to write to</p>
     * @throws IOException <p>If unable to write to the buffered writer</p>
     */
    private void saveFrameBlockTypes(BufferedWriter bufferedWriter) throws IOException {
        for (Map.Entry<Character, Material> entry : characterMaterialMap.entrySet()) {
            Character type = entry.getKey();
            Material value = entry.getValue();
            //Skip characters not part of the frame
            if (type.equals(GateHandler.getAnythingCharacter()) ||
                    type.equals(GateHandler.getEntranceCharacter()) ||
                    type.equals(GateHandler.getExitCharacter())) {
                continue;
            }

            bufferedWriter.append(type);
            bufferedWriter.append('=');
            if (value != null) {
                bufferedWriter.append(value.toString());
            }
            bufferedWriter.newLine();
        }
    }

    /**
     * Writes a formatted string to a buffered writer
     *
     * @param bufferedWriter <p>The buffered writer to write the formatted string to</p>
     * @param key            <p>The config key to save</p>
     * @param value          <p>The config value to save</p>
     * @throws IOException <p>If unable to write to the buffered writer</p>
     */
    private void writeConfig(BufferedWriter bufferedWriter, String key, Object value) throws IOException {
        //Figure out the correct formatting to use
        String format = "%s=";
        if (value instanceof Boolean) {
            format += "%b";
        } else if (value instanceof Integer) {
            format += "%d";
        } else if (value instanceof String) {
            format += "%s";
        } else {
            throw new IllegalArgumentException("Unrecognized config value type");
        }

        bufferedWriter.append(String.format(format, key, value));
        bufferedWriter.newLine();
    }

}