/*
 * WorldEdit, a Minecraft world manipulation toolkit
 * Copyright (C) sk89q <http://www.sk89q.com>
 * Copyright (C) WorldEdit team and contributors
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package com.sk89q.worldedit.util;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
import com.sk89q.worldedit.EditSession;
import com.sk89q.worldedit.MaxChangedBlocksException;
import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldedit.world.block.BlockState;
import com.sk89q.worldedit.world.block.BlockStateHolder;
import com.sk89q.worldedit.world.block.BlockTypes;

import javax.annotation.Nullable;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Random;
import java.util.Set;

/**
 * Tree generator.
 */
public final class TreeGenerator {

    public enum TreeType {
        TREE("Oak tree", "oak", "tree", "regular"),
        BIG_TREE("Large oak tree", "largeoak", "bigoak", "big", "bigtree"),
        REDWOOD("Spruce tree", "spruce", "redwood", "sequoia", "sequoioideae"),
        TALL_REDWOOD("Tall spruce tree", "tallspruce", "bigspruce", "tallredwood", "tallsequoia", "tallsequoioideae"),
        MEGA_REDWOOD("Large spruce tree", "largespruce", "megaredwood"),
        RANDOM_REDWOOD("Random spruce tree", "randspruce", "randredwood", "randomredwood", "anyredwood") {
            @Override
            public boolean generate(EditSession editSession, BlockVector3 pos) throws MaxChangedBlocksException {
                TreeType[] choices = {REDWOOD, TALL_REDWOOD, MEGA_REDWOOD};
                return choices[TreeGenerator.RANDOM.nextInt(choices.length)].generate(editSession, pos);
            }
        },
        BIRCH("Birch tree", "birch", "white", "whitebark"),
        TALL_BIRCH("Tall birch tree", "tallbirch"),
        RANDOM_BIRCH("Random birch tree", "randbirch", "randombirch") {
            @Override
            public boolean generate(EditSession editSession, BlockVector3 pos) throws MaxChangedBlocksException {
                TreeType[] choices = {BIRCH, TALL_BIRCH};
                return choices[TreeGenerator.RANDOM.nextInt(choices.length)].generate(editSession, pos);
            }
        },
        JUNGLE("Jungle tree", "jungle"),
        SMALL_JUNGLE("Small jungle tree", "smalljungle"),
        SHORT_JUNGLE("Short jungle tree", "shortjungle") {
            @Override
            public boolean generate(EditSession editSession, BlockVector3 pos) throws MaxChangedBlocksException {
                return SMALL_JUNGLE.generate(editSession, pos);
            }
        },
        RANDOM_JUNGLE("Random jungle tree", "randjungle", "randomjungle") {
            @Override
            public boolean generate(EditSession editSession, BlockVector3 pos) throws MaxChangedBlocksException {
                TreeType[] choices = {JUNGLE, SMALL_JUNGLE};
                return choices[TreeGenerator.RANDOM.nextInt(choices.length)].generate(editSession, pos);
            }
        },
        JUNGLE_BUSH("Jungle bush", "junglebush", "jungleshrub"),
        RED_MUSHROOM("Red mushroom", "redmushroom", "redgiantmushroom"),
        BROWN_MUSHROOM("Brown mushroom", "brownmushroom", "browngiantmushroom"),
        CRIMSON_FUNGUS("Crimson fungus", "crimsonfungus", "rednethermushroom"),
        WARPED_FUNGUS("Warped fungus", "warpedfungus", "greennethermushroom"),
        RANDOM_MUSHROOM("Random mushroom", "randmushroom", "randommushroom") {
            @Override
            public boolean generate(EditSession editSession, BlockVector3 pos) throws MaxChangedBlocksException {
                TreeType[] choices = {RED_MUSHROOM, BROWN_MUSHROOM, CRIMSON_FUNGUS, WARPED_FUNGUS};
                return choices[TreeGenerator.RANDOM.nextInt(choices.length)].generate(editSession, pos);
            }
        },
        SWAMP("Swamp tree", "swamp", "swamptree"),
        ACACIA("Acacia tree", "acacia"),
        AZALEA("Azalea tree", "azalea"),
        DARK_OAK("Dark oak tree", "darkoak"),
        PINE("Pine tree", "pine") {
            @Override
            public boolean generate(EditSession editSession, BlockVector3 pos) throws MaxChangedBlocksException {
                makePineTree(editSession, pos);
                return true;
            }
        },
        CHORUS_PLANT("Chorus plant", "chorusplant") {
            @Override
            public boolean generate(EditSession editSession, BlockVector3 pos) throws MaxChangedBlocksException {
                // chorus plants have to generate starting in the end stone itself, not the air above the ground
                return editSession.getWorld().generateTree(this, editSession, pos.subtract(0, 1, 0));
            }
        },
        RANDOM("Random tree", "rand", "random") {
            @Override
            public boolean generate(EditSession editSession, BlockVector3 pos) throws MaxChangedBlocksException {
                TreeType[] choices = TreeType.values();
                return choices[TreeGenerator.RANDOM.nextInt(choices.length)].generate(editSession, pos);
            }
        };

        /**
         * Stores a map of the names for fast access.
         */
        private static final Map<String, TreeType> lookup = new HashMap<>();
        private static final Set<String> primaryAliases = Sets.newHashSet();

        private final String name;
        public final ImmutableList<String> lookupKeys;

        static {
            for (TreeType type : EnumSet.allOf(TreeType.class)) {
                for (String key : type.lookupKeys) {
                    lookup.put(key, type);
                }
                if (type.lookupKeys.size() > 0) {
                    primaryAliases.add(type.lookupKeys.get(0));
                }
            }
        }

        TreeType(String name, String... lookupKeys) {
            this.name = name;
            this.lookupKeys = ImmutableList.copyOf(lookupKeys);
        }

        public static Set<String> getAliases() {
            return Collections.unmodifiableSet(lookup.keySet());
        }

        public static Set<String> getPrimaryAliases() {
            return Collections.unmodifiableSet(primaryAliases);
        }

        public boolean generate(EditSession editSession, BlockVector3 pos) throws MaxChangedBlocksException {
            return editSession.getWorld().generateTree(this, editSession, pos);
        }

        /**
         * Get user-friendly tree type name.
         *
         * @return a name
         */
        public String getName() {
            return name;
        }

        /**
         * Return type from name. May return null.
         *
         * @param name name to search
         * @return a tree type or null
         */
        @Nullable
        public static TreeType lookup(String name) {
            return lookup.get(name.toLowerCase(Locale.ROOT));
        }
    }

    private TreeGenerator() {
    }

    private static final Random RANDOM = new Random();

    /**
     * Makes a terrible looking pine tree.
     *
     * @param basePosition the base position
     */
    private static void makePineTree(EditSession editSession, BlockVector3 basePosition)
            throws MaxChangedBlocksException {
        int trunkHeight = (int) Math.floor(Math.random() * 2) + 3;
        int height = (int) Math.floor(Math.random() * 5) + 8;

        BlockState logBlock = BlockTypes.OAK_LOG.getDefaultState();
        BlockState leavesBlock = BlockTypes.OAK_LEAVES.getDefaultState();

        // Create trunk
        for (int i = 0; i < trunkHeight; ++i) {
            if (!setBlockIfAir(editSession, basePosition.add(0, i, 0), logBlock)) {
                return;
            }
        }

        // Move up
        basePosition = basePosition.add(0, trunkHeight, 0);

        // Create tree + leaves
        for (int i = 0; i < height; ++i) {
            setBlockIfAir(editSession, basePosition.add(0, i, 0), logBlock);

            // Less leaves at these levels
            double chance = ((i == 0 || i == height - 1) ? 0.6 : 1);

            // Inner leaves
            setChanceBlockIfAir(editSession, basePosition.add(-1, i, 0), leavesBlock, chance);
            setChanceBlockIfAir(editSession, basePosition.add(1, i, 0), leavesBlock, chance);
            setChanceBlockIfAir(editSession, basePosition.add(0, i, -1), leavesBlock, chance);
            setChanceBlockIfAir(editSession, basePosition.add(0, i, 1), leavesBlock, chance);
            setChanceBlockIfAir(editSession, basePosition.add(1, i, 1), leavesBlock, chance);
            setChanceBlockIfAir(editSession, basePosition.add(-1, i, 1), leavesBlock, chance);
            setChanceBlockIfAir(editSession, basePosition.add(1, i, -1), leavesBlock, chance);
            setChanceBlockIfAir(editSession, basePosition.add(-1, i, -1), leavesBlock, chance);

            if (!(i == 0 || i == height - 1)) {
                for (int j = -2; j <= 2; ++j) {
                    setChanceBlockIfAir(editSession, basePosition.add(-2, i, j), leavesBlock, 0.6);
                }
                for (int j = -2; j <= 2; ++j) {
                    setChanceBlockIfAir(editSession, basePosition.add(2, i, j), leavesBlock, 0.6);
                }
                for (int j = -2; j <= 2; ++j) {
                    setChanceBlockIfAir(editSession, basePosition.add(j, i, -2), leavesBlock, 0.6);
                }
                for (int j = -2; j <= 2; ++j) {
                    setChanceBlockIfAir(editSession, basePosition.add(j, i, 2), leavesBlock, 0.6);
                }
            }
        }

        setBlockIfAir(editSession, basePosition.add(0, height, 0), leavesBlock);
    }

    /**
     * Looks up a tree type. May return null if a tree type by that
     * name is not found.
     *
     * @param type the tree type
     * @return a tree type or null
     */
    @Nullable
    public static TreeType lookup(String type) {
        return TreeType.lookup(type);
    }

    /**
     * Set a block (only if a previous block was not there) if {@link Math#random()}
     * returns a number less than the given probability.
     *
     * @param position    the position
     * @param block       the block
     * @param probability a probability between 0 and 1, inclusive
     * @return whether a block was changed
     * @throws MaxChangedBlocksException thrown if too many blocks are changed
     */
    private static <B extends BlockStateHolder<B>> boolean setChanceBlockIfAir(
            EditSession session,
            BlockVector3 position,
            B block,
            double probability
    )
            throws MaxChangedBlocksException {
        return Math.random() <= probability && setBlockIfAir(session, position, block);
    }

    /**
     * Set a block only if there's no block already there.
     *
     * @param position the position
     * @param block    the block to set
     * @return if block was changed
     * @throws MaxChangedBlocksException thrown if too many blocks are changed
     */
    private static <B extends BlockStateHolder<B>> boolean setBlockIfAir(
            EditSession session,
            BlockVector3 position,
            B block
    ) throws MaxChangedBlocksException {
        return session.getBlock(position).getBlockType().getMaterial().isAir() && session.setBlock(position, block);
    }

}
