/*
 * 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.weaver;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.axiom.weaver.classio.ClassFetcher;
import org.apache.axiom.weaver.mixin.ClassDefinition;
import org.apache.axiom.weaver.mixin.Mixin;
import org.apache.axiom.weaver.mixin.WeavingContext;
import org.apache.axiom.weaver.mixin.clazz.MixinFactory;
import org.apache.axiom.weaver.mixin.factory.FactoryMixinFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.github.veithen.jrel.association.MutableReferences;
import com.github.veithen.jrel.collection.LinkedIdentityHashSet;

public final class Weaver {
    private static final Log log = LogFactory.getLog(Weaver.class);

    private final ClassFetcher classFetcher;
    private final ImplementationClassNameMapper implementationClassNameMapper;
    private final Map<Class<?>, InterfaceNode> interfaceNodes = new HashMap<>();
    private final Map<Class<?>, ImplementationNode> implementationNodes = new HashMap<>();
    private final Map<Class<?>, Set<Mixin>> mixinsByInterface = new HashMap<>();
    private final MutableReferences<ImplementationNode> nodes =
            Relations.WEAVER.getConverse().newReferenceHolder(this);
    private int nextId = 1;

    public Weaver(
            ClassLoader classLoader, ImplementationClassNameMapper implementationClassNameMapper) {
        classFetcher = new ClassFetcherImpl(classLoader);
        this.implementationClassNameMapper = implementationClassNameMapper;
    }

    public void loadWeavablePackage(String packageName) {
        for (Mixin mixin : MixinFactory.loadMixins(classFetcher, packageName)) {
            addMixin(mixin);
        }
    }

    private void addMixin(Mixin mixin) {
        mixinsByInterface
                .computeIfAbsent(mixin.getTargetInterface(), k -> new LinkedIdentityHashSet<>())
                .add(mixin);
    }

    private InterfaceNode addInterface(Class<?> iface) {
        InterfaceNode interfaceNode = interfaceNodes.get(iface);
        if (interfaceNode == null) {
            FactoryMixinFactory.createFactoryMixin(classFetcher, iface).ifPresent(this::addMixin);
            Set<InterfaceNode> parentInterfaces = new LinkedIdentityHashSet<>();
            Set<ImplementationNode> parentImplementations = new LinkedIdentityHashSet<>();
            for (Class<?> superClass : iface.getInterfaces()) {
                InterfaceNode parentInterface = addInterface(superClass);
                parentInterfaces.add(parentInterface);
                parentImplementations.addAll(parentInterface.getImplementations());
            }
            InterfaceClassVisitor cv = new InterfaceClassVisitor();
            classFetcher.fetch(iface.getName(), cv);
            interfaceNode = new InterfaceNode(iface, parentInterfaces, cv.isSingleton());
            interfaceNodes.put(iface, interfaceNode);
            Set<MixinNode> mixinNodes = new LinkedIdentityHashSet<>();
            Set<Mixin> mixins = mixinsByInterface.get(iface);
            if (mixins != null) {
                for (Mixin mixin : mixins) {
                    mixinNodes.add(new MixinNode(mixin, interfaceNode));
                }
            }
            ImplementationNode implementationNode =
                    new ImplementationNode(
                            nextId++,
                            parentImplementations,
                            interfaceNode,
                            mixinNodes,
                            // The class name is evaluated lazily so that mapper only needs to
                            // produce values for classes that are actually generated.
                            () ->
                                    implementationClassNameMapper
                                            .getImplementationClassName(iface)
                                            .replace('.', '/'));
            implementationNodes.put(iface, implementationNode);
            nodes.add(implementationNode);
        }
        return interfaceNode;
    }

    public void addInterfaceToImplement(Class<?> iface) {
        addInterface(iface).getImplementations().forEach(ImplementationNode::requireImplementation);
    }

    private void compact() {
        while (true) {
            boolean updated = false;
            for (ImplementationNode node : nodes) {
                updated |= node.compact();
            }
            if (!updated) {
                break;
            }
        }
    }

    private void promoteCommonMixins() {
        while (true) {
            boolean updated = false;
            for (ImplementationNode node : nodes) {
                updated |= node.promoteCommonMixins();
            }
            if (!updated) {
                break;
            }
        }
    }

    private void dump(String prefix, boolean showImplName) {
        if (log.isDebugEnabled()) {
            StringBuilder builder = new StringBuilder(prefix);
            builder.append("digraph G {\n  rankdir = BT;\n  node [shape=box];\n");
            for (ImplementationNode node : nodes) {
                node.dump(builder, showImplName);
            }
            builder.append("}");
            log.debug(builder.toString());
        }
    }

    public ClassDefinition[] generate() {
        dump("Initial graph:\n", false);

        nodes.forEach(ImplementationNode::removeIfNotRequired);
        dump("Graph after removing nodes that are not required:\n", false);

        Map<Set<ImplementationNode>, Set<ImplementationNode>> mergableNodes = new HashMap<>();
        for (ImplementationNode node : nodes) {
            mergableNodes
                    .computeIfAbsent(
                            node.getRequiredDescendantsOrSelf(), k -> new LinkedIdentityHashSet<>())
                    .add(node);
        }
        for (Set<ImplementationNode> nodes : mergableNodes.values()) {
            if (nodes.size() > 1) {
                ImplementationNode.merge(nodes);
            }
        }
        dump("Graph after merging nodes:\n", false);

        nodes.forEach(ImplementationNode::reduce);
        dump("Graph after applying transitive reduction:\n", false);

        //        compact();
        //        dump("Graph after compaction:\n", false);

        nodes.forEach(ImplementationNode::ensureSingleParent);
        dump("Graph after removing multiple inheritance:\n", false);

        // TODO: do this in a loop until there are no more updates
        compact();
        promoteCommonMixins();
        compact();
        dump("Final graph:\n", true);

        Map<Class<?>, String> implementationClassNames = new HashMap<>();
        interfaceNodes
                .values()
                .forEach(
                        interfaceNode -> {
                            Set<ImplementationNode> implementationNodes =
                                    interfaceNode.getImplementations();
                            if (implementationNodes.size() == 1) {
                                implementationClassNames.put(
                                        interfaceNode.getInterface(),
                                        implementationNodes.iterator().next().getClassName());
                            }
                        });
        WeavingContext context = new WeavingContextImpl(implementationClassNames);

        List<ClassDefinition> result = new ArrayList<>();
        for (ImplementationNode node : nodes) {
            result.addAll(node.toClassDefinitions(context));
        }
        return result.toArray(new ClassDefinition[result.size()]);
    }

    public ClassLoader toClassLoader(ClassLoader parent) {
        return new WeavingClassLoader(parent, generate());
    }
}
