/*
 *  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.ivy.osgi.repo;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Stack;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;

import org.apache.ivy.util.Message;

// T is the type of the resource "path"
public abstract class AbstractFSManifestIterable /* <T> implements Iterable/* <ManifestAndLocation> */{

    private final Object/* T */root;

    public AbstractFSManifestIterable(Object /* T */root) {
        this.root = root;
    }

    public Iterator/* <ManifestAndLocation> */iterator() {
        return new FSManifestIterator(root);
    }

    abstract protected List/* <T> */listBundleFiles(Object/* T */dir) throws IOException;

    abstract protected List/* <T> */listDirs(Object/* T */dir) throws IOException;

    abstract protected InputStream getInputStream(Object/* T */f) throws IOException;

    abstract protected URI buildBundleURI(Object/* T */location) throws IOException;

    class FSManifestIterator implements Iterator/* <ManifestAndLocation> */{

        private ManifestAndLocation next = null;

        /**
         * Stack of list of directories. An iterator in the stack represents the current directory
         * being lookup. The first element in the stack is the root directory. The next element in
         * the stack is an iterator on the children on the root. The last iterator in the stack
         * points to {@link #currentDir}.
         */
        private Stack/* <Iterator<T>> */dirs = new Stack/* <Iterator<T>> */();

        /**
         * The bundles files being lookup.
         */
        private Iterator/* <T> */bundleCandidates = null;

        private Object/* T */currentDir = null;

        FSManifestIterator(Object /* T */root) {
            dirs.add(Collections.singleton(root).iterator());
        }

        /**
         * Deep first tree lookup for the directories and the bundles are searched on each found
         * directory.
         */
        public boolean hasNext() {
            while (next == null) {
                // no current directory
                if (currentDir == null) {
                    // so get the next one
                    if (((Iterator) dirs.peek()).hasNext()) {
                        currentDir = ((Iterator) dirs.peek()).next();
                        try {
                            bundleCandidates = listBundleFiles(currentDir).iterator();
                        } catch (IOException e) {
                            Message.warn("Unlistable dir: " + currentDir + " (" + e + ")");
                            currentDir = null;
                        }
                    } else if (dirs.size() <= 1) {
                        // no next directory, but we are at the root: finished
                        return false;
                    } else {
                        // remove the top of the stack and continue with a sibling.
                        dirs.pop();
                    }
                } else if (bundleCandidates.hasNext()) {
                    Object/* T */bundleCandidate = bundleCandidates.next();
                    JarInputStream in = null;
                    try {
                        in = new JarInputStream(getInputStream(bundleCandidate));
                        Manifest manifest = in.getManifest();
                        if (manifest != null) {
                            next = new ManifestAndLocation(manifest,
                                    buildBundleURI(bundleCandidate));
                        } else {
                            Message.debug("No manifest in jar: " + bundleCandidate);
                        }
                    } catch (FileNotFoundException e) {
                        Message.debug("Jar file just removed: " + bundleCandidate + " (" + e + ")");
                    } catch (IOException e) {
                        Message.warn("Unreadable jar: " + bundleCandidate + " (" + e + ")");
                    } finally {
                        if (in != null) {
                            try {
                                in.close();
                            } catch (IOException e) {
                                // Don't care
                            }
                        }
                    }
                } else {
                    // no more candidate on the current directory
                    // so lookup in the children directories
                    try {
                        dirs.add(listDirs(currentDir).iterator());
                    } catch (IOException e) {
                        Message.warn("Unlistable dir: " + currentDir + " (" + e + ")");
                        dirs.add(Collections.EMPTY_LIST.iterator());
                    }
                    currentDir = null;
                }
            }
            return true;
        }

        public Object next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            ManifestAndLocation manifest = next;
            next = null;
            return manifest;
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }
    }
}
