/*
 * Copyright 2014, The OpenNMS Group
 * 
 * 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.opennms.newts.gsod;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.Stack;
import java.util.zip.GZIPInputStream;

import com.google.common.base.Function;
import com.google.common.base.Throwables;
import com.google.common.collect.AbstractIterator;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.Iterables;

public class FileIterable {
    
    private FileIterable() {}

    public static FluentIterable<Path> fileTreeWalker(final Path root) {
        return FluentIterable.from(Iterables.concat(groupFilesByDir(root)));
    }
    
    public static class KeyedIterable<K, T> extends FluentIterable<T> {
        
        private K m_key;
        private Iterable<T> m_items;
        

        public KeyedIterable(K key, Iterable<T> items) {
            m_key = key;
            m_items = items;
        }

        @Override
        public Iterator<T> iterator() {
            return m_items.iterator();
        }
        
        public K getKey() {
            return m_key;
        }

    }
    
    private static KeyedIterable<Path, Path> children(File dir, FileFilter filter) {
        return new KeyedIterable<>(dir.toPath(), toPaths(dir.listFiles(filter)));
    }

    private static KeyedIterable<Path, Path> files(File dir) {
        return children(dir, fileMatcher());
    }
    
    private static KeyedIterable<Path, Path> subdirs(File dir) {
        return children(dir, directoryMatcher());
    }


    private static FileFilter fileMatcher() {
        return new FileFilter() {
            
            @Override
            public boolean accept(File f) {
                return f.isFile();
            }
        };
    }
    
    private static FileFilter directoryMatcher() {
        return new FileFilter() {
            
            @Override
            public boolean accept(File f) {
                return f.isDirectory();
            }
        };
    }


    public static Iterable<Path> toPaths(File[] files) {
        return files == null ? Collections.<Path>emptyList() : toPaths(Arrays.asList(files));
    }
    
    public static Iterable<Path> toPaths(Iterable<File> files) {
        return Iterables.transform(files, new Function<File, Path>(){

            @Override
            public Path apply(File input) {
                return input.toPath();
            }
            
        });
    }
    
    public static Iterable<File> toFiles(Iterable<Path> files) {
        return Iterables.transform(files, new Function<Path, File>(){

            @Override
            public File apply(Path input) {
                return input.toFile();
            }
            
        });
    }
    
    public static class GroupedPathIterator extends AbstractIterator<KeyedIterable<Path, Path>> {
        
        Stack<Iterator<Path>> m_dirStack;
        File m_root;
        
        GroupedPathIterator(File root) {
            m_root = root;
        }

        @Override
        protected KeyedIterable<Path, Path> computeNext() {
            if (m_dirStack == null) {
                m_dirStack = new Stack<>();
                m_dirStack.push(subdirs(m_root).iterator());
                return files(m_root);
            } 
            
            while(!m_dirStack.isEmpty()) {
                Iterator<Path> subdirs = m_dirStack.peek();
                if (!subdirs.hasNext()) {
                    m_dirStack.pop();
                } else {
                    File dir = subdirs.next().toFile();
                    m_dirStack.push(subdirs(dir).iterator());
                    return files(dir);
                }
            }
            
            endOfData();
            return null;
        }
        
    }
    
    public static FluentIterable<KeyedIterable<Path, Path>> groupFilesByDir(final Path root) {
        return new FluentIterable<KeyedIterable<Path, Path>>() {

            @Override
            public Iterator<KeyedIterable<Path, Path>> iterator() {
                return new GroupedPathIterator(root.toFile());
            }
            
        };

    }
    
    private static class LineIterator implements Iterator<String> {
        
        private BufferedReader m_in;
        private String m_nextLine;
        
        public LineIterator(Reader r) {
            m_in = new BufferedReader(r);
            fetchLine();
        }
        
        private void fetchLine() {
            if (m_in == null) return;
            try {
                m_nextLine = m_in.readLine();
            } catch (IOException e) {
                throw Throwables.propagate(e);
            } finally {
                if (m_nextLine == null) {
                    close();
                }
            }
        }
        
        private void close() {
            try {
                if (m_in != null) {
                    m_in.close();
                }
            } catch(IOException e) {
                throw Throwables.propagate(e);
            } finally {
                m_in = null;
            }
        }


        @Override
        public boolean hasNext() {
            return m_nextLine != null;
        }

        @Override
        public String next() {
            String line = m_nextLine;
            fetchLine();
            return line;
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException("Iterator<String>.remove is not yet implemented.");
        }
        
    }
    
    public static FluentIterable<String> lines(final Path path) {
        return new FluentIterable<String>() {

            @Override
            public Iterator<String> iterator() {
                try {
                    return new LineIterator(new InputStreamReader(new FileInputStream(path.toFile()), StandardCharsets.UTF_8));
                } catch (FileNotFoundException e) {
                    throw Throwables.propagate(e);
                }
            }
        };
        
    }
    
    public static FluentIterable<String> unzipLines(final Path path, final Charset cs) {
        return new FluentIterable<String>() {

            @Override
            public Iterator<String> iterator() {
                try {
                    return new LineIterator(zippedFileReader(path, cs));
                } catch (IOException e) {
                    throw Throwables.propagate(e);
                }
            }
        };
        
    }
    
    private static Reader zippedFileReader(final Path path, final Charset cs)  throws IOException {
        InputStream gzipStream = new GZIPInputStream(new FileInputStream(path.toFile()));
        return new InputStreamReader(gzipStream, cs);
    }
    
    public static <F, T> Function<? super Iterable<F>, Iterable<T>> bind(final Function<? super F, Iterable<T>> f) {
        return new Function<Iterable<F>, Iterable<T>>() {
            @Override
            public Iterable<T> apply(Iterable<F> input) {
                return Iterables.concat(Iterables.transform(input, f));
            }
        };
    }
    
    public static <F, T> Function<? super Iterable<F>, Iterable<T>> lift(final Function<F, T> f) {
        return new Function<Iterable<F>, Iterable<T>>() {

            @Override
            public Iterable<T> apply(Iterable<F> input) {
                return Iterables.transform(input, f);
            }
            
        };
    }
    

}
