001/*
002 * SPDX-License-Identifier: Apache-2.0
003 *
004 * Copyright 2025-2026 The Enola <https://enola.dev> Authors
005 *
006 * Licensed under the Apache License, Version 2.0 (the "License");
007 * you may not use this file except in compliance with the License.
008 * You may obtain a copy of the License at
009 *
010 *     https://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package dev.enola.data.id;
019
020import static java.util.Objects.requireNonNull;
021
022import com.google.errorprone.annotations.Immutable;
023
024import dev.enola.common.convert.ConversionException;
025import dev.enola.common.convert.ObjectToStringBiConverter;
026import dev.enola.data.iri.IRIConverter;
027import dev.enola.data.iri.StringableIRI;
028
029import org.jspecify.annotations.Nullable;
030
031import java.io.IOException;
032import java.util.Optional;
033
034@Immutable(containerOf = "T")
035/** ID-IRI is an {@link IRI} based on an ID object. */
036// skipcq: JAVA-W0100
037public abstract class IDIRI<T extends Comparable<T>> extends StringableIRI {
038    // TODO dev.enola.data.id.IDIRI to dev.enola.data.iri
039
040    // TODO Add URL Pattern, in & out!
041
042    private final T id;
043
044    protected IDIRI(T id) {
045        this.id = requireNonNull(id);
046    }
047
048    // TODO Consider "pulling up" an Object id() method to IRI itself?
049    public T id() {
050        return id;
051    }
052
053    protected abstract <C extends IDIRI<T>> IRIConverter<C> iriConverter();
054
055    @Override
056    public void append(Appendable appendable) throws IOException {
057        if (!iriConverter().convertInto(this, appendable))
058            throw new IllegalArgumentException(id.toString());
059    }
060
061    @Override
062    protected String createStringIRI() {
063        return iriConverter().convertTo(this);
064    }
065
066    @Override
067    protected boolean isEqualTo(Object other) {
068        return id.equals(other);
069    }
070
071    @Override
072    public int hashCode() {
073        return id.hashCode();
074    }
075
076    @Override
077    @SuppressWarnings("unchecked")
078    protected int compare(Object other) {
079        return id.compareTo(((IDIRI<T>) other).id);
080    }
081
082    protected abstract static class ConverterX<C extends IDIRI<T>, T extends Comparable<T>>
083            implements IRIConverter<C> {
084        private final String prefix;
085        private final ObjectToStringBiConverter<T> converter;
086
087        protected ConverterX(String prefix, ObjectToStringBiConverter<T> converter) {
088            this.prefix = prefix;
089            this.converter = converter;
090        }
091
092        protected abstract C create(T id);
093
094        @Override
095        public Optional<C> convert(String input) throws ConversionException {
096            if (!input.startsWith(prefix)) return Optional.empty();
097            @Nullable T id = converter.convertFrom(input.substring(prefix.length()));
098            if (id == null)
099                throw new ConversionException("Not a TestID: " + input); // Optional.empty()?
100            return Optional.of(create(id));
101        }
102
103        @Override
104        public boolean convertInto(C from, Appendable into)
105                throws ConversionException, IOException {
106            into.append(prefix);
107            return converter.convertInto(from.id(), into);
108        }
109    }
110}