/* vim: set ts=8 sts=4 sw=4 tw=80 noet: */
/*======================================================================
Copyright (C) 2004,2005,2009,2013 Walter Doekes <walter+tthsum@wjd.nu>
This file is part of tthsum.

tthsum is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

tthsum 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 tthsum.  If not, see <http://www.gnu.org/licenses/>.
======================================================================*/
#include "read.h"

#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#ifdef _WIN32
#   define STDIN_FILENO fileno(stdin) /* fileno is in <stdio.h> */
#   define WINDOWS_LEAN_AND_MEAN
#   include <windows.h>
#else /* !_WIN32 */
#   include <unistd.h>
#   include <sys/types.h>
#   include <sys/mman.h>
#   define O_BINARY 0
#endif /* !_WIN32 */

#ifdef USE_TEXTS
#   include "texts.h"
#endif

#define DEFAULT_BLOCK_SIZE 8192 /* must be a multiple of 1024 */


enum rofile_type { MEM, MMAP, SYS };

struct rofile_mem {
    char* buf;
    const char* cur;
    unsigned left;
};

struct rofile_mmap {
#ifdef _WIN32
    HANDLE fd;
    HANDLE map;
    LPVOID view;
#else /* !_WIN32 */
    int fd;
    void* map;
    unsigned last;
#endif /* !_WIN32 */
    uint64_t off;
    uint64_t left;
};

struct rofile_sys {
    int fd;
    int close_fd; /* close fd afterwards */
#ifdef _WIN32
    int fdmode; /* original fd mode (bin/txt) */
#endif /* _WIN32 */
    int done;
    char* buf;
};

struct rofile {
    enum rofile_type rof_type;
    unsigned rof_blocksize;
    uint64_t rof_filesize;
    union {
	struct rofile_mem rof_mem;
	struct rofile_mmap rof_mmap;
	struct rofile_sys rof_sys;
    } u; /* ansi does not allow anonymous unions */
};


_INLINE static void close_or_warn(int fd) {
    /* Send warning to stderr but ignore failure...
     * After all, we did only read bytes, not write any. */
    if (close(fd) != 0)
	perror("rofclose: closing fd failed");
}

struct rofile* rofopen_mem(const char* data, unsigned length) {
    struct rofile* rf = NULL;
    rf = (struct rofile*)malloc(sizeof(struct rofile));
    if (rf != NULL) {
	rf->rof_type = MEM;
	rf->rof_blocksize = DEFAULT_BLOCK_SIZE;
	rf->rof_filesize = length;
	rf->u.rof_mem.buf = (char*)malloc(length);
	if (rf->u.rof_mem.buf != NULL) {
	    memcpy(rf->u.rof_mem.buf, data, length);
	    rf->u.rof_mem.cur = rf->u.rof_mem.buf;
	    rf->u.rof_mem.left = length;
	    return rf;
	} else {
#ifdef USE_TEXTS
	    set_error("malloc", ERROR_FROM_OS);
#endif /* USE_TEXTS */
	}
	free(rf);
    } else {
#ifdef USE_TEXTS
	set_error("malloc", ERROR_FROM_OS);
#endif /* USE_TEXTS */
    }
    return NULL;
}

struct rofile* rofopen_mmap(const char* filename) {
    struct rofile* rf = NULL;
#ifdef _WIN32
    SYSTEM_INFO si;
    LARGE_INTEGER li;
    rf = (struct rofile*)malloc(sizeof(struct rofile));
    if (rf == NULL) {
#ifdef USE_TEXTS
	set_error("malloc", ERROR_FROM_OS);
#endif /* USE_TEXTS */
	return NULL;
    }
    rf->rof_type = MMAP;
    GetSystemInfo(&si);
    /* 0x1000000 = 2^24 = 1.6MiB */
    rf->rof_blocksize = 0x1000000 - (0x1000000 % si.dwAllocationGranularity);
    rf->u.rof_mmap.fd = CreateFile(filename, GENERIC_READ, FILE_SHARE_READ,
	    NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
    if (rf->u.rof_mmap.fd != INVALID_HANDLE_VALUE) {
	if (GetFileSizeEx(rf->u.rof_mmap.fd, &li) == TRUE) {
	    rf->rof_filesize = (uint64_t)li.QuadPart;
	    rf->u.rof_mmap.left = rf->rof_filesize;
	    rf->u.rof_mmap.off = 0;
	    rf->u.rof_mmap.view = NULL;
	    /* windows fails to map empty files :-/ */
	    if (rf->rof_filesize != 0) {
		rf->u.rof_mmap.map = CreateFileMapping(rf->u.rof_mmap.fd, NULL, 
			PAGE_READONLY, 0, 0, NULL);
		if (rf->u.rof_mmap.map != NULL)
		    return rf;
#ifdef USE_TEXTS
		else
		    set_error("CreateFileMapping", ERROR_FROM_OS);
#endif /* USE_TEXTS */
	    } else {
		rf->u.rof_mmap.map = NULL;
		return rf;
	    }
#ifdef USE_TEXTS
	} else {
	    set_error("GetFileSizeEx", ERROR_FROM_OS);
#endif /* USE_TEXTS */
	}    
	CloseHandle(rf->u.rof_mmap.fd);
#ifdef USE_TEXTS
    } else {
	set_error("CreateFile", ERROR_FROM_OS);
#endif /* USE_TEXTS */
    }
#else /* !_WIN32 */
    struct stat st;
    rf = (struct rofile*)malloc(sizeof(struct rofile));
    if (rf != NULL) {
	rf->rof_type = MMAP;
	rf->u.rof_mmap.fd = open(filename, O_RDONLY | O_BINARY);
	if (rf->u.rof_mmap.fd != -1) {
	    if (fstat(rf->u.rof_mmap.fd, &st) != -1) {
		rf->rof_filesize = rf->u.rof_mmap.left = (uint64_t)st.st_size;
		/* 0x1000000 = 2^24 = 1.6MiB */
		rf->rof_blocksize = 0x1000000;
#if defined(_BSD_SOURCE) || _XOPEN_SOURCE >= 500
		rf->rof_blocksize -= 0x1000000 % getpagesize();
#endif /* _BSD_SOURCE || _XOPEN_SOURCE >= 500 */
		rf->u.rof_mmap.off = 0;
		rf->u.rof_mmap.map = NULL;
		return rf;
#ifdef USE_TEXTS
	    } else {
		set_error("fstat", ERROR_FROM_OS);
#endif /* USE_TEXTS */
	    }
	    close_or_warn(rf->u.rof_mmap.fd);
#ifdef USE_TEXTS
	} else {
	    set_error("open", ERROR_FROM_OS);
#endif /* USE_TEXTS */
	}
    }
#endif /* !_WIN32 */
    free(rf);
    return NULL;
}

struct rofile* rofopen_sysfd(int fd) {
    struct rofile *rf = NULL;
#ifdef _WIN32
    int old_fdmode;
    if ((old_fdmode = setmode(fd, O_BINARY)) == -1) {
#ifdef USE_TEXTS
	set_error("fdmode", ERROR_FROM_OS);
#endif /* USE_TEXTS */
	return NULL;
    }
#endif /* _WIN32 */

    if ((rf = (struct rofile*)malloc(sizeof(struct rofile))) != NULL) {
	rf->rof_type = SYS;
	rf->rof_blocksize = DEFAULT_BLOCK_SIZE;
	rf->rof_filesize = (uint64_t)-1; /* the "unknown" size */
	rf->u.rof_sys.fd = fd;
	rf->u.rof_sys.close_fd = 0;
#ifdef _WIN32
	rf->u.rof_sys.fdmode = old_fdmode;
#endif /* _WIN32 */
	rf->u.rof_sys.done = 0;
	if ((rf->u.rof_sys.buf = (char*)malloc(rf->rof_blocksize)) != NULL) {
	    return rf;
	} else {
#ifdef USE_TEXTS
	    set_error("malloc", ERROR_FROM_OS);
#endif /* USE_TEXTS */
	}
	free(rf);
    } else {
#ifdef USE_TEXTS
	set_error("malloc", ERROR_FROM_OS);
#endif /* USE_TEXTS */
    }

#ifdef _WIN32
    if (old_fdmode != O_BINARY)
	setmode(fd, old_fdmode);
#endif /* _WIN32 */
    return NULL;
}

struct rofile* rofopen_sysfd_stdin() {
    return rofopen_sysfd(STDIN_FILENO);
}

struct rofile* rofopen_sysfile(const char* filename) {
    int fd;
    if ((fd = open(filename, O_RDONLY | O_BINARY)) >= 0) {
	struct rofile* rf;
	if ((rf = rofopen_sysfd(fd)) != NULL) {
	    struct stat st;
	    if (fstat(fd, &st) == 0)
		rf->rof_filesize = (uint64_t)st.st_size;
	    rf->u.rof_sys.close_fd = 1;
	    return rf;
	}
	close_or_warn(fd);
    } else {
#ifdef USE_TEXTS
	set_error("open", ERROR_FROM_OS);
#endif /* USE_TEXTS */
    }
    return NULL;
}

_INLINE static int rofread_mem(const char** next, unsigned* size,
	struct rofile* rf) {
    if (rf->u.rof_mem.left == 0)
	return 0;
    if (rf->u.rof_mem.left >= rf->rof_blocksize)
	*size = rf->rof_blocksize;
    else
	*size = rf->u.rof_mem.left;
    *next = rf->u.rof_mem.cur;
    rf->u.rof_mem.cur += *size;
    rf->u.rof_mem.left -= *size;
    return 1;
}

_INLINE static int rofread_mmap(const char** next, unsigned* size,
	struct rofile* rf) {
#ifdef _WIN32
    unsigned mapped_size;
#endif /* _WIN32 */
    if (rf->u.rof_mmap.left == 0)
	return 0;
#ifdef _WIN32
    if (rf->u.rof_mmap.view)
	UnmapViewOfFile(rf->u.rof_mmap.view);
    mapped_size = rf->u.rof_mmap.left < (uint64_t)rf->rof_blocksize
		? (unsigned)rf->u.rof_mmap.left : rf->rof_blocksize;
    rf->u.rof_mmap.view = MapViewOfFile(rf->u.rof_mmap.map, FILE_MAP_READ,
	    rf->u.rof_mmap.off >> 32, (DWORD)rf->u.rof_mmap.off, mapped_size);
    if (rf->u.rof_mmap.view == NULL) {
#ifdef USE_TEXTS
	set_error("MapViewOfFile", ERROR_FROM_OS);
#endif /* USE_TEXTS */
	return -1;
    }
    *next = rf->u.rof_mmap.view;
    rf->u.rof_mmap.off += mapped_size;
    rf->u.rof_mmap.left -= mapped_size;
    *size = mapped_size;
#else /* !_WIN32 */
    if (rf->u.rof_mmap.map)
	munmap(rf->u.rof_mmap.map, rf->u.rof_mmap.last);
    rf->u.rof_mmap.last = rf->u.rof_mmap.left < (uint64_t)rf->rof_blocksize
	    ? (unsigned)rf->u.rof_mmap.left : rf->rof_blocksize;
    rf->u.rof_mmap.map = mmap(0, rf->u.rof_mmap.last, PROT_READ, MAP_SHARED,
	    rf->u.rof_mmap.fd, (off_t)rf->u.rof_mmap.off);
    if (rf->u.rof_mmap.map == MAP_FAILED) {
#ifdef USE_TEXTS
	set_error("mmap", ERROR_FROM_OS);
#endif /* USE_TEXTS */
	rf->u.rof_mmap.map = NULL;
	return -1;
    }
#ifdef _BSD_SOURCE
    madvise(rf->u.rof_mmap.map, rf->u.rof_mmap.last,
	    MADV_SEQUENTIAL | MADV_WILLNEED);
#endif /* _BSD_SOURCE */
    *next = (const char*)rf->u.rof_mmap.map;
    rf->u.rof_mmap.off += rf->u.rof_mmap.last;
    rf->u.rof_mmap.left -= rf->u.rof_mmap.last;
    *size = rf->u.rof_mmap.last;
#endif /* !_WIN32 */
    return 1;
}

_INLINE static int rofread_sys(const char** next, unsigned* size,
	struct rofile* rf) {
    unsigned len = 0;
    if (rf->u.rof_sys.done)
	return 0;
    *next = rf->u.rof_sys.buf;
    do {
	int ret;
	if ((ret = (int)read(rf->u.rof_sys.fd, rf->u.rof_sys.buf + len,
		rf->rof_blocksize - len)) < 0) {
#ifdef _WIN32
	    /* MSDN says:
	     * _read returns the number of bytes read, which may be less than
	     * count if there are fewer than count bytes left in the file or if
	     * the file was opened in text mode [snip] (nothing about EINTR) */
#else /* !_WIN32 */
	    /* man 2 read says:
	     * It is not an error if this number is smaller than the number of
	     * bytes requested; this may happen for example because fewer bytes
	     * are actually available right now (maybe because we were close to
	     * end-of-file, or because we are reading from a pipe, or from a
	     * terminal), or because read() was interrupted by a signal. */
	    if (errno == EINTR)
		continue;
#endif /* !_WIN32 */
#ifdef USE_TEXTS
	    set_error("read", ERROR_FROM_OS);
#endif /* USE_TEXTS */
	    return -1;
	} else if (ret == 0) {
	    rf->u.rof_sys.done = 1;
	    if (len == 0)
		return 0;
	    break;
	} else {
	    len += ret;
	}
    } while (len < rf->rof_blocksize);
    *size = len;
    return 1;
}
 
_INLINE static void rofclose_mem(struct rofile* rf) {
    free(rf->u.rof_mem.buf);
    free(rf);
}
    
_INLINE static void rofclose_mmap(struct rofile* rf) {
#ifdef _WIN32
    if (rf->u.rof_mmap.view)
	UnmapViewOfFile(rf->u.rof_mmap.view);
    if (rf->u.rof_mmap.map)
	CloseHandle(rf->u.rof_mmap.map);
    CloseHandle(rf->u.rof_mmap.fd);
#else /* !_WIN32 */
    if (rf->u.rof_mmap.map)
	munmap(rf->u.rof_mmap.map, rf->u.rof_mmap.last);
    close_or_warn(rf->u.rof_mmap.fd);
#endif /* !_WIN32 */
    free(rf);
}

_INLINE static void rofclose_sys(struct rofile* rf) {
    free(rf->u.rof_sys.buf);
    if (rf->u.rof_sys.close_fd)
	close_or_warn(rf->u.rof_sys.fd);
#ifdef _WIN32
    else if (rf->u.rof_sys.fdmode != O_BINARY)
	/* Ignore return value.. we can't do anything about it anyway */
	setmode(rf->u.rof_sys.fd, rf->u.rof_sys.fdmode);
#endif /* _WIN32 */
    free(rf);
}

void rofinfo(unsigned* blocksize, uint64_t* filesize, struct rofile* stream) {
    *blocksize = stream->rof_blocksize;
    *filesize = stream->rof_filesize;
}
    
int rofread(const char** next, unsigned* size, struct rofile* stream) {
    /* I assume sysread will be the most used case, check it first */
    if (stream->rof_type == SYS)
	return rofread_sys(next, size, stream);
    else if (stream->rof_type == MMAP)
	return rofread_mmap(next, size, stream);
    else if (stream->rof_type == MEM)
	return rofread_mem(next, size, stream);
    return -1;
}

void rofclose(struct rofile* stream) {
    if (stream->rof_type == MEM)
	rofclose_mem(stream);
    else if (stream->rof_type == MMAP)
	rofclose_mmap(stream);
    else if (stream->rof_type == SYS)
	rofclose_sys(stream);
}

char* rof_readall(struct rofile* stream, unsigned* length) {
    int ret;
    const char* next;
    char* buf;
    size_t memsize = 4096, written = 0;
    unsigned left, blocksize, readsize;
    uint64_t filesize;

    if (stream == NULL)
	return NULL;

    rofinfo(&blocksize, &filesize, stream);
    if (filesize != (uint64_t)-1) {
	memsize = (size_t)filesize;
	/* Special files may return 0 but contain more data */
	if (memsize == 0)
	    memsize = 8;
    }

    left = (unsigned)memsize;
    if ((buf = (char*)malloc(memsize)) == NULL) {
#ifdef USE_TEXTS
	set_error("malloc", ERROR_FROM_OS);
#endif /* USE_TEXTS */
	return NULL;
    }
   
    while ((ret = rofread(&next, &readsize, stream)) > 0) {
	while (readsize > left) {
	    char* newbuf = NULL;
	    memsize <<= 1;
	    if ((ssize_t)memsize > 0) /* Catch overflow */
		newbuf = (char*)realloc(buf, memsize);
	    else
		errno = ENOMEM;
	    if (newbuf == NULL) {
#ifdef USE_TEXTS
		set_error("realloc", ERROR_FROM_OS);
#endif /* USE_TEXTS */
		free(buf);
		return NULL;
	    }
	    buf = newbuf;
	    left = memsize - written;
	}
	memcpy(buf + written, next, readsize);
	written += readsize;
	left -= readsize;
    }
    if (ret == -1) {
	free(buf);
	return NULL;
    }

    *length = written;
    return buf;
}
