package edu.oregonstate.cartography.grid;

import edu.oregonstate.cartography.gui.ProgressIndicator;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class EsriASCIIGridReader {

    volatile private Exception producerConsumerException = null;
    
    private EsriASCIIGridReader() {
    }

    /**
     * Returns whether a reader references valid data that can be read.
     * @param br
     * @return
     */
    public static boolean canRead(BufferedReader br) {
        try {
            GridHeaderImporter header = new GridHeaderImporter();
            header.readHeader(br, true);
            return header.isValid();
        } catch (IOException exc) {
            return false;
        }
    }

    /**
     * Returns whether a file references valid data that can be rea
     * @param filePath
     * @return 
     */
    public static boolean canRead(String filePath) {
        BufferedReader br = null;
        try {
            File file = new File(filePath);
            FileInputStream fis = new FileInputStream(file.getAbsolutePath());
            InputStreamReader in = new InputStreamReader(fis);
            br = new BufferedReader(in);
            return EsriASCIIGridReader.canRead(br);
        } catch (FileNotFoundException exc) {
            return false;
        } finally {
            if (br != null) {
                try {
                    br.close();
                } catch (Throwable exc) {
                }
            }
        }
    }

    /** Read a Grid from a file in ESRI ASCII format.
     * @param filePath The path to the file to be read.
     * @return The read grid.
     * @throws java.io.IOException
     */
    public static Grid read(String filePath) throws java.io.IOException {
        return EsriASCIIGridReader.read(filePath, null);
    }

    /** Read a Grid from a file in ESRI ASCII format.
     * @param filePath The path to the file to be read.
     * @param progressIndicator A WorkerProgress to inform about the progress.
     * @return The read grid.
     * @throws java.io.IOException
     */
    public static Grid read(String filePath, ProgressIndicator progressIndicator)
            throws java.io.IOException {

        File file = new File(filePath);
        InputStream fis = new FileInputStream(file.getAbsolutePath());
        EsriASCIIGridReader esriReader = new EsriASCIIGridReader();
        Grid grid = esriReader.read(fis, progressIndicator);
        if (progressIndicator != null && progressIndicator.isCancelled()) {
            return null;
        }
        return grid;
    }

    /** Read a grid from an InputStream.
     * @param input The stream to read from.
     * @param progressIndicator A WorkerProgress to inform about the progress.
     * @return The read grid.
     * @throws java.io.IOException
     */
    public static Grid readStream(InputStream input, ProgressIndicator progressIndicator)
            throws IOException {
        
        EsriASCIIGridReader esriReader = new EsriASCIIGridReader();
        Grid grid = esriReader.read(input, progressIndicator);
        if (progressIndicator != null && progressIndicator.isCancelled()) {
            return null;
        }
        return grid;

    }

    /** Read a grid from a stream in ESRI ASCII format.
     * @param input The stream to read from. The stream is closed at the end.
     * @param progressIndicator A WorkerProgress to inform about the progress.
     * @return The read grid.
     * @throws java.io.IOException
     */
    public Grid read(InputStream input, ProgressIndicator progressIndicator)
            throws IOException {

        // initialize the progress monitor at the beginning
        if (progressIndicator != null) {
            progressIndicator.start();
        }

        BufferedReader br = null;

        try {
            InputStreamReader in = new InputStreamReader(input);
            br = new BufferedReader(in);
            GridHeaderImporter header = new GridHeaderImporter();
            String firstGridLine = header.readHeader(br, true);
            Grid grid = new Grid(header.getCols(), header.getRows(), header.getCellSize());
            grid.setWest(header.getWest());
            grid.setSouth(header.getSouth());

            // http://www.java2s.com/Code/Java/Threads/ProducerconsumerforJ2SE15usingconcurrent.htm
            // a limited capacity of around 64 works fastest on the system used
            // for development with large grids
            BlockingQueue<String> q = new LinkedBlockingQueue<>(64);
            q.put(firstGridLine);
            
            Producer producer = new Producer(q, br);
            Thread producerThread = new Thread(producer);
            producerThread.start();
            
            Consumer consumer = new Consumer(q, grid, header.getNoDataValue(), progressIndicator);
            Thread consumerThread = new Thread(consumer);
            consumerThread.start();
            
            try {
                producerThread.join();
            } catch (InterruptedException ex) {
                consumerThread.interrupt();
            }
            consumerThread.join();
            
            return grid;
        } catch (InterruptedException ex) {
            return null;
        } finally {
            try {
                if (br != null) {
                    br.close();
                }
            } catch (IOException exc) {
            }
            
            if (producerConsumerException != null) {
                throw new IOException(producerConsumerException);
            }
        }

    }

    /**
     * Indicates end of file.
     */
    private static final String EOF = "END_OF_FILE";
    
    /**
     * Reads the grid body line by line.
     */
    private class Producer implements Runnable {

        private final BlockingQueue<String> queue;
        private final BufferedReader reader;

        Producer(BlockingQueue<String> queue, BufferedReader reader) {
            this.queue = queue;
            this.reader = reader;
        }

        @Override
        public void run() {
            try {
                // read file line by line and store read lines in blocking queue
                String line;
                while ((line = reader.readLine()) != null
                        // check whether this thread has been interrupted
                        && !Thread.currentThread().isInterrupted()
                        // check whether consumer thread has encountered an exception
                        && producerConsumerException == null) {
                    queue.put(line);
                }
                
                // add end-of-file object
                queue.put(EOF);
            } catch (IOException | InterruptedException ex) {
                // store the exception for the main thread
                producerConsumerException = ex;
            }
        }
    }

    /**
     * Parses grid lines.
     */
    private class Consumer implements Runnable {

        private final BlockingQueue<String> queue;
        private final float noDataValue;
        private final Grid grid;
        private final ProgressIndicator progressIndicator;
        private int counter = 0;

        Consumer(BlockingQueue<String> queue, Grid grid, float noDataValue, ProgressIndicator progressIndicator) {
            this.queue = queue;
            this.grid = grid;
            this.noDataValue = noDataValue;
            this.progressIndicator = progressIndicator;
        }

        @Override
        public void run() {
            try {
                final int nCols = grid.getCols();
                final int nRows = grid.getRows();
                final int nbrValues = nRows * nCols;
                do {
                    String str = queue.take();
                    
                    // test for end of file
                    if (EOF.equals(str)) {
                        // make sure the correct number of values has been read
                        if (counter != nbrValues) {
                            throw new IOException("invalid Esri Ascii grid file");
                        }
                        break;
                    }
                    
                    // split each line in tokens and parse the tokens for a float.
                    // One row in the grid might not correspond to a grid row.
                    // StringTokenizer is faster than String.split()
                    // About 50% of the time in this spent with tokenizing the string,
                    // and 50% is spent with Float.parseFloat().
                    StringTokenizer tokenizer = new StringTokenizer(str, " \t");
                    while (tokenizer.hasMoreTokens()
                            // check whether this thread has been interrupted
                            && !Thread.currentThread().isInterrupted()
                            // check whether producer thread has encountered an exception
                            && producerConsumerException == null) {
                        int col = counter % nCols;
                        int row = counter / nCols;
                        ++counter;
                        
                        // make sure we do not read too many cell values
                        if (counter > nbrValues) {
                            throw new IOException("corrupt Esri Ascii grid file");
                        }
                        float v = Float.parseFloat(tokenizer.nextToken());
                        grid.setValue(v == noDataValue ? Float.NaN : v, col, row);
                    }
                    
                    // update progress info
                    if (progressIndicator != null) {
                        int row = counter / nCols;
                        int perc = (int) ((row + 1d) / nRows * 100);
                        if (!progressIndicator.progress(perc)) {
                            counter = nbrValues;
                            break;
                        }
                    }
                } while (counter < nbrValues
                        // check whether this thread has been interrupted
                        && !Thread.currentThread().isInterrupted()
                        // check whether producer thread has encountered an exception
                        && producerConsumerException == null);
            } catch (IOException | InterruptedException | NumberFormatException ex) {
                // store the exception for the main thread
                producerConsumerException = ex;
            }
        }
    }
}
