package com.earth2me.essentials.signs;

import com.earth2me.essentials.ChargeException;
import com.earth2me.essentials.CommandSource;
import com.earth2me.essentials.MetaItemStack;
import com.earth2me.essentials.Trade;
import com.earth2me.essentials.User;
import com.earth2me.essentials.utils.AdventureUtil;
import com.earth2me.essentials.utils.FormatUtil;
import com.earth2me.essentials.utils.MaterialUtil;
import com.earth2me.essentials.utils.NumberUtil;
import com.earth2me.essentials.utils.VersionUtil;
import net.ess3.api.IEssentials;
import net.ess3.api.MaxMoneyException;
import net.ess3.api.events.SignBreakEvent;
import net.ess3.api.events.SignCreateEvent;
import net.ess3.api.events.SignInteractEvent;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.block.Block;
import org.bukkit.block.BlockFace;
import org.bukkit.block.Sign;
import org.bukkit.block.data.Directional;
import org.bukkit.entity.Player;
import org.bukkit.event.block.SignChangeEvent;
import org.bukkit.inventory.ItemStack;

import java.math.BigDecimal;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;

import static com.earth2me.essentials.I18n.tlLiteral;

public class EssentialsSign {
    private static final String SIGN_OWNER_KEY = "sign-owner";
    protected static final BigDecimal MINTRANSACTION = new BigDecimal("0.01");
    private static final Set<Material> EMPTY_SET = new HashSet<>();
    protected transient final String signName;

    public EssentialsSign(final String signName) {
        this.signName = signName;
    }

    protected static boolean checkIfBlockBreaksSigns(final Block block) {
        final Block sign = block.getRelative(BlockFace.UP);
        if (MaterialUtil.isSignPost(sign.getType()) && isValidSign(new BlockSign(sign))) {
            return true;
        }
        final BlockFace[] directions = new BlockFace[] {BlockFace.NORTH, BlockFace.EAST, BlockFace.SOUTH, BlockFace.WEST};
        for (final BlockFace blockFace : directions) {
            final Block signBlock = block.getRelative(blockFace);
            if (MaterialUtil.isWallSign(signBlock.getType()) || MaterialUtil.isWallHangingSign(signBlock.getType())) {
                try {
                    if (getWallSignFacing(signBlock) == blockFace && isValidSign(new BlockSign(signBlock))) {
                        return true;
                    }
                } catch (final NullPointerException ex) {
                    // Sometimes signs enter a state of being semi broken, having no text or state data, usually while burning.
                }
            }
        }
        return false;
    }

    /**
     * @deprecated use {@link #isValidSign(IEssentials, ISign)} if possible
     */
    @Deprecated
    public static boolean isValidSign(final ISign sign) {
        return sign.getLine(0).matches("§1\\[.*]");
    }

    public static boolean isValidSign(final IEssentials ess, final ISign sign) {
        if (!sign.getLine(0).matches("§1\\[.*]"))
            return false;

        // Validate that the sign is actually an essentials sign
        final String signName = ChatColor.stripColor(sign.getLine(0)).replaceAll("[^a-zA-Z]", "");
        for (final EssentialsSign essSign : ess.getSettings().enabledSigns()) {
            if (essSign.getName().equalsIgnoreCase(signName))
                return true;
        }

        return false;
    }

    private static BlockFace getWallSignFacing(final Block block) {
        if (VersionUtil.PRE_FLATTENING) {
            //noinspection deprecation
            final org.bukkit.material.Sign signMat = (org.bukkit.material.Sign) block.getState().getData();
            return signMat.getFacing();
        }

        final Directional signData = (Directional) block.getState().getBlockData();
        return signData.getFacing();
    }

    protected final boolean onSignCreate(final SignChangeEvent event, final IEssentials ess) {
        final ISign sign = new EventSign(event);
        final User user = ess.getUser(event.getPlayer());
        if (!(user.isAuthorized("essentials.signs." + signName.toLowerCase(Locale.ENGLISH) + ".create") || user.isAuthorized("essentials.signs.create." + signName.toLowerCase(Locale.ENGLISH)))) {
            // Return true, so other plugins can use the same sign title, just hope
            // they won't change it to §1[Signname]
            return true;
        }
        sign.setLine(0, AdventureUtil.miniToLegacy(tlLiteral("signFormatFail", this.signName)));

        final SignCreateEvent signEvent = new SignCreateEvent(sign, this, user);
        ess.getServer().getPluginManager().callEvent(signEvent);
        if (signEvent.isCancelled()) {
            if (ess.getSettings().isDebug()) {
                ess.getLogger().info("SignCreateEvent cancelled for sign " + signEvent.getEssentialsSign().getName());
            }
            return false;
        }

        try {
            final boolean ret = onSignCreate(sign, user, getUsername(user), ess);
            if (ret) {
                sign.setLine(0, getSuccessName(ess));
            }
            return ret;
        } catch (final ChargeException | SignException ex) {
            showError(ess, user.getSource(), ex, signName);
        }
        setOwnerData(ess, user, sign);
        // Return true, so the player sees the wrong sign.
        return true;
    }

    public String getSuccessName(final IEssentials ess) {
        final String successName = getSuccessName();
        if (successName == null) {
            ess.getLogger().severe("signFormatSuccess message must use the {0} argument.");
        }
        return successName;
    }

    public String getSuccessName() {
        String successName = AdventureUtil.miniToLegacy(tlLiteral("signFormatSuccess", this.signName));
        if (successName.isEmpty() || !successName.contains(this.signName)) {
            // Set to null to cause an error in place of no functionality. This makes an error obvious as opposed to leaving users baffled by lack of
            // functionality.
            successName = null;
        }
        return successName;
    }

    public String getTemplateName() {
        return AdventureUtil.miniToLegacy(tlLiteral("signFormatTemplate", this.signName));
    }

    public String getName() {
        return this.signName;
    }

    public String getUsername(final User user) {
        // Truncate username to ensure it can fit on a sign
        return user.getName().substring(0, Math.min(user.getName().length(), 13));
    }

    public void setOwner(final IEssentials ess, final User user, final ISign signProvider, final int nameIndex, final String namePrefix) {
        setOwnerData(ess, user, signProvider);
        signProvider.setLine(nameIndex, namePrefix + getUsername(user));
    }

    public void setOwnerData(final IEssentials ess, final User user, final ISign signProvider) {
        if (ess.getSignDataProvider() == null) {
            return;
        }
        final Sign sign = (Sign) signProvider.getBlock().getState();
        ess.getSignDataProvider().setSignData(sign, SIGN_OWNER_KEY, user.getUUID().toString());
    }

    public boolean isOwner(final IEssentials ess, final User user, final ISign signProvider, final int nameIndex, final String namePrefix) {
        final Sign sign = (Sign) signProvider.getBlock().getState();
        if (ess.getSignDataProvider() == null || ess.getSignDataProvider().getSignData(sign, SIGN_OWNER_KEY) == null) {
            final boolean isLegacyOwner = FormatUtil.stripFormat(signProvider.getLine(nameIndex)).equalsIgnoreCase(getUsername(user));
            if (ess.getSignDataProvider() != null && isLegacyOwner) {
                ess.getSignDataProvider().setSignData(sign, SIGN_OWNER_KEY, user.getUUID().toString());
            }
            return isLegacyOwner;
        }

        if (user.getUUID().toString().equals(ess.getSignDataProvider().getSignData(sign, SIGN_OWNER_KEY))) {
            signProvider.setLine(nameIndex, namePrefix + getUsername(user));
            return true;
        }
        return false;
    }

    protected final boolean onSignInteract(final Block block, final Player player, final IEssentials ess) {
        final ISign sign = new BlockSign(block);
        final User user = ess.getUser(player);
        if (user.checkSignThrottle()) {
            return false;
        }
        try {
            if (user.getBase().isDead() || !(user.isAuthorized("essentials.signs." + signName.toLowerCase(Locale.ENGLISH) + ".use") || user.isAuthorized("essentials.signs.use." + signName.toLowerCase(Locale.ENGLISH)))) {
                return false;
            }

            final SignInteractEvent signEvent = new SignInteractEvent(sign, this, user);
            ess.getServer().getPluginManager().callEvent(signEvent);
            if (signEvent.isCancelled()) {
                return false;
            }

            return onSignInteract(sign, user, getUsername(user), ess);
        } catch (final Exception ex) {
            showError(ess, user.getSource(), ex, signName);
            return false;
        }
    }

    protected final boolean onSignBreak(final Block block, final Player player, final IEssentials ess) throws MaxMoneyException {
        final ISign sign = new BlockSign(block);
        final User user = ess.getUser(player);
        try {
            if (!(user.isAuthorized("essentials.signs." + signName.toLowerCase(Locale.ENGLISH) + ".break") || user.isAuthorized("essentials.signs.break." + signName.toLowerCase(Locale.ENGLISH)))) {
                return false;
            }

            final SignBreakEvent signEvent = new SignBreakEvent(sign, this, user);
            ess.getServer().getPluginManager().callEvent(signEvent);
            if (signEvent.isCancelled()) {
                return false;
            }

            return onSignBreak(sign, user, getUsername(user), ess);
        } catch (final SignException ex) {
            showError(ess, user.getSource(), ex, signName);
            return false;
        }
    }

    protected boolean onSignCreate(final ISign sign, final User player, final String username, final IEssentials ess) throws SignException, ChargeException {
        return true;
    }

    protected boolean onSignInteract(final ISign sign, final User player, final String username, final IEssentials ess) throws SignException, ChargeException, MaxMoneyException {
        return true;
    }

    protected boolean onSignBreak(final ISign sign, final User player, final String username, final IEssentials ess) throws SignException, MaxMoneyException {
        return true;
    }

    protected final boolean onBlockPlace(final Block block, final Player player, final IEssentials ess) {
        final User user = ess.getUser(player);
        try {
            return onBlockPlace(block, user, getUsername(user), ess);
        } catch (final ChargeException | SignException ex) {
            showError(ess, user.getSource(), ex, signName);
        }
        return false;
    }

    protected final boolean onBlockInteract(final Block block, final Player player, final IEssentials ess) {
        final User user = ess.getUser(player);
        try {
            return onBlockInteract(block, user, getUsername(user), ess);
        } catch (final ChargeException | SignException ex) {
            showError(ess, user.getSource(), ex, signName);
        }
        return false;
    }

    protected final boolean onBlockBreak(final Block block, final Player player, final IEssentials ess) throws MaxMoneyException {
        final User user = ess.getUser(player);
        try {
            return onBlockBreak(block, user, getUsername(user), ess);
        } catch (final SignException ex) {
            showError(ess, user.getSource(), ex, signName);
        }
        return false;
    }

    protected boolean onBlockBreak(final Block block, final IEssentials ess) {
        return true;
    }

    protected boolean onBlockExplode(final Block block, final IEssentials ess) {
        return true;
    }

    protected boolean onBlockBurn(final Block block, final IEssentials ess) {
        return true;
    }

    protected boolean onBlockIgnite(final Block block, final IEssentials ess) {
        return true;
    }

    protected boolean onBlockPush(final Block block, final IEssentials ess) {
        return true;
    }

    protected boolean onBlockPlace(final Block block, final User player, final String username, final IEssentials ess) throws SignException, ChargeException {
        return true;
    }

    protected boolean onBlockInteract(final Block block, final User player, final String username, final IEssentials ess) throws SignException, ChargeException {
        return true;
    }

    protected boolean onBlockBreak(final Block block, final User player, final String username, final IEssentials ess) throws SignException, MaxMoneyException {
        return true;
    }

    public Set<Material> getBlocks() {
        return EMPTY_SET;
    }

    public boolean areHeavyEventRequired() {
        return false;
    }

    private String getSignText(final ISign sign, final int lineNumber) {
        return sign.getLine(lineNumber).trim();
    }

    protected final void validateTrade(final ISign sign, final int index, final IEssentials ess) throws SignException {
        final String line = getSignText(sign, index);
        if (line.isEmpty()) {
            return;
        }
        final Trade trade = getTrade(sign, index, 0, ess);
        final BigDecimal money = trade.getMoney();
        if (money != null) {
            sign.setLine(index, NumberUtil.shortCurrency(money, ess));
        }
    }

    protected final void validateTrade(final ISign sign, final int amountIndex, final int itemIndex, final User player, final IEssentials ess) throws SignException {
        final String itemType = getSignText(sign, itemIndex);
        if (itemType.equalsIgnoreCase("exp") || itemType.equalsIgnoreCase("xp")) {
            final int amount = getIntegerPositive(getSignText(sign, amountIndex));
            sign.setLine(amountIndex, Integer.toString(amount));
            sign.setLine(itemIndex, "exp");
            return;
        }
        final Trade trade = getTrade(sign, amountIndex, itemIndex, player, ess);
        final ItemStack item = trade.getItemStack();
        sign.setLine(amountIndex, Integer.toString(item.getAmount()));
        sign.setLine(itemIndex, itemType);
    }

    protected final Trade getTrade(final ISign sign, final int amountIndex, final int itemIndex, final User player, final IEssentials ess) throws SignException {
        return getTrade(sign, amountIndex, itemIndex, player, false, ess);
    }

    protected final Trade getTrade(final ISign sign, final int amountIndex, final int itemIndex, final User player, final boolean allowId, final IEssentials ess) throws SignException {
        final String itemType = getSignText(sign, itemIndex);
        if (itemType.equalsIgnoreCase("exp") || itemType.equalsIgnoreCase("xp")) {
            final int amount = getIntegerPositive(getSignText(sign, amountIndex));
            return new Trade(amount, ess);
        }
        final ItemStack item = getItemStack(itemType, 1, allowId, ess);
        final int amount = Math.min(getIntegerPositive(getSignText(sign, amountIndex)), item.getType().getMaxStackSize() * player.getBase().getInventory().getSize());
        if (item.getType() == Material.AIR || amount < 1) {
            throw new SignException("moreThanZero");
        }
        item.setAmount(amount);
        return new Trade(item, ess);
    }

    protected final void validateInteger(final ISign sign, final int index) throws SignException {
        final String line = getSignText(sign, index);
        if (line.isEmpty()) {
            throw new SignException("emptySignLine", index + 1);
        }
        final int quantity = getIntegerPositive(line);
        sign.setLine(index, Integer.toString(quantity));
    }

    protected final int getIntegerPositive(final String line) throws SignException {
        final int quantity = getInteger(line);
        if (quantity < 1) {
            throw new SignException("moreThanZero");
        }
        return quantity;
    }

    protected final int getInteger(final String line) throws SignException {
        try {
            return Integer.parseInt(line);
        } catch (final NumberFormatException ex) {
            throw new SignException("invalidSign");
        }
    }

    protected final ItemStack getItemStack(final String itemName, final int quantity, final IEssentials ess) throws SignException {
        return getItemStack(itemName, quantity, false, ess);
    }

    protected final ItemStack getItemStack(final String itemName, final int quantity, final boolean allowId, final IEssentials ess) throws SignException {
        if (allowId && ess.getSettings().allowOldIdSigns()) {
            final Material newMaterial = ess.getItemDb().getFromLegacy(itemName);
            if (newMaterial != null) {
                return new ItemStack(newMaterial, quantity);
            }
        }

        try {
            final ItemStack item = ess.getItemDb().get(itemName);
            item.setAmount(quantity);
            return item;
        } catch (final Exception ex) {
            throw new SignException(ex, "errorWithMessage", ex.getMessage());
        }
    }

    protected final ItemStack getItemMeta(final ItemStack item, final String meta, final IEssentials ess) throws SignException {
        return this.getItemMeta(null, item, meta, ess);
    }

    protected final ItemStack getItemMeta(final CommandSource source, final ItemStack item, final String meta, final IEssentials ess) throws SignException {
        ItemStack stack = item;
        try {
            if (!meta.isEmpty()) {
                final MetaItemStack metaStack = new MetaItemStack(stack);
                final boolean allowUnsafe = ess.getSettings().allowUnsafeEnchantments();
                metaStack.addStringMeta(source, allowUnsafe, meta, ess);
                stack = metaStack.getItemStack();
            }
        } catch (final Exception ex) {
            throw new SignException(ex, "errorWithMessage", ex.getMessage());
        }
        return stack;
    }

    protected final BigDecimal getMoney(final String line, final IEssentials ess) throws SignException {
        final boolean isMoney = line.matches("^[^0-9-.]?[.0-9]+[^0-9-.]?$");
        return isMoney ? getBigDecimalPositive(line, ess) : null;
    }

    protected final BigDecimal getBigDecimalPositive(final String line, final IEssentials ess) throws SignException {
        final BigDecimal quantity = getBigDecimal(line, ess);
        if (quantity.compareTo(MINTRANSACTION) < 0) {
            throw new SignException("moreThanZero");
        }
        return quantity;
    }

    protected final BigDecimal getBigDecimal(final String line, final IEssentials ess) throws SignException {
        try {
            return new BigDecimal(NumberUtil.sanitizeCurrencyString(line, ess));
        } catch (final ArithmeticException | NumberFormatException ex) {
            throw new SignException(ex, "errorWithMessage", ex.getMessage());
        }
    }

    protected final Trade getTrade(final ISign sign, final int index, final IEssentials ess) throws SignException {
        return getTrade(sign, index, 1, ess);
    }

    protected final Trade getTrade(final ISign sign, final int index, final int decrement, final IEssentials ess) throws SignException {
        return getTrade(sign, index, decrement, false, ess);
    }

    protected final Trade getTrade(final ISign sign, final int index, final int decrement, final boolean allowId, final IEssentials ess) throws SignException {
        final String line = getSignText(sign, index);
        if (line.isEmpty()) {
            return new Trade(signName.toLowerCase(Locale.ENGLISH) + "sign", ess);
        }

        final BigDecimal money = getMoney(line, ess);
        if (money == null) {
            final String[] split = line.split("[ :]+", 2);
            if (split.length != 2) {
                throw new SignException("invalidCharge");
            }
            final int quantity = getIntegerPositive(split[0]);

            final String item = split[1].toLowerCase(Locale.ENGLISH);
            if (item.equalsIgnoreCase("times")) {
                sign.setLine(index, (quantity - decrement) + " times");
                sign.updateSign();
                return new Trade(signName.toLowerCase(Locale.ENGLISH) + "sign", ess);
            } else if (item.equalsIgnoreCase("exp") || item.equalsIgnoreCase("xp")) {
                sign.setLine(index, quantity + " exp");
                return new Trade(quantity, ess);
            } else {
                final ItemStack stack = getItemStack(item, quantity, allowId, ess);
                sign.setLine(index, quantity + " " + item);
                return new Trade(stack, ess);
            }
        } else {
            return new Trade(money, ess);
        }
    }

    private void showError(final IEssentials ess, final CommandSource sender, final Throwable exception, final String signName) {
        ess.showError(sender, exception, "\\ sign: " + signName);
    }

    public interface ISign {
        String getLine(final int index);

        void setLine(final int index, final String text);

        Block getBlock();

        void updateSign();
    }

    static class EventSign implements ISign {
        private final transient SignChangeEvent event;
        private final transient Block block;
        private transient Sign sign;

        EventSign(final SignChangeEvent event) {
            this.event = event;
            this.block = event.getBlock();
            this.sign = (Sign) block.getState();
        }

        @Override
        public final String getLine(final int index) {
            final StringBuilder builder = new StringBuilder();
            for (final char c : event.getLine(index).toCharArray()) {
                if (c < 0xF700 || c > 0xF747) {
                    builder.append(c);
                }
            }
            return builder.toString();
            //return event.getLine(index); // Above code can be removed and replaced with this line when https://github.com/Bukkit/Bukkit/pull/982 is merged.
        }

        @Override
        public final void setLine(final int index, final String text) {
            event.setLine(index, text);
            sign.setLine(index, text);
            updateSign();
        }

        @Override
        public Block getBlock() {
            return block;
        }

        @Override
        public void updateSign() {
            sign.update();
            sign = (Sign) block.getState();
        }
    }

    static class BlockSign implements ISign {
        private final transient Block block;

        BlockSign(final Block block) {
            this.block = block;
        }

        @Override
        public final String getLine(final int index) {
            final StringBuilder builder = new StringBuilder();
            for (final char c : getSign().getLine(index).toCharArray()) {
                if (c < 0xF700 || c > 0xF747) {
                    builder.append(c);
                }
            }
            return builder.toString();
            //return event.getLine(index); // Above code can be removed and replaced with this line when https://github.com/Bukkit/Bukkit/pull/982 is merged.
        }

        @Override
        public final void setLine(final int index, final String text) {
            getSign().setLine(index, text);
            updateSign();
        }

        @Override
        public final Block getBlock() {
            return block;
        }

        private Sign getSign() {
            // Starting in 1.19, the block state is no longer persistent I guess? We must get it every time to prevent
            // sign updates from not taking effect.
            return (Sign) block.getState();
        }

        @Override
        public final void updateSign() {
            getSign().update();
        }
    }
}
