/**
 * Copyright 2009, Google Inc.  All rights reserved.
 * Licensed to PSF under a Contributor Agreement.
 */
package org.python.indexer;

import org.python.indexer.NBinding;
import org.python.indexer.Util;
import org.python.indexer.types.NModuleType;
import org.python.indexer.types.NType;
import org.python.indexer.types.NUnionType;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

/**
 * Generates a file outline from the index: a structure representing the
 * variable and attribute definitions in a file.
 */
public class Outliner {

    public static abstract class Entry {
        protected String qname;  // entry qualified name
        protected int offset;  // file offset of referenced declaration
        protected NBinding.Kind kind;  // binding kind of outline entry

        public Entry() {
        }

        public Entry(String qname, int offset, NBinding.Kind kind) {
            this.qname = qname;
            this.offset = offset;
            this.kind = kind;
        }

        public abstract boolean isLeaf();
        public Leaf asLeaf() {
            return (Leaf)this;
        }

        public abstract boolean isBranch();
        public Branch asBranch() {
            return (Branch)this;
        }

        public abstract boolean hasChildren();
        public abstract List<Entry> getChildren();
        public abstract void setChildren(List<Entry> children);

        public String getQname() {
            return qname;
        }
        public void setQname(String qname) {
            if (qname == null) {
                throw new IllegalArgumentException("qname param cannot be null");
            }
            this.qname = qname;
        }

        /**
         * Returns the file offset of the beginning of the identifier referenced
         * by this outline entry.
         */
        public int getOffset() {
            return offset;
        }
        public void setOffset(int offset) {
            this.offset = offset;
        }

        public NBinding.Kind getKind() {
            return kind;
        }
        public void setKind(NBinding.Kind kind) {
            if (kind == null) {
                throw new IllegalArgumentException("kind param cannot be null");
            }
            this.kind = kind;
        }

        /**
         * Returns the simple (unqualified) name of the identifier.
         */
        public String getName() {
            String[] parts = qname.split("[.&@%]");
            return parts[parts.length-1];
        }

        @Override public String toString() {
            StringBuilder sb = new StringBuilder();
            toString(sb, 0);
            return sb.toString().trim();
        }

        public void toString(StringBuilder sb, int depth) {
            for (int i = 0; i < depth; i++) {
                sb.append("  ");
            }
            sb.append(getKind());
            sb.append(" ");
            sb.append(getName());
            sb.append("\n");
            if (hasChildren()) {
                for (Entry e : getChildren()) {
                    e.toString(sb, depth + 1);
                }
            }
        }
    }

    /**
     * An outline entry with children.
     */
    public static class Branch extends Entry {
        private List<Entry> children = new ArrayList<Entry>();

        public Branch() {
        }
        public Branch(String qname, int start, NBinding.Kind kind) {
            super(qname, start, kind);
        }
        public boolean isLeaf() {
            return false;
        }
        public boolean isBranch() {
            return true;
        }
        public boolean hasChildren() {
            return children != null && !children.isEmpty();
        }
        public List<Entry> getChildren() {
            return children;
        }
        public void setChildren(List<Entry> children) {
            this.children = children;
        }
    }

    /**
     * An entry with no children.
     */
    public static class Leaf extends Entry {
        public boolean isLeaf() {
            return true;
        }
        public boolean isBranch() {
            return false;
        }

        public Leaf() {
        }

        public Leaf(String qname, int start, NBinding.Kind kind) {
            super(qname, start, kind);
        }
        public boolean hasChildren() {
            return false;
        }
        public List<Entry> getChildren() {
            return new ArrayList<Entry>();
        }
        public void setChildren(List<Entry> children) {
            throw new UnsupportedOperationException("Leaf nodes cannot have children.");
        }
    }

    /**
     * Create an outline for a file in the index.
     * @param scope the file scope
     * @param path the file for which to build the outline
     * @return a list of entries constituting the file outline.
     * Returns an empty list if the indexer hasn't indexed that path.
     */
    public List<Entry> generate(Indexer idx, String abspath) throws Exception {
        NModuleType mt = idx.getModuleForFile(abspath);
        if (mt == null) {
            return new ArrayList<Entry>();
        }
        return generate(mt.getTable(), abspath);
    }

    /**
     * Create an outline for a symbol table.
     * @param scope the file scope
     * @param path the file for which we're building the outline
     * @return a list of entries constituting the outline
     */
    public List<Entry> generate(Scope scope, String path) {
        List<Entry> result = new ArrayList<Entry>();

        Set<NBinding> entries = new TreeSet<NBinding>();
        for (NBinding nb : scope.values()) {
            if (!nb.isSynthetic()
                && !nb.isBuiltin()
                && !nb.getDefs().isEmpty()
                && path.equals(nb.getSignatureNode().getFile())) {
                entries.add(nb);
            }
        }

        for (NBinding nb : entries) {
            Def signode = nb.getSignatureNode();
            List<Entry> kids = null;

            if (nb.getKind() == NBinding.Kind.CLASS) {
                NType realType = nb.followType();
                if (realType.isUnionType()) {
                    for (NType t : realType.asUnionType().getTypes()) {
                        if (t.isClassType()) {
                            realType = t;
                            break;
                        }
                    }
                }
                kids = generate(realType.getTable(), path);
            }

            Entry kid = kids != null ? new Branch() : new Leaf();
            kid.setOffset(signode.start());
            kid.setQname(nb.getQname());
            kid.setKind(nb.getKind());

            if (kids != null) {
                kid.setChildren(kids);
            }
            result.add(kid);
        }
        return result;
    }
}
