001/*
002 * PlotSquared, a land and world management plugin for Minecraft.
003 * Copyright (C) IntellectualSites <https://intellectualsites.com>
004 * Copyright (C) IntellectualSites team and contributors
005 *
006 * This program is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 *
011 * This program is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014 * GNU General Public License for more details.
015 *
016 * You should have received a copy of the GNU General Public License
017 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
018 */
019package com.plotsquared.core.plot;
020
021import com.plotsquared.core.location.Direction;
022import org.checkerframework.checker.nullness.qual.NonNull;
023import org.checkerframework.checker.nullness.qual.Nullable;
024
025import java.util.Iterator;
026import java.util.NoSuchElementException;
027
028/**
029 * The PlotId class represents a Plot's x and y coordinates within a {@link PlotArea}. PlotId x,y values do not correspond to Block locations.
030 * A PlotId instance can be created using the {@link #of(int, int)} method or parsed from a string using the {@link #fromString(String)} method.
031 */
032public final class PlotId {
033
034    private final int x;
035    private final int y;
036    private final int hash;
037
038    /**
039     * Constructs a new PlotId with the given x and y coordinates.
040     *
041     * @param x the x-coordinate of the plot
042     * @param y the y-coordinate of the plot
043     */
044    private PlotId(final int x, final int y) {
045        this.x = x;
046        this.y = y;
047        this.hash = (this.getX() << 16) | (this.getY() & 0xFFFF);
048    }
049
050    /**
051     * Returns a new PlotId instance with the specified x and y coordinates.
052     *
053     * @param x the x-coordinate of the plot
054     * @param y the y-coordinate of the plot
055     * @return a new PlotId instance with the specified x and y coordinates
056     */
057    public static @NonNull PlotId of(final int x, final int y) {
058        return new PlotId(x, y);
059    }
060
061    /**
062     * Get a Plot Id based on a string
063     *
064     * @param string to create id from
065     * @return the PlotId representation of the argument
066     * @throws IllegalArgumentException if the string does not contain a valid PlotId
067     */
068    public static @NonNull PlotId fromString(final @NonNull String string) {
069        final PlotId plot = fromStringOrNull(string);
070        if (plot == null) {
071            throw new IllegalArgumentException("Cannot create PlotID. String invalid.");
072        }
073        return plot;
074    }
075
076    /**
077     * Returns a PlotId object from the given string, or null if the string is invalid.
078     * The string should be in the format "x;y" where x and y are integers.
079     * The string can also contain any combination of the characters ";_,."
080     * as delimiters.
081     *
082     * @param string the string to parse
083     * @return a PlotId object parsed from the given string, or null if the string is invalid
084     */
085    public static @Nullable PlotId fromStringOrNull(final @NonNull String string) {
086        final String[] parts = string.split("[;_,.]");
087        if (parts.length < 2) {
088            return null;
089        }
090        int x;
091        int y;
092        try {
093            x = Integer.parseInt(parts[0]);
094            y = Integer.parseInt(parts[1]);
095        } catch (final NumberFormatException ignored) {
096            return null;
097        }
098        return of(x, y);
099    }
100
101
102    /**
103     * Returns a new PlotId instance from the given hash.
104     *
105     * @param hash the hash to unpair
106     * @return a new PlotId instance
107     */
108    public static @NonNull PlotId unpair(final int hash) {
109        return PlotId.of(hash >> 16, hash & 0xFFFF);
110    }
111
112    /**
113     * Returns the x-coordinate of this Plot ID.
114     *
115     * @return the x-coordinate of this Plot ID
116     */
117    public int getX() {
118        return this.x;
119    }
120
121    /**
122     * Returns the y-coordinate of this Plot ID.
123     *
124     * @return the y-coordinate of this Plot ID
125     */
126    public int getY() {
127        return this.y;
128    }
129
130    /**
131     * Returns the next Plot ID for claiming purposes based on the current Plot ID.
132     *
133     * @return the next Plot ID
134     */
135    public @NonNull PlotId getNextId() {
136        final int absX = Math.abs(x);
137        final int absY = Math.abs(y);
138        if (absX > absY) {
139            if (x > 0) {
140                return PlotId.of(x, y + 1);
141            } else {
142                return PlotId.of(x, y - 1);
143            }
144        } else if (absY > absX) {
145            if (y > 0) {
146                return PlotId.of(x - 1, y);
147            } else {
148                return PlotId.of(x + 1, y);
149            }
150        } else {
151            if (x == y && x > 0) {
152                return PlotId.of(x, y + 1);
153            }
154            if (x == absX) {
155                return PlotId.of(x, y + 1);
156            }
157            if (y == absY) {
158                return PlotId.of(x, y - 1);
159            }
160            return PlotId.of(x + 1, y);
161        }
162    }
163
164    /**
165     * Returns a new Plot ID in the specified relative direction based on the
166     * current Plot ID.
167     *
168     * @param direction the direction in which to get the relative Plot ID
169     * @return the relative Plot ID
170     */
171    public @NonNull PlotId getRelative(final @NonNull Direction direction) {
172        return switch (direction) {
173            case NORTH -> PlotId.of(this.getX(), this.getY() - 1);
174            case EAST -> PlotId.of(this.getX() + 1, this.getY());
175            case SOUTH -> PlotId.of(this.getX(), this.getY() + 1);
176            case WEST -> PlotId.of(this.getX() - 1, this.getY());
177            default -> this;
178        };
179    }
180
181    @Override
182    public boolean equals(final Object obj) {
183        if (this == obj) {
184            return true;
185        }
186        if (obj == null) {
187            return false;
188        }
189        if (this.hashCode() != obj.hashCode()) {
190            return false;
191        }
192        if (getClass() != obj.getClass()) {
193            return false;
194        }
195        final PlotId other = (PlotId) obj;
196        return this.getX() == other.getX() && this.getY() == other.getY();
197    }
198
199    /**
200     * Returns a string representation of this Plot ID in the format "x;y".
201     *
202     * <p> The format is {@code x + ";" + y}
203     *
204     * @return a string representation of this Plot ID
205     */
206    @Override
207    public @NonNull String toString() {
208        return this.getX() + ";" + this.getY();
209    }
210
211    /**
212     * Returns a string representation of this Plot ID with the specified separator.
213     * <p>
214     * The format is {@code x + separator + y}
215     *
216     * @param separator the separator to use between the X and Y coordinates
217     * @return a string representation of this Plot ID with the specified separator
218     */
219    public @NonNull String toSeparatedString(String separator) {
220        return this.getX() + separator + this.getY();
221    }
222
223    /**
224     * Returns a string representation of this Plot ID in the format "x,y".
225     *
226     * @return a string representation of this Plot ID
227     */
228    public @NonNull String toCommaSeparatedString() {
229        return this.getX() + "," + this.getY();
230    }
231
232    /**
233     * Returns a string representation of this Plot ID in the format "x_y".
234     *
235     * @return a string representation of this Plot ID
236     */
237
238    public @NonNull String toUnderscoreSeparatedString() {
239        return this.getX() + "_" + this.getY();
240    }
241
242    /**
243     * Returns a string representation of this Plot ID in the format "x-y".
244     *
245     * @return a string representation of this Plot ID
246     */
247    public @NonNull String toDashSeparatedString() {
248        return this.getX() + "-" + this.getY();
249    }
250
251    @Override
252    public int hashCode() {
253        return this.hash;
254    }
255
256
257    /**
258     * An iterator that iterates over a range of {@link PlotId}s.
259     * The range is defined by a start and end {@link PlotId}.
260     */
261    public static final class PlotRangeIterator implements Iterator<PlotId>, Iterable<PlotId> {
262
263        private final PlotId start;
264        private final PlotId end;
265
266        private int x;
267        private int y;
268
269        private PlotRangeIterator(final @NonNull PlotId start, final @NonNull PlotId end) {
270            this.start = start;
271            this.end = end;
272            this.x = this.start.getX();
273            this.y = this.start.getY();
274        }
275
276        /**
277         * Returns a new {@link PlotRangeIterator} that iterates over the range of Plots between the specified start and end Plots (inclusive).
278         *
279         * @param start the starting Plot of the range
280         * @param end the ending Plot of the range
281         * @return a new {@link PlotRangeIterator} that iterates over the range of Plots between the specified start and end Plots (inclusive)
282         */
283        public static PlotRangeIterator range(final @NonNull PlotId start, final @NonNull PlotId end) {
284            return new PlotRangeIterator(start, end);
285        }
286
287        @Override
288        public boolean hasNext() {
289            // end is fully included
290            return this.x <= this.end.getX() && this.y <= this.end.getY();
291        }
292
293        @Override
294        public PlotId next() {
295            if (!hasNext()) {
296                throw new NoSuchElementException("The iterator has no more entries");
297            }
298            // increment *after* getting the result to include the minimum
299            // the id to return
300            PlotId result = PlotId.of(this.x, this.y);
301            // first increase y, then x
302            if (this.y == this.end.getY()) {
303                this.x++;
304                this.y = this.start.getY();
305            } else {
306                this.y++;
307            }
308            return result;
309        }
310
311        @NonNull
312        @Override
313        public Iterator<PlotId> iterator() {
314            return this;
315        }
316
317    }
318
319}