/*
 * xtc - The eXTensible Compiler
 * Copyright (C) 2004, 2006 Robert Grimm
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * version 2 as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
 * USA.
 */
package xtc.util;

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;

import java.util.EventListener;
import java.util.LinkedList;

/**
 * Implementation of a nested reader.  A nested reader combines
 * several streams into a single stream.  It starts by reading from a
 * main stream.  Additional streams are added through the {@link
 * #insert(Reader)} and {@link
 * #insert(Reader,NestedReader.EOFListener)} methods and are consumed
 * completely before returning to read from the previous stream.  Note
 * that inserted streams are automatically closed after having being
 * consumed.  Further note that closing a nested reader closes all
 * streams currently associated with that nested reader.
 *
 * @author Robert Grimm
 * @version $Revision: 1.5 $
 */
public class NestedReader extends Reader {

  /**
   * Event listener to provide notification a stream has reached its
   * end.
   *
   * @see #insert(Reader,NestedReader.EOFListener)
   */
  public static interface EOFListener extends EventListener {

    /** Signal that all characters have been consumed. */
    public void consumed();
  
  }

  /** Flag for whether this nested reader has been closed. */
  protected boolean closed;
    
  /** The current character stream. */
  protected Reader reader;

  /** The corresponding end-of-file listener. */
  protected EOFListener listener;

  /**
   * The stack of readers, with the most recently added stream at the
   * front.
   */
  protected LinkedList<Reader> readerStack;

  /**
   * The stack of listeners, with the most recently added listener at
   * the front.
   */
  protected LinkedList<EOFListener> listenerStack;

  /**
   * Create a new nested reader.
   *
   * @param in The main stream.
   */
  public NestedReader(Reader in) {
    closed        = false;
    reader        = in;
    listener      = null;
    readerStack   = new LinkedList<Reader>();
    listenerStack = new LinkedList<EOFListener>();
  }

  /**
   * Open the specified file.  The implementation of this method
   * simply creates a new file reader with the specified file name.
   *
   * @param file The file name.
   * @return The corresponding character stream.
   * @throws IOException Signals an I/O error.
   */
  public Reader open(String file) throws IOException {
    return new BufferedReader(new FileReader(file));
  }

  /**
   * Insert the specified character stream.  After reading all
   * characters from the specified stream, this nested reader silently
   * returns to reading characters from the current stream.
   * 
   * @param in The stream.
   * @throws IOException Signals an I/O error.
   */
  public void insert(Reader in) throws IOException {
    insert(in, null);
  }

  /**
   * Insert the specified character stream.  After reading all
   * characters from the specified stream, but before returning to
   * read characters from the current stream, this nested reader
   * {@link NestedReader.EOFListener#consumed() notifies} the
   * specified listener.
   *
   * @param in The stream.
   * @param eof The listener to be notified when the specified stream
   *   has been consumed.
   * @throws IOException Signals an I/O error.
   */
  public void insert(Reader in, EOFListener eof) throws IOException {
    synchronized (lock) {
      if (closed) {
        throw new IOException("Nested reader closed");
      }
      readerStack.addFirst(reader);
      listenerStack.addFirst(listener);
      reader   = in;
      listener = eof;
    }
  }

  /**
   * Restore the previous character stream.  This method must be
   * called while holding the {@link #lock}.
   *
   * @throws IOException Signals an I/O error.
   */
  private void restore() throws IOException {
    // Notify the listener and close the current stream.
    if (null != listener) {
      listener.consumed();
    }
    reader.close();

    // Actually restore the previous stream.
    reader   = readerStack.removeFirst();
    listener = listenerStack.removeFirst();
  }

  public int read() throws IOException {
    synchronized (lock) {
      do {
        int result = reader.read();

        // Return on a character or the end-of-file for the main stream.
        if ((-1 != result) || readerStack.isEmpty()) {
          return result;
        }

        // Restore the previous stream.
        restore();

        // Try again.
      } while (true);
    }
  }

  public int read(char[] cbuf, int off, int len) throws IOException {
    synchronized (lock) {
      do {
        int result = reader.read(cbuf, off, len);

        // Return on characters or the end-of-file for the main stream.
        if ((-1 != result) || readerStack.isEmpty()) {
          return result;
        }

        // Restore the previous stream.
        restore();

        // Try again.
      } while (true);
    }
  }

  public void close() throws IOException {
    synchronized (lock) {
      if (closed) {
        return;
      } else {
        closed = true;
      }

      IOException error = null;

      try {
        reader.close();
      } catch (IOException x) {
        error = x;
      }

      while (! readerStack.isEmpty()) {
        reader = readerStack.removeFirst();
        try {
          reader.close();
        } catch (IOException x) {
          error = x;
        }
        listenerStack.removeFirst();
      }

      if (null != error) {
        throw error;
      }
    }
  }

}
