001/*
002 * This file is part of Baritone.
003 *
004 * Baritone is free software: you can redistribute it and/or modify
005 * it under the terms of the GNU Lesser General Public License as published by
006 * the Free Software Foundation, either version 3 of the License, or
007 * (at your option) any later version.
008 *
009 * Baritone is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012 * GNU Lesser General Public License for more details.
013 *
014 * You should have received a copy of the GNU Lesser General Public License
015 * along with Baritone.  If not, see <https://www.gnu.org/licenses/>.
016 */
017
018package baritone.api.command.datatypes;
019
020import baritone.api.command.exception.CommandException;
021import baritone.api.command.helpers.TabCompleteHelper;
022import baritone.api.utils.BlockOptionalMeta;
023import net.minecraft.core.registries.BuiltInRegistries;
024import net.minecraft.resources.ResourceLocation;
025import net.minecraft.world.level.block.Block;
026import net.minecraft.world.level.block.state.properties.Property;
027
028import java.util.Set;
029import java.util.regex.Pattern;
030import java.util.stream.Collectors;
031import java.util.stream.Stream;
032
033public enum ForBlockOptionalMeta implements IDatatypeFor<BlockOptionalMeta> {
034    INSTANCE;
035
036    /**
037     * Matches (domain:)?name([(property=value)*])? but the input can be truncated at any position.
038     * domain and name are [a-z0-9_.-]+ and [a-z0-9/_.-]+ because that's what mc 1.13+ accepts.
039     * property and value use the same format as domain.
040     */
041    // Good luck reading this.
042    private static Pattern PATTERN = Pattern.compile("(?:[a-z0-9_.-]+:)?(?:[a-z0-9/_.-]+(?:\\[(?:(?:[a-z0-9_.-]+=[a-z0-9_.-]+,)*(?:[a-z0-9_.-]+(?:=(?:[a-z0-9_.-]+(?:\\])?)?)?)?|\\])?)?)?");
043
044    @Override
045    public BlockOptionalMeta get(IDatatypeContext ctx) throws CommandException {
046        return new BlockOptionalMeta(ctx.getConsumer().getString());
047    }
048
049    @Override
050    public Stream<String> tabComplete(IDatatypeContext ctx) throws CommandException {
051        String arg = ctx.getConsumer().peekString();
052
053        if (!PATTERN.matcher(arg).matches()) {
054            // Invalid format; we can't complete this.
055            ctx.getConsumer().getString();
056            return Stream.empty();
057        }
058
059        if (arg.endsWith("]")) {
060            // We are already done.
061            ctx.getConsumer().getString();
062            return Stream.empty();
063        }
064
065        if (!arg.contains("[")) {
066            // no properties so we are completing the block id
067            return ctx.getConsumer().tabCompleteDatatype(BlockById.INSTANCE);
068        }
069
070        ctx.getConsumer().getString();
071
072        // destructuring assignment? Please?
073        String blockId, properties;
074        {
075            String[] parts = splitLast(arg, '[');
076            blockId = parts[0];
077            properties = parts[1];
078        }
079
080        Block block = BuiltInRegistries.BLOCK.getOptional(ResourceLocation.parse(blockId)).orElse(null);
081        if (block == null) {
082            // This block doesn't exist so there's no properties to complete.
083            return Stream.empty();
084        }
085
086        String leadingProperties, lastProperty;
087        {
088            String[] parts = splitLast(properties, ',');
089            leadingProperties = parts[0];
090            lastProperty = parts[1];
091        }
092
093        if (!lastProperty.contains("=")) {
094            // The last property-value pair doesn't have a value yet so we are completing its name
095            Set<String> usedProps = Stream.of(leadingProperties.split(","))
096                    .map(pair -> pair.split("=")[0])
097                    .collect(Collectors.toSet());
098
099            String prefix = arg.substring(0, arg.length() - lastProperty.length());
100            return new TabCompleteHelper()
101                    .append(
102                            block.getStateDefinition()
103                                    .getProperties()
104                                    .stream()
105                                    .map(Property::getName)
106                    )
107                    .filter(prop -> !usedProps.contains(prop))
108                    .filterPrefix(lastProperty)
109                    .sortAlphabetically()
110                    .map(prop -> prefix + prop)
111                    .stream();
112        }
113
114        String lastName, lastValue;
115        {
116            String[] parts = splitLast(lastProperty, '=');
117            lastName = parts[0];
118            lastValue = parts[1];
119        }
120
121        // We are completing the value of a property
122        String prefix = arg.substring(0, arg.length() - lastValue.length());
123
124        Property<?> property = block.getStateDefinition().getProperty(lastName);
125        if (property == null) {
126            // The property does not exist so there's no values to complete
127            return Stream.empty();
128        }
129
130        return new TabCompleteHelper()
131                .append(getValues(property))
132                .filterPrefix(lastValue)
133                .sortAlphabetically()
134                .map(val -> prefix + val)
135                .stream();
136    }
137
138    /**
139     * Always returns exactly two strings.
140     * If the separator is not found the FIRST returned string is empty.
141     */
142    private static String[] splitLast(String string, char chr) {
143        int idx = string.lastIndexOf(chr);
144        if (idx == -1) {
145            return new String[]{"", string};
146        }
147        return new String[]{string.substring(0, idx), string.substring(idx + 1)};
148    }
149
150    // this shouldn't need to be a separate method?
151    private static <T extends Comparable<T>> Stream<String> getValues(Property<T> property) {
152        return property.getPossibleValues().stream().map(property::getName);
153    }
154}