001/*
002 * SPDX-License-Identifier: Apache-2.0
003 *
004 * Copyright 2024-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.thing.template;
019
020import com.github.fge.uritemplate.URITemplate;
021import com.github.fge.uritemplate.URITemplateException;
022import com.github.fge.uritemplate.URITemplateParseException;
023import com.google.common.base.Function;
024import com.google.common.collect.Iterables;
025
026import dev.enola.common.collect.MoreIterables;
027import dev.enola.data.iri.template.URITemplateMatcherChain;
028import dev.enola.data.iri.template.VariableMaps;
029import dev.enola.thing.*;
030import dev.enola.thing.impl.ImmutableThing;
031import dev.enola.thing.repo.ThingRepository;
032
033import org.jspecify.annotations.Nullable;
034
035import java.util.*;
036import java.util.AbstractMap.SimpleImmutableEntry;
037
038public class TemplateThingRepository implements ThingRepository, TemplateService {
039
040    private record Match(String iriTemplate, Function<Map<String, String>, Thing> function) {}
041
042    private final ThingRepository delegate;
043    private final URITemplateMatcherChain<Match> iriTemplateChain;
044
045    public TemplateThingRepository(ThingRepository delegate) {
046        this.delegate = delegate;
047        int size = MoreIterables.sizeIfKnown(delegate.list()).orElse(42);
048        var iriTemplateChainBuilder = URITemplateMatcherChain.<Match>builderWithExpectedSize(size);
049        for (var thing : delegate.list()) {
050            if (!thing.isIterable(KIRI.RDF.TYPE)
051                    && KIRI.RDFS.CLASS.equals(thing.getString(KIRI.RDF.TYPE))) {
052                thing.getOptional(KIRI.E.IRI_TEMPLATE_PROPERTY, String.class)
053                        .ifPresent(
054                                iriTemplate ->
055                                        iriTemplateChainBuilder.add(
056                                                iriTemplate, gen(iriTemplate, thing)));
057            }
058        }
059        this.iriTemplateChain = iriTemplateChainBuilder.build();
060    }
061
062    private Match gen(String classIRITemplate, Thing rdfClass) {
063        Set<SimpleImmutableEntry<String, URITemplate>> set = new HashSet<>();
064        for (String predicateIRI : rdfClass.predicateIRIs()) {
065            if (KIRI.E.IRI_TEMPLATE_DATATYPE.equals(rdfClass.datatype(predicateIRI))) {
066                var uriTemplate = newURITemplate(rdfClass.getString(predicateIRI));
067                set.add(new SimpleImmutableEntry<String, URITemplate>(predicateIRI, uriTemplate));
068            }
069        }
070        var templatePredicates = Collections.unmodifiableSet(set);
071
072        var classURITemplate = newURITemplate(classIRITemplate);
073        return new Match(
074                classIRITemplate,
075                params -> {
076                    try {
077                        var varMap = VariableMaps.from(params);
078                        var builder = ImmutableThing.builder();
079                        var newIRI = Templates.unescapeURL(classURITemplate.toString(varMap));
080                        builder.iri(newIRI);
081                        builder.set(KIRI.RDF.TYPE, new Link(rdfClass.iri()));
082                        for (var templatePredicate : templatePredicates) {
083                            var predicateIRI = templatePredicate.getKey();
084                            var predicateURITemplate = templatePredicate.getValue();
085                            var link = Templates.unescapeURL(predicateURITemplate.toString(varMap));
086                            builder.set(predicateIRI, new Link(link));
087                        }
088
089                        return builder.build();
090                    } catch (URITemplateException e) {
091                        throw new IllegalArgumentException(rdfClass.iri(), e);
092                    }
093                });
094    }
095
096    private URITemplate newURITemplate(String template) {
097        try {
098            return new URITemplate(template);
099        } catch (URITemplateParseException e) {
100            throw new IllegalArgumentException(template + " invalid IRI Template", e);
101        }
102    }
103
104    @Override
105    public Iterable<String> listIRI() {
106        return Iterables.concat(iriTemplateChain.listTemplates(), delegate.listIRI());
107    }
108
109    @Override
110    public @Nullable Thing get(String iri) {
111        // TODO Check delegate first, and merge() if found... with TDD!
112        var optEntry = iriTemplateChain.match(iri);
113        if (optEntry.isPresent()) {
114            var entry = optEntry.get();
115            var match = entry.getKey();
116            var params = entry.getValue();
117            var function = match.function;
118            return function.apply(params);
119        } else return delegate.get(iri);
120    }
121
122    @Override
123    public Optional<Breakdown> breakdown(String nonTemplateIRI) {
124        if (Templates.hasVariables(nonTemplateIRI))
125            throw new IllegalArgumentException("Template: " + nonTemplateIRI);
126        var optEntry = iriTemplateChain.match(nonTemplateIRI);
127        if (optEntry.isPresent()) {
128            var entry = optEntry.get();
129            var match = entry.getKey();
130            var params = entry.getValue();
131            var iriTemplate = match.iriTemplate;
132            return Optional.of(new Breakdown(iriTemplate, params));
133        } else return Optional.empty();
134    }
135}