/*
 * 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.openejb.core.ivm.naming;

import org.apache.openejb.OpenEJBRuntimeException;

import javax.naming.Context;
import javax.naming.NameAlreadyBoundException;
import javax.naming.NameNotFoundException;
import javax.naming.NamingException;
import java.io.PrintStream;
import java.io.Serializable;
import java.util.ArrayList;

public class NameNode implements Serializable {
    private final String atomicName;
    private final int atomicHash;
    private NameNode lessTree;
    private NameNode grtrTree;
    private NameNode subTree;
    private NameNode parentTree;
    private final NameNode parent;
    private Object myObject;
    private transient IvmContext myContext;
    private boolean unbound;
    private boolean subTreeUnbound;

    public NameNode(final NameNode parent, final ParsedName name, final Object obj, final NameNode parentTree) {
        atomicName = name.getComponent();
        atomicHash = name.getComponentHashCode();
        this.parent = parent;
        this.parentTree = parentTree;
        if (name.next()) {
            subTree = new NameNode(this, name, obj, this);
        } else if (obj instanceof Context) {
            myObject = new Federation();
            ((Federation) myObject).add((Context) obj);
        } else {
            myObject = obj;
        }
    }

    void setMyContext(final IvmContext myContext) {
        this.myContext = myContext;
    }

    public Object getBinding() {
        if (myObject != null && !(myObject instanceof Federation)) {
            return myObject;// if NameNode has an object it must be a binding
        } else {
            if (myContext == null) {
                myContext = new IvmContext(this);
            }
            return myContext;
        }
    }

    public Object resolve(final ParsedName name) throws NameNotFoundException {
        final int compareResult = name.compareTo(atomicHash);
        NameNotFoundException n = null;
        final int pos = name.getPos();
        if (compareResult == ParsedName.IS_EQUAL && name.getComponent().equals(atomicName)) {
            // hashcodes and String values are equal
            if (name.next()) {
                if (subTree != null) {
                    try {
                        return subTree.resolve(name);
                    } catch (final NameNotFoundException e) {
                        n = e;
                    }
                } else if (!subTreeUnbound && !unbound && myContext != null && !Federation.class.isInstance(myObject)) {
                    try {
                        return myContext.mynode.resolve(name);
                    } catch (final NameNotFoundException e) {
                        n = e;
                    }
                }
            } else if (!unbound) {
                return getBinding();
            }
        } else if (compareResult == ParsedName.IS_LESS) {
            // parsed hash is less than
            if (lessTree != null) {
                return lessTree.resolve(name);
            }

        } else {
            //ParsedName.IS_GREATER
            if (grtrTree != null) {
                return grtrTree.resolve(name);
            }
        }
        if (myObject instanceof Federation) {
            name.reset(pos);
            final String nameInContext = name.remaining().path();
            Federation f = null;
            for (final Context c : (Federation) myObject) {
                try {
                    final Object o = c.lookup(nameInContext);
                    if (o instanceof Context) {
                        if (f == null) {
                            f = new Federation();
                        }
                        f.add((Context) o);
                    } else {
                        return o;
                    }
                } catch (final NamingException e) {
                    //ignore
                }
            }
            if (f != null) {
                final NameNode node = new NameNode(null, new ParsedName(""), f, null);
                return new IvmContext(node);
            }
        }
        if (n != null) {
            throw n;
        }
        throw new NameNotFoundException("Cannot resolve " + name);
    }

    public void bind(final ParsedName name, final Object obj) throws NameAlreadyBoundException {
        final int compareResult = name.compareTo(atomicHash);
        if (compareResult == ParsedName.IS_EQUAL && name.getComponent().equals(atomicName)) {
            if (name.next()) {
                if (myObject != null && !(myObject instanceof Federation)) {
                    throw new NameAlreadyBoundException();
                }
                if (subTree == null) {
                    subTree = new NameNode(this, name, obj, this);
                    subTreeUnbound = false;
                } else {
                    subTree.bind(name, obj);
                }
            } else {
                if (obj instanceof Context) {
                    if (myObject != null) {
                        if (!(myObject instanceof Federation)) {
                            throw new NameAlreadyBoundException(name.toString());
                        }
                    } else {
                        myObject = new Federation();
                    }
                    ((Federation) myObject).add((Context) obj);
                } else {
                    if (subTree != null) {
                        throw new NameAlreadyBoundException(name.toString());
                    }
                    if (myObject != null) {
                        throw new NameAlreadyBoundException(name.toString());
                    }
                    unbound = false;
                    myObject = obj;// bind the object to this node
                }
            }
        } else if (compareResult == ParsedName.IS_LESS) {
            if (lessTree == null) {
                lessTree = new NameNode(this.parent, name, obj, this);
            } else {
                lessTree.bind(name, obj);
            }
        } else {
            //ParsedName.IS_GREATER ...
            if (grtrTree == null) {
                grtrTree = new NameNode(this.parent, name, obj, this);
            } else {
                grtrTree.bind(name, obj);
            }
        }
    }

    public void tree(final String indent, final PrintStream out) {
        out.println(atomicName + " @ " + atomicHash + (myObject != null ? " [" + myObject + "]" : ""));

        if (grtrTree != null) {
            out.print(indent + " + ");
            grtrTree.tree(indent + "    ", out);
        }
        if (lessTree != null) {
            out.print(indent + " - ");
            lessTree.tree(indent + "    ", out);
        }
        if (subTree != null) {
            out.print(indent + " - ");
            subTree.tree(indent + "    ", out);
        }
    }


    public int compareTo(final int otherHash) {
        if (atomicHash == otherHash) {
            return 0;
        } else if (atomicHash > otherHash) {
            return 1;
        } else {
            return -1;
        }
    }

    private void bind(final NameNode node) {
        int compareResult = node.compareTo(atomicHash);

        // This seems to be needed because of an inbalanced way
        // in which we use bind/unbind/lookup.  Bind/unbind require
        // the prefix of the node to be appended because lookup
        // will add the prefix when doing a lookup.  Seems if
        // we got rid of the prefix usage in lookup, we could
        // kill this if block and would have more balanced
        // btrees
        if (node.parent == this) {
            compareResult = ParsedName.IS_EQUAL;
        }

        if (compareResult == ParsedName.IS_EQUAL) {
            if (subTree == null) {
                subTree = node;
                subTreeUnbound = false;
                subTree.parentTree = this;
            } else {
                subTree.bind(node);
            }
        } else if (compareResult == ParsedName.IS_LESS) {
            if (lessTree == null) {
                lessTree = node;
                lessTree.parentTree = this;
            } else {
                lessTree.bind(node);
            }
        } else {//ParsedName.IS_GREATER ...

            if (grtrTree == null) {
                grtrTree = node;
                grtrTree.parentTree = this;
            } else {
                grtrTree.bind(node);
            }
        }
    }

    public void unbind(final ParsedName name) throws NameAlreadyBoundException {
        final int compareResult = name.compareTo(atomicHash);
        if (compareResult == ParsedName.IS_EQUAL && name.getComponent().equals(atomicName)) {
            if (name.next()) {
                if (subTree != null) {
                    subTree.unbind(name);
                }
            } else {
                unbound = true;
                myObject = null;
                parentTree.unbind(this);
            }
        } else if (compareResult == ParsedName.IS_LESS) {
            if (lessTree != null) {
                lessTree.unbind(name);
            }
        } else {//ParsedName.IS_GREATER ...

            if (grtrTree != null) {
                grtrTree.unbind(name);
            }
        }
    }

    private void unbind(final NameNode node) {
        if (subTree == node) {
            subTree = null;
            subTreeUnbound = true;
        } else if (grtrTree == node) {
            grtrTree = null;
        } else if (lessTree == node) {
            lessTree = null;
        }
        rebalance(node);
    }

    private void rebalance(final NameNode node) {
        if (node.subTree != null) {
            this.bind(node.subTree);
        }
        if (node.lessTree != null) {
            this.bind(node.lessTree);
        }
        if (node.grtrTree != null) {
            this.bind(node.grtrTree);
        }
    }

    protected void prune() {
        prune(this);
    }

    private void prune(final NameNode until) {
        if (subTree != null) {
            subTree.prune(until);
        }
        if (lessTree != null) {
            lessTree.prune(until);
        }
        if (grtrTree != null) {
            grtrTree.prune(until);
        }

        if (this == until) {
            return;
        }

        if (!hasChildren() && myObject == null) {
            parentTree.unbind(this);
        }
    }

    private boolean hasChildren() {
        return hasChildren(this);
    }

    private boolean hasChildren(final NameNode node) {
        if (subTree != null && subTree.hasChildren(node)) {
            return true;
        }
        if (grtrTree != null && grtrTree.hasChildren(node)) {
            return true;
        }
        if (lessTree != null && lessTree.hasChildren(node)) {
            return true;
        }

        return parent == node;
    }

    protected void clearCache() {
        if (myContext != null) {
            myContext.fastCache.clear();
        }
        if (grtrTree != null) {
            grtrTree.clearCache();
        }
        if (lessTree != null) {
            lessTree.clearCache();
        }
        if (subTree != null) {
            subTree.clearCache();
        }
    }

    public IvmContext createSubcontext(final ParsedName name) throws NameAlreadyBoundException {
        try {
            bind(name, null);
            name.reset();
            return (IvmContext) resolve(name);
        } catch (final NameNotFoundException exception) {
            exception.printStackTrace();
            throw new OpenEJBRuntimeException(exception);
        }
    }

    public String getAtomicName() {
        return atomicName;
    }

    public NameNode getLessTree() {
        return lessTree;
    }

    public NameNode getGrtrTree() {
        return grtrTree;
    }

    public NameNode getSubTree() {
        return subTree;
    }

    public NameNode getParent() {
        return parent;
    }


    @Override
    public String toString() {
        return "NameNode{" +
            "atomicName='" + atomicName + '\'' +
            ", atomicHash=" + atomicHash +
            ", lessTree=" + (lessTree != null ? lessTree.atomicName : "null") +
            ", grtrTree=" + (grtrTree != null ? grtrTree.atomicName : "null") +
            ", subTree=" + (subTree != null ? subTree.atomicName : "null") +
            ", parentTree=" + (parentTree != null ? parentTree.atomicName : "null") +
            ", parent=" + (parent != null ? parent.atomicName : "null") +
            ", myObject=" + myObject +
            ", myContext=" + myContext +
            ", unbound=" + unbound +
            '}';
    }

    private static class Federation extends ArrayList<Context> {
    }

    ;
}
