/*
 * $Header: /home/cvspublic/jakarta-slide/proposals/tamino/src/util/org/apache/slide/util/nodeset/NodeSet.java,v 1.1 2004/03/25 16:18:12 juergen Exp $
 * $Revision: 1.1 $
 * $Date: 2004/03/25 16:18:12 $
 *
 * ====================================================================
 *
 * Copyright 1999-2004 The Apache Software Foundation.
 *
 * Licensed 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.slide.util.nodeset;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.oro.text.regex.Pattern;
import org.apache.slide.util.Files;
import org.apache.slide.util.Strings;

/**
 * <p>Selects nodes from trees. If the tree is the file system, this class is similar to Jakarta
 * Ant's FileSets. A set is basically a list of paths to include or exclude. In addition,
 * predicated can be used to further restrict the collected nodes. </p>
 *
 * <p>Usage. Create a new instance, use the various selections methods (include, exclude, etc.),
 * and run one of the collection methods (list, invoke, ...). Selection methods return
 * <code>this</code> to allow expressions.</p>
 *
 * <p>A paths is a list of names, separated as configured in the underlying adapter.
 * Paths must *not* start with a separator, i.e they have to be relative. Paths remain relative
 * until the Set is actually applied to a tree. Paths must not end with a separator either. </p>
 *
 * <p>Names use the familar glob syntax. Sets do not know about extensions. </p>
 */
public class NodeSet {
    public static final int DEPTH_INFINITE = Integer.MAX_VALUE;
    //--
    
    private final Adapter adapter;
    
    /** List of compiled paths. */
    private final List includes;
    
    /** List of compiled paths. */
    private final List excludes;
    
    private final List predicates;
    
    private boolean ignoreCase;
    
    private int minDepth;
    private int maxDepth;
    
    public NodeSet() {
        this(FileAdapter.INSTANCE);
    }
    
    public NodeSet(Adapter adapter) {
        this.adapter = adapter;
        this.includes = new ArrayList();
        this.excludes = new ArrayList();
        this.predicates = new ArrayList();
        this.ignoreCase = false;
        this.minDepth = adapter.getDefaultMinDepth();
        this.maxDepth = adapter.getDefaultMaxDepth();
    }
    
    public NodeSet(NodeSet orig) {
        this.adapter = orig.adapter;
        this.includes = new ArrayList(orig.includes);
        this.excludes = new ArrayList(orig.excludes);
        this.predicates = new ArrayList(orig.predicates); // TODO: not a deep clone ...
        this.ignoreCase = orig.ignoreCase;
        this.minDepth = orig.minDepth;
        this.maxDepth = orig.maxDepth;
    }
    
    //-- selections methods
    
    /** Does *not* affect previous calles to include/exclude */
    public NodeSet ignoreCase() {
        ignoreCase = true;
        return this;
    }
    
    public NodeSet minDepth(int minDepth) {
        this.minDepth = minDepth;
        return this;
    }
    
    public NodeSet maxDepth(int maxDepth) {
        this.maxDepth = maxDepth;
        return this;
    }
    
    public NodeSet predicate(Predicate p) {
        predicates.add(p);
        return this;
    }
    
    public NodeSet include(String path) {
        includes.add(compile(path));
        return this;
    }
    
    public NodeSet includes(String[] paths) {
        int i;
        
        for (i = 0; i < paths.length; i++) {
            include(paths[i]);
        }
        return this;
    }
    
    public NodeSet includeName(String name) {
        include(Files.join("**", name));
        return this;
    }
    
    public NodeSet includeNames(String[] names) {
        int i;
        
        for (i = 0; i < names.length; i++) {
            includeName(names[i]);
        }
        return this;
    }
    
    public NodeSet exclude(String path) {
        excludes.add(compile(path));
        return this;
    }
    
    public NodeSet excludes(String[] paths) {
        int i;
        
        for (i = 0; i < paths.length; i++) {
            exclude(paths[i]);
        }
        return this;
    }
    
    public NodeSet excludeName(String name) {
        exclude(Files.join("**", name));
        return this;
    }
    
    public NodeSet excludeNames(String[] names) {
        int i;
        
        for (i = 0; i < names.length; i++) {
            includeName(names[i]);
        }
        return this;
    }
    
    //-- select helper methods
    
    private Object[] compile(String path) {
        String[] lst;
        
        lst = Strings.split(path, adapter.getSeparator());
        if (lst.length == 0) {
            throw new IllegalArgumentException("empty path: " + path);
        }
        if (lst[0].equals("")) {
            throw new IllegalArgumentException("absolute path not allowed: " + path);
        }
        if (lst[lst.length - 1].equals("")) {
            throw new IllegalArgumentException(
                "path must not end with " + File.separator + ": " + path);
        }
        return compileTail(lst, 0);
    }
    
    /**
     * @param lst  array of patterns
     */
    private Object[] compileTail(String[] lst, int start) {
        Pattern head;
        Object[] tail;
        
        if (start == lst.length) {
            return null;
        } else {
            head = Glob.compile(lst[start], ignoreCase);
            tail = compileTail(lst, start + 1);
            if (head == Glob.STARSTAR) {
                if (tail == null) {
                    throw new IllegalArgumentException("** must be followed by some content");
                }
                if (tail[0] == Glob.STARSTAR) {
                    throw new IllegalArgumentException("**/** is not allowed");
                }
            }
        }
        return new Object[] { head, tail };
    }
    
    
    //-- collect methods
    
    /**
     * @return List of Nodes.
     */
    public List list(Object root) throws IOException {
        List lst;
        
        lst = new ArrayList();
        list(root, lst);
        return lst;
    }
    
    public void list(Object root, final List result) throws IOException {
        invoke(root, collectTask(result));
    }
    
    public static Task collectTask(final List result) {
        return new Task() {
            public void invoke(Object node) {
                result.add(node);
            }
        };
    }
    
    /**
     * Main methods of this class.
     *
     * @throws IOException as thrown by the specified FileTask
     */
    public void invoke(Object root, Task task) throws IOException {
        doInvoke(0, root, new ArrayList(includes), new ArrayList(excludes), task);
    }
    
    private void doInvoke(int currentDepth, Object node, List includes, List excludes, Task task)
        throws IOException
    {
        Object[] files;
        int i;
        List remainingIncludes;
        List remainingExcludes;
        String name;
        boolean in;
        boolean ex;
        
        if (currentDepth > maxDepth || includes.size() == 0 || excludesAll(excludes)) {
            return;
        }
        if (currentDepth >= minDepth) {
            name = adapter.getName(node);
            remainingIncludes = new ArrayList();
            remainingExcludes = new ArrayList();
            in = doMatch(name, includes, remainingIncludes);
            ex = doMatch(name, excludes, remainingExcludes);
            if (in && !ex && matchPredicates(node)) {
                task.invoke(node);
            }
        } else {
            remainingIncludes = includes;
            remainingExcludes = excludes;
        }
        files = adapter.getChildren(node);
        for (i = 0; i < files.length; i++) {
            doInvoke(currentDepth + 1, files[i], remainingIncludes, remainingExcludes, task);
        }
    }
    
    private boolean matchPredicates(Object node) {
        int i;
        int max;
        Predicate p;
        
        max = predicates.size();
        for (i = 0; i < max; i++) {
            p = (Predicate) predicates.get(i);
            if (!p.matches(node)) {
                return false;
            }
        }
        return true;
    }
    
    private static boolean excludesAll(List excludes) {
        int i;
        int max;
        Object[] pair;
        Object[] tail;
        
        max = excludes.size();
        for (i = 0; i < max; i++) {
            pair = (Object[]) excludes.get(i);
            tail = (Object[]) pair[1];
            if (pair[0] == Glob.STARSTAR && tail[0] == Glob.STAR) {
                return true;
            }
        }
        return false;
    }
    
    private static boolean doMatch(String name, List paths, List remainingPaths) {
        boolean found;
        int i;
        int max;
        Object[] path;
        Pattern head;
        Object[] tail;
        
        found = false;
        max = paths.size();
        for (i = 0; i < max; i++) {
            path = (Object[]) paths.get(i);
            if (path == null) {
                throw new IllegalStateException("unexpected empty path");
            }
            head = (Pattern) path[0];
            tail = (Object[]) path[1];
            if (head == Glob.STARSTAR) {
                remainingPaths.add(path);
                head = (Pattern) tail[0];
                tail = (Object[]) tail[1];
            }
            if (Glob.matches(head, name)) {
                if (tail != null) {
                    remainingPaths.add(tail);
                } else {
                    found = true;
                }
            }
        }
        return found;
    }
    
    public String toString() {
        return "includes=" + includes + ", excludes=" + excludes;
    }
}

