//    This is part of the iostream library, providing input/output for C++.
//    Copyright (C) 1991, 1992 Per Bothner.
//
//    This library is free software; you can redistribute it and/or
//    modify it under the terms of the GNU Library General Public
//    License as published by the Free Software Foundation; either
//    version 2 of the License, or (at your option) any later version.
//
//    This library 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
//    Library General Public License for more details.
//
//    You should have received a copy of the GNU Library General Public
//    License along with this library; if not, write to the Free
//    Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

#include "ioprivat.h"
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>

// An fstream can be in at most one of put mode, get mode, or putback mode.
// Putback mode is a variant of get mode.

// In a filebuf, there is only one current position, instead of two
// separate get and put pointers.  In get mode, the current posistion
// is that of gptr(); in put mode that of pptr().

// The position in the buffer that corresponds to the position
// in external file system is file_ptr().
// This is normally egptr(), except in putback mode, when it is _save_egptr.
// If the field _fb._offset is >= 0, it gives the offset in
// the file as a whole of the start of the buffer (base()).

// PUT MODE:
// If a filebuf is in put mode, pbase() is non-NULL and equal to base().
// Also, epptr() == ebuf().
// Also, eback() == gptr() && gptr() == egptr().
// The un-flushed character are those between pbase() and pptr().
// GET MODE:
// If a filebuf is in get or putback mode, eback() != egptr().
// In get mode, the unread characters are between gptr() and egptr().
// The OS file position corresponds to that of egptr().
// PUTBACK MODE:
// Putback mode is used to remember "excess" characters that have
// been sputbackc'd in a separate putback buffer.
// In putback mode, the get buffer points to the special putback buffer.
// The unread characters are the characters between gptr() and egptr()
// in the putback buffer, as well as the area between save_gptr()
// and save_egptr(), which point into the original reserve buffer.
// (The pointers save_gptr() and save_egptr() are the values
// of gptr() and egptr() at the time putback mode was entered.)
// The OS position corresponds to that of save_egptr().
//
// LINE BUFFERED OUTPUT:
// During line buffered output, pbase()==base() && epptr()==base().
// However, ptr() may be anywhere between base() and ebuf().
// This forces a call to filebuf::overflow(int C) on every put.
// If there is more space in the buffer, and C is not a '\n',
// then C is inserted, and pptr() incremented.
//
// UNBUFFERED STREAMS:
// If a filebuf is unbuffered(), the _shortbuf[1] is used as the buffer.

void filebuf::init()
{
    _fb._fake = 0;
    _fb._offset = 0;
    _fb._fake = 0;
    xsetflags(_S_IS_FILEBUF);
    _fb._save_gptr = NULL;

    _link_in();
    _fb._fileno = -1;
}

void streambuf::_un_link()
{
    if (_flags & _S_LINKED) {
	streambuf **f;
	for (f = &_list_all; *f != NULL; f = &(*f)->xchain()) {
	    if (*f == this) {
		*f = xchain();
		break;
	    }
	}
	_flags &= _S_LINKED;
    }
}

void streambuf::_link_in()
{
    if (_flags & _S_LINKED) // Already linked.
	abort();
    _flags |= _S_LINKED;
    xchain() = _list_all;
    _list_all = this;
}


filebuf::filebuf()
{
    init();
}

filebuf::filebuf(int fd)
{
    init();
    attach(fd);
}

filebuf::filebuf(int fd, char* p, size_t len)
{
    init();
    attach(fd);
    setbuf(p, len);
}

filebuf::~filebuf()
{
    if (!(xflags() & _S_DELETE_DONT_CLOSE))
	close();

    _un_link();
}

filebuf* filebuf::open(const char *filename, int mode, int prot)
{
    if (is_open())
	return NULL;
    int posix_mode;
    int read_write;
    if ((mode & (ios::in|ios::out)) == (ios::in|ios::out))
	posix_mode = O_RDWR, read_write = _S_CAN_READ+_S_CAN_WRITE;
    else if (mode & (ios::out|ios::app))
	posix_mode = O_WRONLY, read_write = _S_CAN_WRITE;
    else if (mode & (int)ios::in)
	posix_mode = O_RDONLY, read_write = _S_CAN_READ;
    else
	posix_mode = 0, read_write = 0;
    if ((mode & (int)ios::trunc) || mode == (int)ios::out)
	posix_mode |= O_TRUNC;
    if (mode & ios::app)
	posix_mode |= O_APPEND;
    if (!(mode & (int)ios::nocreate) && mode != ios::in)
	posix_mode |= O_CREAT;
    if (mode & (int)ios::noreplace)
	posix_mode |= O_EXCL;
    int fd = ::open(filename, posix_mode, prot);
    if (fd < 0)
	return NULL;
    _fb._fileno = fd;
    xsetflags(read_write);
    if (mode & ios::ate) {
	if (seekoff(0, ios::end) == EOF)
	    return NULL;
    }
    return this;
}

filebuf* filebuf::open(const char *filename, const char *mode)
{
    if (is_open())
	return NULL;
    int oflags = 0, omode;
    int read_write;
    int oprot = 0666;
    switch (*mode++) {
      case 'r':
	omode = O_RDONLY;
	read_write = _S_CAN_READ;
	break;
      case 'w':
	omode = O_WRONLY;
	oflags = O_CREAT|O_TRUNC;
	read_write = _S_CAN_WRITE;
	break;
      case 'a':
	omode = O_WRONLY;
	oflags = O_CREAT|O_APPEND;
	read_write = _S_CAN_WRITE;
	break;
      default:
	errno = EINVAL;
	return NULL;
    }
    if (mode[0] == '+' || (mode[0] == 'b' && mode[1] == '+')) {
	omode = O_RDWR;
	read_write = _S_CAN_READ+_S_CAN_WRITE;
    }
    int fdesc = ::open(filename, omode|oflags, oprot);
    if (fdesc < 0)
	return NULL;
    _fb._fileno = fdesc;
    xsetflags(read_write);
    return this;
}

filebuf* filebuf::attach(int fd)
{
    if (is_open())
	return NULL;
    _fb._fileno = fd;
    xsetflags(_S_CAN_READ|_S_CAN_WRITE|_S_DELETE_DONT_CLOSE);
    return this;
}

int filebuf::overflow(int c)
{
    if (pptr() == pbase() && c == EOF)
	return 0;
    if ((xflags() & _S_CAN_WRITE) == 0) // SET ERROR
	return EOF;
    if (is_reading()) {
	if (pptr() != gptr() && pptr() > pbase())
	    if (do_flush())
		return EOF;
	setp(gptr(), ebuf());
	setg(egptr(), egptr(), egptr());
    }
    if (allocate() > 0) {
	if (xflags() & _S_LINE_BUF) setp(base(), base());
	else setp(base(), ebuf());
	setg(pbase(), pbase(), pbase());
    }
    int flush_only;
    if (c == EOF) flush_only = 1, c = 0;
    else flush_only = 0;
    if (epptr() == pbase()) { // Line buffering
	if (pptr() < ebuf() && !flush_only) {
	    xput_char(c);
	    if (c != '\n')
		return (unsigned char)c;
	    else
		flush_only = 1;
	}
    }
    size_t to_do = out_waiting();
    if (to_do > 0) {
	char *ptr = pbase();
	for (;;) {
	    size_t count = sys_write(ptr, to_do);
	    if (count == EOF)
		return EOF;
	    _fb._offset += count;
	    to_do -= count;
	    if (to_do == 0)
		break;
	    ptr += count;
	}
	if (xflags() & _S_LINE_BUF) setp(pbase(), pbase());
	else setp(pbase(), epptr());
	setg(egptr(), egptr(), egptr());
    }
    if (flush_only)
	return c;
    else
	return sputc(c);
}

int filebuf::underflow()
{
#if 0
    /* SysV does not make this test; take it out for compatibility */
    if (fp->_flags & __SEOF)
	return (EOF);
#endif

    if ((xflags() & _S_CAN_READ) == 0) // SET ERROR
	return EOF;
  retry:
    if (gptr() < egptr())
	return *(unsigned char*)gptr();
    if (_fb._save_gptr) { // Free old putback buffer.
	if (eback() != _fb._shortbuf)
	    FREE_BUF(eback());
	_fb._save_gptr = NULL;
	setg(base(), _fb._save_gptr, _fb._save_egptr); // Restore get area.
	goto retry;
    }

    allocate();
#if 0
    /* if not already reading, have to be reading and writing */
    if ((fp->_flags & __SRD) == 0) {
	if ((fp->_flags & __SRW) == 0)
	    return (EOF);
	/* switch to reading */
	if (fp->_flags & __SWR) {
	    if (fflush(fp))
		return (EOF);
	    fp->_flags &= ~__SWR;
	    fp->_w = 0;
	    fp->_lbfsize = 0;
	}
	fp->_flags |= __SRD;
    } else {
	// We were reading.  If there is an ungetc buffer,
	// we must have been reading from that.  Drop it,
	// restoring the previous buffer (if any).  If there
	// is anything in that buffer, return.
	if (HASUB(fp)) {
	    FREEUB(fp);
	    if ((fp->_r = fp->_ur) != 0) {
		fp->_p = fp->_up;
		return (0);
	    }
	}
    }
#endif
    if ((xflags() & _S_LINE_BUF) || unbuffered()) {
	// Flush all line buffered files before reading.
	streambuf::flush_all_linebuffered();
    }
    if (pptr() > pbase())
	if (do_flush()) return EOF;
    long count = sys_read(base(), ebuf() - base());
    if (count <= 0) {
	if (count == 0)
	    xsetflags(_S_EOF_SEEN);
	else
	    xsetflags(_S_ERR_SEEN), count = 0;
	return EOF;
    }
    _fb._offset += count;
    setg(base(), base(), base() + count);
    return *(unsigned char*)gptr();
}

int filebuf::pbackfail(int c)
{
    if (pbase() != NULL) { // is writing()
	overflow(EOF);
	// FIXME: check for writing!
    }
    if (_fb._save_gptr == NULL) { // No putback buffer.
	_fb._save_gptr = gptr(); _fb._save_egptr = egptr();
	setg(_fb._shortbuf, _fb._shortbuf+1, _fb._shortbuf+1);
    }
    else { // Increase size of existing putback buffer.
	size_t new_size;
	size_t old_size = egptr() - eback();
	new_size = eback() == _fb._shortbuf ? 128 : 2 * old_size;
	char* new_buf = ALLOC_BUF(new_size);
	memcpy(new_buf+(new_size-old_size), eback(), old_size);
	if (eback() != _fb._shortbuf)
	    FREE_BUF(eback());
	setg(new_buf, new_buf+(new_size-old_size), new_buf+new_size);
    }
    gbump(-1);
    *gptr() = c;
    return (unsigned char)c;
}

int filebuf::do_flush()
{
    if (egptr() != pbase()) {
	long new_pos = sys_seek(pbase()-egptr(), ios::cur);
	if (new_pos == -1)
	    return EOF;
    }
    long to_do = pptr()-pbase();
    char* ptr = pbase();
    while (to_do > 0) {
	size_t count = sys_write(ptr, to_do);
	if (count == EOF)
	    return EOF;
	if (_fb._offset >= 0)
	    _fb._offset += count;
	to_do -= count;
	ptr += count;
    }
    setg(base(), pptr(), pptr());
    setp(base(), base());
    return 0;
}

int filebuf::sync()
{
//    char* ptr = cur_ptr();
    if (pptr() > pbase())
	do_flush();
    if (gptr() != egptr()) {
	if (sys_seek(gptr() - egptr(), ios::cur) == EOF)
	    return EOF;
    }
    // FIXME: Handle putback mode!
//    setg(base(), ptr, ptr);
    return 0;
}

streampos filebuf::seekoff(streamoff offset, _seek_dir dir, int mode)
{
    fpos_t result, new_offset, delta;
    long count;

    // Flush unwritten characters.
    // (This may do an unneeded write if we seek within the buffer.
    // But to be able to switch to reading, we would need to set
    // egptr to ptr.  That can't be done in the current design,
    // which assumes file_ptr() is egptr.  Anyway, since we probably
    // end up flushing when we close(), it doesn't make much difference.)
    if (pptr() > pbase())
	do_flush();

    if (unbuffered() || base() == NULL)
	goto dumb;
    // FIXME: What if buffer already allocated even though unbuffered()
    switch (dir) {
      case ios::cur:
	if (_fb._offset < 0) {
	    _fb._offset = sys_seek(0, ios::cur);
	    if (_fb._offset < 0)
		return EOF;
	}
	// Make offset absolute, assuming current pointer is file_ptr().
	offset += _fb._offset;
	// Now adjust for unread/unflushed characters:
	// FIXME - this stuff needs more thought.
#if 0
	offset += cur_ptr() - file_ptr();
	// FIXME - might be confused if there is a putback buffer.
#else
	offset -= egptr() - gptr(); // Subtract unread characters, if any.
	if (_fb._save_gptr) // Putback mode
	    offset -= _fb._save_egptr - _fb._save_gptr;
	// Normally, egptr()==pbase(), but if we earlier switched from
	// get to put mode, the following will (correctly) decrease offset.
	// However, it won't work if there is a putback buffer - FIXME!
	offset += pptr() - egptr(); // Add unflushed characters, in put mode.
#endif
	dir = ios::beg;
	break;
      case ios::beg:
	break;
      case ios::end:
	struct stat st;
	if (sys_stat(&st) == 0 && (st.st_mode & S_IFMT) == S_IFREG) {
	    offset += st.st_size;
	    dir = ios::beg;
	}
	else
	    goto dumb;
    }
    // At this point, dir==ios::beg.
    // FIXME: Handle case of there being a putback buffer!
    // If destination is within current buffer, optimize:
    if (_fb._offset >= 0) {
	fpos_t rel_offset = offset - _fb._offset
	    + (file_ptr()-base()); // Offset relative to base().
	if (rel_offset >= 0 && rel_offset <= egptr() - base()) {
		setg(base(), base() + rel_offset, egptr());
		setp(base(), base());
		return offset;
	    }
    }

    // Try to seek to a block boundary, to improve kernal page management.
    new_offset = offset & ~(ebuf() - base() - 1);
    delta = offset - new_offset;
    if (delta > ebuf() - base()) {
	new_offset = offset;
	delta = 0;
    }
    result = sys_seek(new_offset, ios::beg);
    if (result < 0)
	return EOF;
    _fb._offset = result;
    count = sys_read(base(), ebuf()-base());
    if (count < 0)
	return EOF;
    setg(base(), base(), base()+count);
    xflags(xflags() & ~ _S_EOF_SEEN);
    return result;
  dumb:
    if (_fb._save_gptr != NULL) { // Get rid of putback buffer.
	if (eback() != _fb._shortbuf)
	    FREE_BUF(eback());
	_fb._save_gptr = NULL;
    }
    result = sys_seek(offset, dir);
    if (result != EOF) {
	_flags &= ~_S_EOF_SEEN;
    }
    char* start = unbuffered() ? _fb._shortbuf : base();
    setg(start, start, start);
    setp(start, start);
    return result;
}

filebuf* filebuf::close()
{
    if (!is_open())
	return NULL;

    // Flush.
    overflow(EOF);

    /* Free the buffer's storage.  */
    // Or should that be done only on destruction???
    if (_base != NULL && !(_flags & _S_USER_BUF)) {
	FREE_BUF(_base);
	_base = NULL;
    }

    int status = sys_close();

    _un_link();
    _flags = _IO_MAGIC | _S_IS_FILEBUF;
    _fb._fileno = EOF;

    return status < 0 ? NULL : this;
}

long filebuf::sys_read(char* buf, size_t size)
{
    return ::read(_fb._fileno, buf, size);
}

long filebuf::sys_seek(long offset, _seek_dir dir)
{
    return ::lseek(fd(), offset, (int)dir);
}

long filebuf::sys_write(const void *buf, long n)
{
    return ::write(fd(), buf, n);
}

int filebuf::sys_stat(void* st)
{
    return ::_fstat(fd(), (struct stat*)st);
}

int filebuf::sys_close()
{
    return ::close(fd());
}

size_t filebuf::sputn(const char *s, size_t n)
{
    // FIXME: OPTIMIZE THIS (specifically, when unbuffered()).
    return streambuf::sputn(s, n);
}

size_t filebuf::sgetn(char *s, size_t n)
{
    // FIXME: OPTIMIZE THIS (specifically, when unbuffered()).
    return streambuf::sgetn(s, n);
}
