001package squidpony;
002
003import regexodus.Matcher;
004import regexodus.Pattern;
005import squidpony.squidmath.CrossHash;
006import squidpony.squidmath.K2;
007
008import java.util.Objects;
009
010/**
011 * Used to standardize conversion for a given type, {@code T}, to and from a serialized String format.
012 * This abstract class should usually be made concrete by a single-purpose class (not the type T itself).
013 * Also includes a static registry of types as CharSequence arrays (including the classes of generic type parameters in
014 * array elements after the first) to the  corresponding StringConvert objects that have been constructed for those
015 * types, although the registry must store the StringConvert objects without any further types (you can cast the
016 * StringConvert to a StringConvert with the desired generic type, or call {@link #restore(String)} on the
017 * un-parametrized type and get back an Object that can be cast to the correct type, but we aren't able to store the
018 * actual type). The static method {@link #lookup(CharSequence[])} can be used to find the StringConvert registered for
019 * a type name combination. The static method {@link #get(CharSequence)} can be used to find a StringConvert by its
020 * name. The static utility method {@link #asArray(CharSequence[])} can be used to reduce the amount of arrays produced
021 * by varargs, especially when you have a bunch of Class items and need Strings, but the array it returns must not be
022 * edited once used to construct a StringConvert.
023 */
024public abstract class StringConvert<T> {
025    public final CharSequence name;
026    public final CharSequence[] typeNames;
027    public final String specificName;
028    public final boolean isArray;
029    private static final Matcher specificMatcher = Pattern.compile("\\p{Js}\\p{Jp}*").matcher();
030    /**
031     * Constructs a StringConvert using a vararg or array of CharSequence objects, such as Strings, as well as a boolean
032     * flag to determine if the StringConvert works on an array instead of a normal object. If an array of types is
033     * passed, it must not be altered after usage. If no varargs are passed, if types is null, or if the first item of
034     * types is null, then this uses a special type representation where the name is "void" and typeNames has "void" as
035     * its only element. If types has length 1, then the name will be the "simple name" of the first element in types,
036     * as produced by {@link Class#getSimpleName()} (note that this produces an empty string for anonymous classes), and
037     * typeNames will again have that simple name as its only value. Otherwise, this considers items after the first to
038     * be the names of generic type arguments of the first, using normal Java syntax of {@code "Outer<A,B>"} if given
039     * the Strings for types {@code "Outer", "A", "B"}. No spaces will be present in the name, but thanks to some
040     * customization of the registry, you can give a String with spaces in it to {@link #get(CharSequence)} and still
041     * find the correct one). You can give type names with generic components as the names of generic type arguments,
042     * such as {@code new StringConvert("OrderedMap", "String", "OrderedSet<String>")} for a mapping of String keys to
043     * values that are themselves sets of Strings. After constructing a StringConvert, it is automatically registered
044     * so it can be looked up by name with {@link #get(CharSequence)} or by component generic types with
045     * {@link #lookup(CharSequence...)}; both of these will not return a StringConvert with type info for what it
046     * takes and returns beyond "Object", but the result can be cast to a StringConvert with the correct type.
047     * @param isArray true if this should convert an array type as opposed to a normal object or primitive type
048     * @param types a vararg of Class objects representing the type this can convert, including generic type parameters
049     *              of the first element, if there are any, at positions after the first
050     */
051    public StringConvert(final boolean isArray, final CharSequence... types) {
052        this.isArray = isArray;
053        if (types == null || types.length <= 0 || types[0] == null) {
054            name = "void";
055            typeNames = new String[]{"void"};
056            specificName = "void";
057        } else if (types.length == 1) {
058            name = types[0];
059            typeNames = types;
060            specificMatcher.setTarget(name);
061            if(specificMatcher.find())
062                specificName = specificMatcher.group();
063            else
064                specificName = "void";
065        } else {
066            name = new StringBuilder(64);
067            ((StringBuilder) name).append(types[0]).append('<').append(types[1]);
068            for (int i = 2; i < types.length; i++) {
069                ((StringBuilder) name).append(',').append(types[i]);
070            }
071            ((StringBuilder) name).append('>');
072            typeNames = types;
073            specificMatcher.setTarget(name);
074            if(specificMatcher.find())
075                specificName = specificMatcher.group();
076            else
077                specificName = "void";
078
079
080        }
081    }
082    /**
083     * Constructs a StringConvert using a vararg or array of CharSequence objects, such as Strings. If an array is
084     * passed, it must not be altered after usage. If no arguments are passed, if types is null, or if the first item of
085     * types is null, then this uses a special type representation where the name is "void" and typeNames has "void" as
086     * its only element. If types has length 1, then the name will be the "simple name" of the first element in types,
087     * as produced by {@link Class#getSimpleName()} (note that this produces an empty string for anonymous classes), and
088     * typeNames will again have that simple name as its only value. Otherwise, this considers items after the first to
089     * be the names of generic type arguments of the first, using normal Java syntax of {@code "Outer<A,B>"} if given
090     * the Strings for types {@code "Outer", "A", "B"}. No spaces will be present in the name, but thanks to some
091     * customization of the registry, you can give a String with spaces in it to {@link #get(CharSequence)} and still
092     * find the correct one). You can give type names with generic components as the names of generic type arguments,
093     * such as {@code new StringConvert("OrderedMap", "String", "OrderedSet<String>")} for a mapping of String keys to
094     * values that are themselves sets of Strings. After constructing a StringConvert, it is automatically registered
095     * so it can be looked up by name with {@link #get(CharSequence)} or by component generic types with
096     * {@link #lookup(CharSequence...)}; both of these will not return a StringConvert with type info for what it
097     * takes and returns beyond "Object", but the result can be cast to a StringConvert with the correct type.
098     * @param types a vararg of Class objects representing the type this can convert, including generic type parameters
099     *              of the first element, if there are any, at positions after the first
100     */
101    public StringConvert(final CharSequence... types)
102    {
103        this(false, types);
104    }
105    public CharSequence getName() {return name;}
106    public abstract String stringify(T item);
107    public abstract T restore(String text);
108
109    /**
110     * Attempts to restore a specific type of value from the given text. Useful when this StringConvert does not have
111     * meaningful generic type information (e.g. {@code StringConvert<?>}), and you know the correct type externally.
112     * May throw a ClassCastException if type is not compatible with the type this deserializes to (that is, T).
113     * @param text the text to try to read as serialized data describing a T2 object
114     * @param type the Class of the data to try to produce, which should be as specific as possible
115     * @param <T2> you must be able to cast from a T (the type described by this class' {@link #specificName}) to a T2
116     * @return if this is successful, a T2 drawn from the data in text; otherwise, this may throw an exception
117     */
118    @SuppressWarnings("unchecked")
119    public <T2> T2 restore(String text, Class<T2> type)
120    {
121        return (T2)restore(text);
122    }
123    /**
124     * Gets the registered StringConvert for the given type name, if there is one, or returns null otherwise.
125     * The name can have the normal parts of a generic type, such as "OrderedMap&lt;String, ArrayList&lt;String&gt;&gt;",
126     * as long as such a type was fully registered. For that example, you could use
127     * {@code Converters.convertOrderedMap(Converters.convertString, Converters.convertArrayList(Converters.convertString))}
128     * to produce and register a StringConvert for the aforementioned generic type.
129     * @param name the name of the type to find a registered StringConvert, such as "ArrayList&lt;String&gt;" or "char[]"
130     * @return the registered StringConvert, if it was found, or null if none was found
131     */
132    public static StringConvert<?> get(final CharSequence name)
133    {
134        int i = registry.indexOfA(name);
135        if(i < 0) return null;
136        return registry.getAAt(i);
137    }
138    /**
139     * Looks up the StringConvert for a given vararg of Class instances (if an array of Classes is used other than a
140     * vararg, it must not be altered in the future, nor reused in a way that modifies its elements). Returns null if no
141     * StringConvert is found. You should usually cast the returned StringConvert, if non-null, to the specific
142     * StringConvert generic type you want.
143     * @param types the vararg of types to look up
144     * @return the StringConvert registered for the given types, or null if none has been made
145     */
146    public static StringConvert<?> lookup(final CharSequence... types)
147    {
148        return registry.getAFromB(types);
149    }
150
151    /**
152     * Simply takes a vararg of Class and returns the simple names of the Classes as a String array.
153     * Can be handy to avoid re-creating arrays implicitly from varargs of Class items.
154     * @param types a vararg of Class
155     * @return the String simple names of types as an array
156     */
157    public static CharSequence[] asArray(final CharSequence... types)
158    {
159        return types;
160    }
161
162    public static final CrossHash.IHasher spaceIgnoringHasher = new CrossHash.IHasher() {
163        @Override
164        public int hash(Object data) {
165            if (!(data instanceof StringConvert || data instanceof CharSequence))
166                return 0;
167            final CharSequence s;
168            if(data instanceof StringConvert) s = ((StringConvert) data).getName();
169            else s = (CharSequence) data;
170
171            long result = 0x1A976FDF6BF60B8EL, z = 0x60642E2A34326F15L;
172            final int len = s.length();
173            for (int i = 0; i < len; i++) {
174                result ^= (z += (s.charAt(i) ^ 0xC6BC279692B5CC85L) * 0x6C8E9CF570932BABL);
175                result = (result << 54 | result >>> 10);
176            }
177            result += (z ^ z >>> 26) * 0x632BE59BD9B4E019L;
178            result = (result ^ result >>> 33) * 0xFF51AFD7ED558CCDL;
179            return (int) ((result ^ result >>> 33) * 0xC4CEB9FE1A85EC53L);
180        }
181
182        @Override
183        public boolean areEqual(Object left, Object right) {
184            if (left == right) return true;
185            if(!((left instanceof StringConvert || left instanceof CharSequence)
186                    && (right instanceof StringConvert || right instanceof CharSequence)))
187                return false;
188            final CharSequence ls, rs;
189            if(left instanceof StringConvert) ls = ((StringConvert) left).getName();
190            else ls = (CharSequence)left;
191            if(right instanceof StringConvert) rs = ((StringConvert) right).getName();
192            else rs = (CharSequence)right;
193            final int llen = ls.length(), rlen = rs.length();
194            char lc = ' ', rc = ' ';
195            for (int l = 0, r = 0; l < llen && r < rlen; l++, r++) {
196                while (l < llen && (lc = ls.charAt(l)) == ' ') ++l;
197                while (r < rlen && (rc = rs.charAt(r)) == ' ') ++r;
198                if(lc != rc)
199                    return false;
200            }
201            return true;
202        }
203    };
204
205    public static final CrossHash.IHasher spaceIgnoringArrayHasher = new CrossHash.IHasher() {
206        @Override
207        public int hash(final Object data) {
208            if (data == null)
209                return 0;
210            if(!(data instanceof CharSequence[]))
211                return data.hashCode();
212            CharSequence[] data2 = (CharSequence[])data;
213
214            long result = 0x1A976FDF6BF60B8EL, z = 0x60642E2A34326F15L;
215            final int len = data2.length;
216            for (int i = 0; i < len; i++) {
217                result ^= (z += (spaceIgnoringHasher.hash(data2[i]) ^ 0xC6BC279692B5CC85L) * 0x6C8E9CF570932BABL);
218                result = (result << 54 | result >>> 10);
219            }
220            result += (z ^ z >>> 26) * 0x632BE59BD9B4E019L;
221            result = (result ^ result >>> 33) * 0xFF51AFD7ED558CCDL;
222            return (int) ((result ^ result >>> 33) * 0xC4CEB9FE1A85EC53L);
223        }
224
225        @Override
226        public boolean areEqual(Object left, Object right) {
227            return left == right || ((left instanceof CharSequence[] && right instanceof CharSequence[]) ? CrossHash.equalityHelper((CharSequence[]) left, (CharSequence[]) right, spaceIgnoringHasher) : Objects.equals(left, right));
228        }
229    };
230
231    public static final K2<StringConvert, CharSequence[]> registry =
232            new K2<>(128, 0.75f, spaceIgnoringHasher, spaceIgnoringArrayHasher);
233
234
235}