/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.axiom.core;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.axiom.core.util.EdgeRelation;
import org.apache.axiom.core.util.TopologicalSort;

public abstract class NodeFactoryImpl implements NodeFactory {
    private final NodeFactory2 factory2;
    private final Map<Class<?>, Constructor<?>> constructorMap;

    public NodeFactoryImpl(ClassLoader cl, String factory2ClassName, String... packages) {
        try {
            factory2 =
                    (NodeFactory2)
                            cl.loadClass(factory2ClassName).getDeclaredField("INSTANCE").get(null);
        } catch (ReflectiveOperationException ex) {
            throw new NodeFactoryException("Failed to instantiate NodeFactory2 implementation", ex);
        }
        List<Class<?>> implementations = new ArrayList<Class<?>>();
        for (String pkg : packages) {
            try {
                BufferedReader in =
                        new BufferedReader(
                                new InputStreamReader(
                                        cl.getResourceAsStream(
                                                pkg.replace('.', '/') + "/nodetypes.index"),
                                        StandardCharsets.UTF_8));
                try {
                    String line;
                    while ((line = in.readLine()) != null) {
                        if (line.startsWith("#")) {
                            continue;
                        }
                        String className = pkg + "." + line;
                        try {
                            implementations.add(cl.loadClass(className));
                        } catch (ClassNotFoundException ex) {
                            throw new NodeFactoryException("Failed to load class " + className, ex);
                        }
                    }
                } finally {
                    in.close();
                }
            } catch (IOException ex) {
                throw new NodeFactoryException(
                        "Failed to load node type index for package " + pkg, ex);
            }
        }
        implementations =
                TopologicalSort.sort(
                        implementations,
                        new EdgeRelation<Class<?>>() {
                            @Override
                            public boolean isEdge(Class<?> from, Class<?> to) {
                                return to.isAssignableFrom(from);
                            }
                        });
        Map<Class<?>, Class<?>> interfaceToImplementationMap = new HashMap<Class<?>, Class<?>>();
        Map<Class<?>, Constructor<?>> implementationToConstructorMap =
                new HashMap<Class<?>, Constructor<?>>();
        Set<Class<?>> ambiguousInterfaces = new HashSet<Class<?>>();
        for (Class<?> implementation : implementations) {
            Set<Class<?>> interfaces = new HashSet<Class<?>>();
            collectInterfaces(implementation, interfaces);
            for (Class<?> iface : interfaces) {
                if (!ambiguousInterfaces.contains(iface)) {
                    Class<?> clazz = interfaceToImplementationMap.get(iface);
                    if (clazz == null || implementation.isAssignableFrom(clazz)) {
                        interfaceToImplementationMap.put(iface, implementation);
                    } else if (!clazz.isAssignableFrom(implementation)) {
                        interfaceToImplementationMap.remove(iface);
                        ambiguousInterfaces.add(iface);
                    }
                }
            }
            try {
                implementationToConstructorMap.put(implementation, implementation.getConstructor());
            } catch (NoSuchMethodException ex) {
                throw new NodeFactoryException(
                        "Failed to get constructor for " + implementation.getName(), ex);
            }
        }
        constructorMap = new HashMap<Class<?>, Constructor<?>>();
        for (Map.Entry<Class<?>, Class<?>> entry : interfaceToImplementationMap.entrySet()) {
            constructorMap.put(
                    entry.getKey(), implementationToConstructorMap.get(entry.getValue()));
        }
        // TODO: this should eventually go away
        constructorMap.putAll(implementationToConstructorMap);
    }

    private static void collectInterfaces(Class<?> clazz, Set<Class<?>> interfaces) {
        for (Class<?> iface : clazz.getInterfaces()) {
            if (interfaces.add(iface)) {
                collectInterfaces(iface, interfaces);
            }
        }
        Class<?> superclass = clazz.getSuperclass();
        if (superclass != null) {
            collectInterfaces(superclass, interfaces);
        }
    }

    @Override
    public NodeFactory2 getFactory2() {
        return factory2;
    }

    @Override
    public final <T extends CoreNode> T createNode(Class<T> type) {
        Constructor<?> constructor = constructorMap.get(type);
        if (constructor == null) {
            throw new NodeFactoryException("Unknown node type " + type.getName());
        } else {
            try {
                return type.cast(constructor.newInstance());
            } catch (InvocationTargetException ex) {
                throw new NodeFactoryException(
                        "Caught exception thrown by constructor", ex.getCause());
            } catch (InstantiationException ex) {
                throw new NodeFactoryException("Failed to invoke constructor", ex);
            } catch (IllegalAccessException ex) {
                throw new NodeFactoryException("Failed to invoke constructor", ex);
            }
        }
    }
}
