001// --------------------------------------------------------------------------------
002// Copyright 2002-2026 Echo Three, LLC
003//
004// Licensed under the Apache License, Version 2.0 (the "License");
005// you may not use this file except in compliance with the License.
006// You may obtain a copy of the License at
007//
008//     http://www.apache.org/licenses/LICENSE-2.0
009//
010// Unless required by applicable law or agreed to in writing, software
011// distributed under the License is distributed on an "AS IS" BASIS,
012// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013// See the License for the specific language governing permissions and
014// limitations under the License.
015// --------------------------------------------------------------------------------
016
017package com.echothree.model.control.sequence.server.logic;
018
019import com.echothree.model.control.sequence.common.SequenceChecksumTypes;
020import com.echothree.model.control.sequence.common.SequenceEncoderTypes;
021import com.echothree.model.control.sequence.common.exception.UnimplementedSequenceChecksumTypeException;
022import com.echothree.model.control.sequence.common.exception.UnimplementedSequenceEncoderTypeException;
023import com.echothree.model.control.sequence.common.exception.UnknownSequenceNameException;
024import com.echothree.model.control.sequence.server.control.SequenceControl;
025import com.echothree.model.control.sequence.server.logic.checksum.Mod10SequenceChecksum;
026import com.echothree.model.control.sequence.server.logic.checksum.Mod36SequenceChecksum;
027import com.echothree.model.control.sequence.server.logic.checksum.NoneSequenceChecksum;
028import com.echothree.model.control.sequence.server.logic.checksum.SequenceChecksum;
029import com.echothree.model.control.sequence.server.logic.encoder.NoneSequenceEncoder;
030import com.echothree.model.control.sequence.server.logic.encoder.ReverseSequenceEncoder;
031import com.echothree.model.control.sequence.server.logic.encoder.ReverseSwapSequenceEncoder;
032import com.echothree.model.data.sequence.server.entity.Sequence;
033import com.echothree.model.data.sequence.server.entity.SequenceDetail;
034import com.echothree.model.data.sequence.server.entity.SequenceType;
035import com.echothree.model.data.sequence.server.entity.SequenceTypeDetail;
036import com.echothree.util.common.message.ExecutionErrors;
037import com.echothree.util.server.cdi.CommandScopeExtension;
038import com.echothree.util.server.control.BaseLogic;
039import com.echothree.util.server.message.ExecutionErrorAccumulator;
040import com.echothree.util.server.persistence.Session;
041import java.util.ArrayDeque;
042import java.util.Deque;
043import java.util.EmptyStackException;
044import java.util.NoSuchElementException;
045import java.util.concurrent.ConcurrentHashMap;
046import java.util.concurrent.ConcurrentMap;
047import java.util.regex.Pattern;
048import javax.enterprise.context.ApplicationScoped;
049import javax.enterprise.inject.spi.CDI;
050
051@ApplicationScoped
052public class SequenceGeneratorLogic
053        extends BaseLogic {
054
055    protected SequenceGeneratorLogic() {
056        super();
057    }
058
059    public static SequenceGeneratorLogic getInstance() {
060        return CDI.current().select(SequenceGeneratorLogic.class).get();
061    }
062
063    // --------------------------------------------------------------------------------
064    //   Generation
065    // --------------------------------------------------------------------------------
066
067    public final static String NUMERIC_VALUES = "0123456789";
068    public final static int NUMERIC_MAX_INDEX = NUMERIC_VALUES.length() - 1;
069    public final static String ALPHABETIC_VALUES = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
070    public final static int ALPHABETIC_MAX_INDEX = ALPHABETIC_VALUES.length() - 1;
071    public final static String ALPHANUMERIC_VALUES = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
072    public final static int ALPHANUMERIC_MAX_INDEX = ALPHANUMERIC_VALUES.length() - 1;
073
074    private final static int DEFAULT_CHUNK_SIZE = 10;
075
076    private final static ConcurrentMap<Long, Deque<String>> sequenceDeques = new ConcurrentHashMap<>();
077
078    private int getChunkSize(SequenceTypeDetail sequenceTypeDetail, SequenceDetail sequenceDetail) {
079        var chunkSize = sequenceDetail.getChunkSize();
080
081        if(chunkSize == null) {
082            chunkSize = sequenceTypeDetail.getChunkSize();
083        }
084
085        return chunkSize == null ? DEFAULT_CHUNK_SIZE : chunkSize;
086    }
087
088    // If the SequenceEncoders are ever modified to do anything other than swap characters
089    // around, getPattern(...) will need to have a special version of this. Right now, as it
090    // is used to identify Sequences based on the different masks, those masks must be altered
091    // to generate a regular expressions that's properly formatted for what's done here when
092    // creating new encoded values during generation.
093    private String encode(SequenceTypeDetail sequenceTypeDetail, String value) {
094        var sequenceEncoderTypeName = sequenceTypeDetail.getSequenceEncoderType().getSequenceEncoderTypeName();
095        var sequenceEncoderType = SequenceEncoderTypes.valueOf(sequenceEncoderTypeName);
096        String encodedValue;
097
098        switch(sequenceEncoderType) {
099            case NONE -> encodedValue = NoneSequenceEncoder.getInstance().encode(value);
100            case REVERSE -> encodedValue = ReverseSequenceEncoder.getInstance().encode(value);
101            case REVERSE_SWAP -> encodedValue = ReverseSwapSequenceEncoder.getInstance().encode(value);
102            default -> throw new UnimplementedSequenceEncoderTypeException();
103        }
104
105        return encodedValue;
106    }
107
108    private SequenceChecksum getSequenceChecksum(SequenceChecksumTypes sequenceChecksumType) {
109        switch(sequenceChecksumType) {
110            case NONE -> {
111                return NoneSequenceChecksum.getInstance();
112            }
113            case MOD_10 -> {
114                return Mod10SequenceChecksum.getInstance();
115            }
116            case MOD_36 -> {
117                return Mod36SequenceChecksum.getInstance();
118            }
119            default -> throw new UnimplementedSequenceChecksumTypeException();
120        }
121    }
122
123    private SequenceChecksum getSequenceChecksum(SequenceTypeDetail sequenceTypeDetail) {
124        var sequenceChecksumType = sequenceTypeDetail.getSequenceChecksumType();
125        SequenceChecksum result = null;
126
127        // Needs to be very careful as the SequenceChecksumType may be null for a given SequenceTypeDetail.
128        // If it is, we'll just leave sequenceChecksum null and return that.
129        if(sequenceChecksumType != null) {
130            var sequenceChecksumTypeName = sequenceChecksumType.getSequenceChecksumTypeName();
131
132            result = getSequenceChecksum(SequenceChecksumTypes.valueOf(sequenceChecksumTypeName));
133        }
134
135        return result;
136    }
137
138    /**
139     * Generate and return the next value for a given Sequence.
140     * 
141     * @return A unique value for the sequence is returned. Null will be returned when the
142     * sequence is exhausted, the length of the mask is not equal to the length of the
143     * value, or an invalid character is encountered in the mask.
144     */
145    public String getNextSequenceValue(Sequence sequence) {
146        var sequenceEntityId = sequence.getPrimaryKey().getEntityId();
147        var sequenceDeque = sequenceDeques.get(sequenceEntityId);
148        String result = null;
149
150        if(sequenceDeque == null) {
151            // Create a new sequenceDeque (aka. a LinkedList), and try to put it into sequenceDeques.
152            // If it is already there, the new one is discarded, and the one that was already there
153            // is returned.
154            var newSequenceDeque = new ArrayDeque<String>();
155
156            sequenceDeque = sequenceDeques.putIfAbsent(sequenceEntityId, newSequenceDeque);
157            if(sequenceDeque == null) {
158                sequenceDeque = newSequenceDeque;
159            }
160        }
161
162        synchronized(sequenceDeque) {
163            try {
164                result = sequenceDeque.removeFirst();
165            } catch (NoSuchElementException nsee1) {
166                try(var ignored = CommandScopeExtension.getCommandScopeContext().push()) {
167                    var sequenceControl = Session.getModelController(SequenceControl.class);
168                    var sequenceValue = sequenceControl.getSequenceValueForUpdate(sequence);
169
170                    if(sequenceValue != null) {
171                        var sequenceDetail = sequence.getLastDetail();
172                        var sequenceTypeDetail = sequenceDetail.getSequenceType().getLastDetail();
173                        var prefix = sequenceTypeDetail.getPrefix();
174                        var suffix = sequenceTypeDetail.getSuffix();
175                        var chunkSize = getChunkSize(sequenceTypeDetail, sequenceDetail);
176                        var mask = sequenceDetail.getMask();
177                        var maskChars = mask.toCharArray();
178                        var value = sequenceValue.getValue();
179                        var valueLength = value.length();
180                        var valueChars = value.toCharArray();
181
182                        // Mask and its value must be the same length.
183                        if(valueLength == mask.length()) {
184                            for(var i = 0; i < chunkSize; i++) {
185                                // Step through the string from the right to the left.
186                                var forceIncrement = false;
187
188                                for(var index = valueLength - 1; index > -1; index--) {
189                                    var maskChar = maskChars[index];
190                                    var valueChar = valueChars[index];
191
192                                    switch(maskChar) {
193                                        case '9' -> {
194                                            var currentIndex = NUMERIC_VALUES.indexOf(valueChar);
195                                            if(currentIndex != -1) {
196                                                int newCharIndex;
197                                                if(currentIndex == NUMERIC_MAX_INDEX) {
198                                                    newCharIndex = 0;
199                                                    forceIncrement = true;
200                                                } else {
201                                                    newCharIndex = currentIndex + 1;
202                                                }
203                                                valueChars[index] = NUMERIC_VALUES.charAt(newCharIndex);
204                                            } else {
205                                                value = null;
206                                            }
207                                        }
208                                        case 'A' -> {
209                                            var currentIndex = ALPHABETIC_VALUES.indexOf(valueChar);
210                                            if(currentIndex != -1) {
211                                                int newCharIndex;
212                                                if(currentIndex == ALPHABETIC_MAX_INDEX) {
213                                                    newCharIndex = 0;
214                                                    forceIncrement = true;
215                                                } else {
216                                                    newCharIndex = currentIndex + 1;
217                                                }
218                                                valueChars[index] = ALPHABETIC_VALUES.charAt(newCharIndex);
219                                            } else {
220                                                value = null;
221                                            }
222                                        }
223                                        case 'Z' -> {
224                                            var currentIndex = ALPHANUMERIC_VALUES.indexOf(valueChar);
225                                            if(currentIndex != -1) {
226                                                int newCharIndex;
227                                                if(currentIndex == ALPHANUMERIC_MAX_INDEX) {
228                                                    newCharIndex = 0;
229                                                    forceIncrement = true;
230                                                } else {
231                                                    newCharIndex = currentIndex + 1;
232                                                }
233                                                valueChars[index] = ALPHANUMERIC_VALUES.charAt(newCharIndex);
234                                            } else {
235                                                value = null;
236                                            }
237                                        }
238                                    }
239
240                                    // If an error occurred, or we do not need to increment any other positions in
241                                    // the sequences value, exit.
242                                    if((value == null) || !forceIncrement) {
243                                        break;
244                                    }
245
246                                    // If we reach the start of the sequences value, and have not yet exited, the
247                                    // sequence is at its maximum possible value, exit.
248                                    if(index == 0) {
249                                        value = null;
250                                    }
251
252                                    forceIncrement = false;
253                                }
254
255                                if(value != null) {
256                                    value = new String(valueChars);
257
258                                    var encodedValue = encode(sequenceTypeDetail, value);
259
260                                    var intermediateValue = (prefix != null ? prefix : "") + encodedValue + (suffix != null ? suffix : "");
261                                    var checksum = getSequenceChecksum(sequenceTypeDetail).calculate(intermediateValue);
262
263                                    sequenceDeque.add(intermediateValue + checksum);
264                                }
265                            }
266
267                            sequenceValue.setValue(value);
268
269                            try {
270                                result = sequenceDeque.removeFirst();
271                            } catch(EmptyStackException ese2) {
272                                // Shouldn't happen, if it does, result stays null
273                            }
274                        }
275                    }
276                }
277            }
278        }
279
280        return result;
281    }
282
283    public String getNextSequenceValue(final ExecutionErrorAccumulator eea, final Sequence sequence) {
284        return getNextSequenceValue(sequence);
285    }
286
287    public String getNextSequenceValue(final ExecutionErrorAccumulator eea, final SequenceType sequenceType) {
288        var sequence = getDefaultSequence(eea, sequenceType);
289
290        return hasExecutionErrors(eea) ? null : getNextSequenceValue(eea, sequence);
291    }
292
293    public String getNextSequenceValue(final ExecutionErrorAccumulator eea, final String sequenceTypeName) {
294        var sequence = getDefaultSequence(eea, sequenceTypeName);
295
296        return hasExecutionErrors(eea) ? null : getNextSequenceValue(eea, sequence);
297    }
298
299    public Sequence getDefaultSequence(final ExecutionErrorAccumulator eea, final SequenceType sequenceType) {
300        var sequenceControl = Session.getModelController(SequenceControl.class);
301        var sequence = sequenceControl.getDefaultSequence(sequenceType);
302
303        if(sequence == null) {
304            handleExecutionError(UnknownSequenceNameException.class, eea, ExecutionErrors.MissingDefaultSequence.name(), sequenceType.getLastDetail().getSequenceTypeName());
305        }
306
307        return sequence;
308    }
309
310    public Sequence getDefaultSequence(final ExecutionErrorAccumulator eea, final String sequenceTypeName) {
311        var sequenceType = SequenceTypeLogic.getInstance().getSequenceTypeByName(eea, sequenceTypeName);
312        Sequence sequence = null;
313
314        if(!hasExecutionErrors(eea)) {
315            sequence = getDefaultSequence(eea, sequenceType);
316        }
317
318        return sequence;
319    }
320
321    // --------------------------------------------------------------------------------
322    //   Identification
323    // --------------------------------------------------------------------------------
324
325    private StringBuilder getPatternFromMask(final String mask) {
326        var maskChars = mask.toCharArray();
327        var pattern = new StringBuilder();
328
329        for(var maskChar : maskChars) {
330            switch(maskChar) {
331                case '9' -> pattern.append("[\\p{Digit}]");
332                case 'A' -> pattern.append("\\p{Upper}");
333                case 'Z' -> pattern.append("[\\p{Upper}\\p{Digit}]");
334            }
335        }
336
337        return pattern;
338    }
339
340    private String getPattern(final Sequence sequence) {
341        var pattern = new StringBuilder("^");
342        var sequenceDetail = sequence.getLastDetail();
343        var sequenceTypeDetail = sequenceDetail.getSequenceType().getLastDetail();
344        var prefix = sequenceTypeDetail.getPrefix();
345        var suffix = sequenceTypeDetail.getSuffix();
346        var mask = sequenceDetail.getMask();
347
348        if(prefix != null) {
349            pattern.append(Pattern.quote(prefix));
350        }
351
352        var encodedMask = encode(sequenceTypeDetail, mask);
353        pattern.append(getPatternFromMask(encodedMask));
354
355        if(suffix != null) {
356            pattern.append(Pattern.quote(suffix));
357        }
358
359        pattern.append(getSequenceChecksum(sequenceTypeDetail).regexp());
360
361        return pattern.append('$').toString();
362    }
363
364    public SequenceType identifySequenceType(final String value) {
365        var sequenceControl = Session.getModelController(SequenceControl.class);
366        var sequenceTypes = sequenceControl.getSequenceTypes();
367        SequenceType result = null;
368
369        // Check all Sequence Types...
370        for(var sequenceType : sequenceTypes) {
371            // ...and each Sequence within them.
372            for(var sequence : sequenceControl.getSequencesBySequenceType(sequenceType)) {
373                // Check the regexp that's generated for this sequence against the value.
374                if(value.matches(getPattern(sequence))) {
375                    // If the regexp matches, check the checksum. If it matches, we've
376                    // probably got a match. If not, continue looking for other matches.
377                    if(verifyValue(sequenceType, value)) {
378                        result = sequenceType;
379                        break;
380                    }
381                }
382            }
383
384            // If the SequenceType was found, break out of the outer for loop as well.
385            if(result != null) {
386                break;
387            }
388        }
389
390        return result;
391    }
392
393    // --------------------------------------------------------------------------------
394    //   Verification
395    // --------------------------------------------------------------------------------
396
397    public boolean verifyValue(final SequenceType sequenceType, final String value) {
398        var sequenceTypeDetail = sequenceType.getLastDetail();
399        var sequenceChecksum = getSequenceChecksum(sequenceTypeDetail);
400
401        return sequenceChecksum == null || sequenceChecksum.verify(value);
402    }
403
404}