/*
 *  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
 *
 *      https://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 T root;

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

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

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

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

    protected abstract InputStream getInputStream(T f) throws IOException;

    protected abstract URI buildBundleURI(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<>();

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

        private T currentDir = null;

        FSManifestIterator() {
            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 (dirs.peek().hasNext()) {
                        currentDir = 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()) {
                    T bundleCandidate = bundleCandidates.next();
                    try (JarInputStream in = new JarInputStream(getInputStream(bundleCandidate))) {
                        Manifest manifest = in.getManifest();
                        if (manifest != null) {
                            next = new ManifestAndLocation(manifest,
                                    buildBundleURI(bundleCandidate), null);
                        } 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);
                    }
                } 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.<T> emptyList().iterator());
                    }
                    currentDir = null;
                }
            }
            return true;
        }

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

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