/*                     __                                               *\
**     ________ ___   / /  ___     Scala API                            **
**    / __/ __// _ | / /  / _ |    (c) 2003-2009, LAMP/EPFL             **
**  __\ \/ /__/ __ |/ /__/ __ |    http://scala-lang.org/               **
** /____/\___/_/ |_/____/_/ | |                                         **
**                          |/                                          **
\*                                                                      */

// $Id: Source.scala 18500 2009-08-17 21:16:39Z extempore $


package scala.io

import java.io.{ FileInputStream, InputStream, PrintStream, File => JFile }
import java.net.{ URI, URL }

/** This object provides convenience methods to create an iterable
 *  representation of a source file.
 *
 *  @author  Burak Emir, Paul Phillips
 *  @version 1.0, 19/08/2004
 */
object Source {
  val DefaultBufSize = 2048
  
  /** Creates a <code>Source</code> from System.in.
   */
  def stdin = fromInputStream(System.in)
  
  /** Creates a <code>Source</code> from an Iterable.
   *
   *  @param    iterable  the Iterable
   *  @return   the <code>Source</code> instance.
   */
  def fromIterable(iterable: Iterable[Char]): Source = new Source {
    val iter = iterable.iterator
  } withReset(() => fromIterable(iterable))

  /** Creates a <code>Source</code> instance from a single character.
   *
   *  @param c ...
   *  @return  the create <code>Source</code> instance.
   */
  def fromChar(c: Char): Source = fromIterable(Array(c))

  /** creates Source from array of characters, with empty description.
   *
   *  @param chars ...
   *  @return      ...
   */
  def fromChars(chars: Array[Char]): Source = fromIterable(chars)

  /** creates Source from string, with empty description.
   *
   *  @param s ...
   *  @return  ...
   */
  def fromString(s: String): Source = fromIterable(s)

  /** Create a <code>Source</code> from array of bytes, decoding
   *  the bytes according to codec.
   *
   *  @param bytes ...
   *  @param enc   ...
   *  @return      the created <code>Source</code> instance.
   */
  def fromBytes(bytes: Array[Byte])(implicit codec: Codec = Codec.default): Source =
    fromString(new String(bytes, codec.name))
  
  /** Create a <code>Source</code> from array of bytes, assuming
   *  one byte per character (ISO-8859-1 encoding.)
   */
  def fromRawBytes(bytes: Array[Byte]): Source = fromString(new String(bytes, Codec.ISO8859.name))

  /** creates Source from file with given name, setting
   *  its description to filename.
   */
  def fromPath(name: String)(implicit codec: Codec = Codec.default): Source = fromFile(new JFile(name))

  /** creates <code>Source</code> from file with given file: URI
   */
  def fromURI(uri: URI)(implicit codec: Codec = Codec.default): Source = fromFile(new JFile(uri))
  
  /** same as fromInputStream(url.openStream())(codec)
   */
  def fromURL(url: URL)(implicit codec: Codec = Codec.default): Source =
    fromInputStream(url.openStream())(codec)

  /** Creates Source from <code>file</code>, using given character encoding,
   *  setting its description to filename. Input is buffered in a buffer of
   *  size <code>bufferSize</code>.
   */
  def fromFile(file: JFile, bufferSize: Int = DefaultBufSize)(implicit codec: Codec = Codec.default): Source = {
    val inputStream = new FileInputStream(file)

    fromInputStream(
      inputStream,
      bufferSize,
      () => fromFile(file, bufferSize)(codec),
      () => inputStream.close()
    ) withDescription ("file:" + file.getAbsolutePath)
  }

  /** Reads data from <code>inputStream</code> with a buffered reader,
   *  using encoding in implicit parameter <code>codec</code>.
   * 
   *  @param  inputStream  the input stream from which to read
   *  @param  bufferSize   buffer size (defaults to Source.DefaultBufSize)
   *  @param  reset        a () => Source which resets the stream (if unset, reset() will throw an Exception)
   *  @param  codec        (implicit) a scala.io.Codec specifying behavior (defaults to Codec.default)
   *  @return              the buffered source
   */
  def fromInputStream(
    inputStream: InputStream,
    bufferSize: Int = DefaultBufSize,
    reset: () => Source = null,
    close: () => Unit = null
  )(implicit codec: Codec = Codec.default): Source =
  {
    // workaround for default arguments being unable to refer to other parameters
    val resetFn = if (reset == null) () => fromInputStream(inputStream, bufferSize, reset, close) else reset
    new BufferedSource(inputStream)(codec) .
      withReset (resetFn) .
      withClose (close)
  }
}

// Coming Soon?
//
// abstract class Source2[T] extends Iterable[T] { }
// 
// abstract class ByteSource() extends Source2[Byte] { }
// 
// abstract class CharSource(implicit codec: Codec = Codec.default) extends Source2[Char] { }

/** The class <code>Source</code> implements an iterable representation
 *  of source data.  Calling method <code>reset</code> returns an identical,
 *  resetted source, where possible.
 *
 *  @author  Burak Emir
 *  @version 1.0
 */
abstract class Source extends Iterator[Char]
{
  /** the actual iterator */
  protected val iter: Iterator[Char]

  // ------ public values

  /** description of this source, default empty */
  var descr: String = ""

  var nerrors = 0
  var nwarnings = 0

  /** convenience method, returns given line (not including newline)
   *  from Source.
   *
   *  @param line the line index, first line is 1
   *  @return     the character string of the specified line.
   *
   */
  def getLine(line: Int): String = getLines() drop (line - 1) next
  
  class LineIterator(separator: String) extends Iterator[String] {
    require(separator.length == 1 || separator.length == 2, "Line separator may be 1 or 2 characters only.")
    lazy val iter: BufferedIterator[Char] = Source.this.iter.buffered
    // For two character newline sequences like \r\n, we peek at
    // the iterator head after seeing \r, and drop the \n if present.
    val isNewline: Char => Boolean = {
      val firstCh = separator(0)
      if (separator.length == 1) (_ == firstCh)
      else (ch: Char) => (ch == firstCh) && iter.hasNext && {
        val res = iter.head == separator(1)
        if (res) { iter.next }  // drop the second character
        res
      }
    }
    private[this] val sb = new StringBuilder
    
    private def getc() =
      if (!iter.hasNext) false
      else {
        val ch = iter.next
        if (isNewline(ch)) false
        else {
          sb append ch
          true
        }
      }

    def hasNext = iter.hasNext
    def next = {
      sb.clear
      while (getc()) { }
      sb.toString
    }
  }

  /** returns an iterator who returns lines (NOT including newline character(s)).
   *  If no separator is given, the platform-specific value "line.separator" is used.
   *  a line ends in \r, \n, or \r\n.
   */
  def getLines(separator: String = compat.Platform.EOL): Iterator[String] =
    new LineIterator(separator)

  /** Returns <code>true</code> if this source has more characters.
   */
  def hasNext = iter.hasNext

  /** Returns next character.
   */
  def next: Char = positioner.next
  
  class Positioner {
    /** the last character returned by next. */
    var ch: Char = _

    /** position of last character returned by next */
    var pos = 0

    /** current line and column */
    var cline = 1
    var ccol = 1

    /** default col increment for tabs '\t', set to 4 initially */
    var tabinc = 4

    def next: Char = {
      ch = iter.next
      pos = Position.encode(cline, ccol)
      ch match {
        case '\n' =>
          ccol = 1
          cline += 1
        case '\t' =>
          ccol += tabinc
        case _ =>
          ccol += 1
      }
      ch
    }
  }
  object NoPositioner extends Positioner {
    override def next: Char = iter.next
  }
  def ch = positioner.ch
  def pos = positioner.pos

  /** Reports an error message to the output stream <code>out</code>.
   *
   *  @param pos the source position (line/column)
   *  @param msg the error message to report
   *  @param out PrintStream to use (optional: defaults to <code>Console.err</code>)
   */
  def reportError(
    pos: Int, 
    msg: String, 
    out: PrintStream = Console.err)
  {
    nerrors += 1
    report(pos, msg, out)
  }

  private def spaces(n: Int) = List.fill(n)(' ').mkString
  /**
   *  @param pos the source position (line/column)
   *  @param msg the error message to report
   *  @param out PrintStream to use
   */
  def report(pos: Int, msg: String, out: PrintStream) {
    val line = Position line pos
    val col = Position column pos
    
    out println "%s:%d:%d: %s%s%s^".format(descr, line, col, msg, getLine(line), spaces(col - 1))
  }

  /**
   *  @param pos the source position (line/column)
   *  @param msg the warning message to report
   *  @param out PrintStream to use (optional: defaults to <code>Console.out</code>)
   */
  def reportWarning(
    pos: Int, 
    msg: String, 
    out: PrintStream = Console.out)
  {
    nwarnings += 1
    report(pos, "warning! " + msg, out)
  }
  
  private[this] var resetFunction: () => Source = null
  private[this] var closeFunction: () => Unit = null
  private[this] var positioner: Positioner = new Positioner
  
  def withReset(f: () => Source): this.type = {
    resetFunction = f
    this
  }
  def withClose(f: () => Unit): this.type = {
    closeFunction = f
    this
  }
  def withDescription(text: String): this.type = {
    descr = text
    this
  }
  // we'd like to default to no positioning, but for now we break
  // less by defaulting to status quo.
  def withPositioning(on: Boolean): this.type = {
    positioner = if (on) new Positioner else NoPositioner
    this
  }

  /** The close() method closes the underlying resource. */
  def close: Unit     =
    if (closeFunction != null) closeFunction()
    
  /** The reset() method creates a fresh copy of this Source. */
  def reset(): Source = 
    if (resetFunction != null) resetFunction()
    else throw new UnsupportedOperationException("Source's reset() method was not set.")
}