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.java;
019
020import com.google.common.reflect.Reflection;
021
022import dev.enola.thing.Thing;
023import dev.enola.thing.impl.IImmutableThing;
024import dev.enola.thing.impl.ImmutableThing;
025
026import org.jspecify.annotations.Nullable;
027
028import java.lang.reflect.InvocationHandler;
029import java.lang.reflect.Method;
030import java.util.Arrays;
031
032public final class ProxyTBF implements TBF {
033
034    private final TBF wrap;
035
036    /**
037     * Constructor.
038     *
039     * @param wrap is a {@link TBF} such as {@link ImmutableThing#FACTORY} or {@link
040     *     dev.enola.thing.impl.MutableThing#FACTORY}.
041     */
042    public ProxyTBF(TBF wrap) {
043        this.wrap = wrap;
044    }
045
046    @Override
047    public boolean handles(Class<?> builderInterface) {
048        // Skip Proxy if wrapped delegate can handle it
049        // This makes TBFChain more efficient, because e.g.
050        // Thing.Builder.class can be "passed through".
051        return !wrap.handles(builderInterface);
052    }
053
054    @Override
055    public boolean handles(String typeIRI) {
056        // ProxyTBF handles any typeIRI, currently.
057        // TODO Make it based on whether create() implementation uses default class or not.
058        return true;
059    }
060
061    @Override
062    @SuppressWarnings("unchecked")
063    public Thing.Builder<Thing> create(String typeIRI) {
064        var classPair = TypeToBuilder.typeToBuilder(typeIRI);
065        return create(classPair.builderClass(), classPair.thingClass());
066    }
067
068    @Override
069    @SuppressWarnings("unchecked")
070    public <T extends Thing, B extends Thing.Builder<T>> B create(
071            Class<B> builderInterface, Class<T> thingInterface) {
072        return (B)
073                create(
074                        wrap,
075                        -1,
076                        (Class<Thing.Builder<IImmutableThing>>) builderInterface,
077                        (Class<IImmutableThing>) thingInterface);
078    }
079
080    @Override
081    @SuppressWarnings("unchecked")
082    public <T extends Thing, B extends Thing.Builder<T>> B create(
083            Class<B> builderInterface, Class<T> thingInterface, int expectedSize) {
084        return (B)
085                create(
086                        wrap,
087                        expectedSize,
088                        (Class<Thing.Builder<IImmutableThing>>) builderInterface,
089                        (Class<IImmutableThing>) thingInterface);
090    }
091
092    @SuppressWarnings("unchecked")
093    private static Thing.Builder<? extends IImmutableThing> create(
094            TBF tbf,
095            int expectedSize,
096            Class<Thing.Builder<IImmutableThing>> builderInterface,
097            Class<IImmutableThing> thingInterface) {
098        if (builderInterface.equals(Thing.Builder.class))
099            return createX(tbf, builderInterface, thingInterface, expectedSize);
100
101        var wrappedBuilder = // an ImmutableThing.Builder or a MutableThing (but NOT another Proxy)
102                (Thing.Builder<? extends IImmutableThing>)
103                        createX(tbf, builderInterface, thingInterface, expectedSize);
104        var handler =
105                new BuilderInvocationHandler(tbf, wrappedBuilder, builderInterface, thingInterface);
106        var proxy = Reflection.newProxy(builderInterface, handler);
107        if (!(builderInterface.isInstance(proxy)))
108            throw new IllegalArgumentException(proxy.toString());
109        return proxy;
110    }
111
112    private static Thing.Builder<? extends IImmutableThing> createX(
113            TBF tbf,
114            Class<Thing.Builder<IImmutableThing>> builderInterface,
115            Class<IImmutableThing> thingInterface,
116            int expectedSize) {
117        if (expectedSize == -1) return tbf.create(builderInterface, thingInterface);
118        else return tbf.create(builderInterface, thingInterface, expectedSize);
119    }
120
121    // TODO Consider using Guava's AbstractInvocationHandler?
122
123    private record BuilderInvocationHandler(
124            TBF tbf,
125            Thing.Builder<? extends IImmutableThing> immutableThingBuilder,
126            Class<Thing.Builder<IImmutableThing>> builderInterface,
127            Class<IImmutableThing> thingInterface)
128            implements InvocationHandler {
129
130        @Override
131        public Object invoke(Object proxy, Method method, @Nullable Object[] args)
132                throws Throwable {
133            if (method.isDefault()) return InvocationHandler.invokeDefault(proxy, method, args);
134            if (method.getName() == "iri" && args != null && args.length > 0) {
135                immutableThingBuilder.iri((String) args[0]);
136                return proxy;
137            } else if (method.getName().equals("build")) {
138                var built = immutableThingBuilder.build();
139                var handler =
140                        new ThingInvocationHandler(tbf, built, builderInterface, thingInterface);
141                return Reflection.newProxy(thingInterface, handler);
142            } else if (method.getName().equals("toString")) return toString();
143            // else
144            try {
145                return method.invoke(immutableThingBuilder, args);
146            } catch (IllegalArgumentException e) {
147                throw new IllegalArgumentException(
148                        "Interface may be missing default method implementations?!"
149                                + " immutableThingBuilder="
150                                + immutableThingBuilder
151                                + "; proxy="
152                                + proxy
153                                + "; method="
154                                + method
155                                + "; args="
156                                + Arrays.toString(args),
157                        e);
158            }
159        }
160
161        @Override
162        public String toString() {
163            return "ProxyTBF.BuilderInvocationHandler{builderInterface="
164                    + builderInterface
165                    + ", builder="
166                    + immutableThingBuilder
167                    + "}";
168        }
169    }
170
171    private record ThingInvocationHandler(
172            TBF tbf,
173            IImmutableThing thing,
174            Class<Thing.Builder<IImmutableThing>> builderInterface,
175            Class<IImmutableThing> thingInterface)
176            implements InvocationHandler {
177
178        @Override
179        public Object invoke(Object proxy, Method method, @Nullable Object[] args)
180                throws Throwable {
181            if (method.isDefault()) return InvocationHandler.invokeDefault(proxy, method, args);
182            if (method.getName().equals("copy")) return copy();
183            if (method.getName().equals("toString")) return toString();
184            return method.invoke(thing, args);
185        }
186
187        @SuppressWarnings(
188                "Immutable") // TODO Remove when switching to (TBD) PredicatesObjects.Visitor
189        private Thing.Builder<? extends IImmutableThing> copy() {
190            var size = thing.properties().size();
191            var builder = create(tbf, size, builderInterface, thingInterface);
192            builder.iri(thing.iri());
193            thing.properties()
194                    .forEach(
195                            (predicateIRI, object) -> {
196                                var datatypeIRI = thing.datatype(predicateIRI);
197                                builder.set(predicateIRI, object, datatypeIRI);
198                            });
199            return builder;
200        }
201
202        @Override
203        public String toString() {
204            return "ProxyTBF.ThingInvocationHandler{thingInterface="
205                    + thingInterface
206                    + ", thing="
207                    + thing
208                    + "}";
209        }
210    }
211}