/*
 * @(#)Store.cpp
 *
 * This file is part of webCDwriter - Network CD Writing.
 *
 * Copyright (C) 1999-2002 Jörg P. M. Haeger
 *
 * webCDwriter is free software. See CDWserver.cpp for details.
 */

#include <dirent.h>
#include <errno.h>
#include <stdlib.h>
#include <sys/types.h>
#ifdef FreeBSD
#include <sys/param.h>
#include <sys/mount.h>
#else
#include <sys/vfs.h>
#endif
#include "File.h"
#include "Config.h"
#include "Server.h"
#include "Session.h"
#include "Store.h"
#include "Queue.h"

Store::Store() {
	pthread_mutex_init(&mutex, NULL);

	spoolDir = new File(config.getSpoolDir());

	maxOpenSessions = config.getMaxOpenSessions();
	sessionsNum = 0;
	sessions = new (Session *)[maxOpenSessions];
	int i;
	for (i = 0; i < maxOpenSessions; i++)
		sessions[i] = NULL;

	queue = new Queue();

	// get blocksize of filesystem
	struct statfs statfsBuf;
	statfs(config.getSpoolDir(), &statfsBuf);
	fsBlockSize = statfsBuf.f_bsize;

	int64 n = 1024 * 1024 * config.getReservedMBytes();
	n /= fsBlockSize;
	reservedBlocks = n;

	maxSizeInKBytes = 1024 * config.getMaxMBytesInSpoolDir();
	sizeInKBytes = 0;

	imageInSpoolDirFlags = new boolean[maxNumOfWriters];
	if (!config.getImageOnTheFly())
		for (int i = 0; i < config.getNumOfWriters(); i++) {
			FILE *imageFile = fopen(config.getImageFile(i), "w");
			if (imageFile == NULL)
				throw new Exception("can't create imageFile");
			fclose(imageFile);

			dev_t imageFileDevice, spoolDirDevice;
			struct stat statBuf;
			if (stat(config.getImageFile(i), &statBuf) != 0)
				throw new Exception("can't stat imageFile");
			imageFileDevice = statBuf.st_dev;
			if (stat(config.getSpoolDir(), &statBuf) != 0)
				throw new Exception("can't stat spoolDir");
			spoolDirDevice = statBuf.st_dev;
			log.put("imageFileDevice = %d, spoolDirDevice = %d",
				(int)imageFileDevice, (int)spoolDirDevice);

			imageInSpoolDirFlags[i]
				= imageFileDevice == spoolDirDevice;
			if (imageInSpoolDirFlags[i])
				maxSizeInKBytes -=
					1024 * config.getMaxMBytesPerSession();
		}

	DIR *dir = opendir(config.getSpoolDir());
	if (dir == NULL)
		throw new Exception("spoolDir does not exist");

	sessionsTotal = 0;
	struct dirent *dirEntry;
	while ((dirEntry = readdir(dir)) != NULL) {
		if (strcmp(dirEntry->d_name, ".") == 0
			|| strcmp(dirEntry->d_name, "..") == 0)
			continue;

		File dir(*spoolDir, dirEntry->d_name);
		if (!Session::isSessionDir(dir))
			continue;

		sessionsTotal++;
		sizeInKBytes += Session::getSessionSize(dir);
	}

	closedir(dir);

	log.put("sessionsTotal=%d, sizeInKBytes = %d, reservedBlocks = %d",
		sessionsTotal, sizeInKBytes, reservedBlocks);
}

Store::~Store() {
}

Session *Store::createSession(Server *server,
	const char *user, const char *password, const char *name,
	int overwrite) {
	char ID[64 + 1];
	makeSessionID(user, password, name, ID, sizeof ID);

	queue->add(server->getNo());
	try {
		return createSession2(server, ID, overwrite);
	}
	catch (Exception *e) {
		queue->remove(server->getNo());
		throw e;
	}
}

Session *Store::createSession2(Server *server,
	const char *ID, int overwrite) {
	int cancel = server->waitForService(queue, maxOpenSessions - 1);
	if (cancel)
		throw new Exception("Waiting for session cancelled.");

	pthread_mutex_lock(&mutex);
	try {
		Session *session = createSession3(server, ID, overwrite);
		pthread_mutex_unlock(&mutex);
		return session;
	}
	catch (Exception *e) {
		pthread_mutex_unlock(&mutex);
		throw e;
	}
}

Session *Store::createSession3(Server *server,
	const char *ID, int overwrite) {
	if (isSessionIDInUse(ID))
		throw new Exception("The session ID is already active.");

	File dir(config.getSpoolDir(), ID);
	int sessionExists = Session::isSessionDir(dir);
	if (sessionExists) {
		if (overwrite) {
			removeSession2(ID);
			sessionExists = 0;
		}
		else {
			time_t t;
			time(&t);
			if (t > 0)
				dir.setLastModified(t);
		}
	}
	else
		if (dir.exists())
			throw new Exception("Cannot get session directory.");

	Session *session = new Session(server, ID, dir.getPath().getBytes());
	if (!sessionExists)
		sessionsTotal++;

	int i;
	for (i = 0; i < maxOpenSessions && sessions[i] != NULL; i++);
	if (i < maxOpenSessions) {
		sessions[i] = session;
		sessionsNum++;
	}
	else
		throw new Exception("internal error in Store::createSession3");

	return session;
}

void Store::freeSpace() {
	while (1) {
		struct statfs statfsBuf;
		statfs(config.getSpoolDir(), &statfsBuf);

		long free = statfsBuf.f_bavail;
		long reserved = reservedBlocks
			+ getReservedBlocksForImageFile();

		if (free > reserved
			&& sizeInKBytes <= maxSizeInKBytes)
			break;

		log.putv("freeSpace: free = %d, reserved = %d",
			free, reserved);
		if (sizeInKBytes > maxSizeInKBytes)
			log.putv("sizeInKBytes = %d, maxSizeInKBytes = %d",
				sizeInKBytes, maxSizeInKBytes);

		removeSessionLRU();
	}
}

void Store::getInfo(
	const char *user, const char *password, const char *name,
	int &files, int &tracks, int &size_KB) {
	char ID[64 + 1];
	makeSessionID(user, password, name, ID, sizeof ID);
	Session::getInfo(ID, files, tracks, size_KB);
}

int Store::getReservedBlocksForImageFile() {
	if (config.getImageOnTheFly())
		return 0;

	long n = 0;
	for (int i = 0; i < config.getNumOfWriters(); i++) {
		if (!imageInSpoolDirFlags[i])
			continue;

		n += sizeToBlocks(
			1024 * 1024 * config.getMaxMBytesPerSession());

		struct stat statBuf;
		if (stat(config.getImageFile(i), &statBuf) == 0)
			n -= sizeToBlocks(statBuf.st_size);
	}

	return n;
}

File *Store::getSessionDir(const char *sessionID) {
	return new File(config.getSpoolDir(), sessionID);
}

int Store::isSessionIDInUse(const char *sessionID) {
	int i;
	for (i = 0; i < maxOpenSessions; i++)
		if (sessions[i] != NULL)
			if (strcmp(sessionID,
				sessions[i]->getID()) == 0)
				break;

	return i < maxOpenSessions;
}

int Store::list(const char *userID, const char *userPassword,
	const char **sessionIDs, int max) {
	char ID[64 + 1];
	makeSessionID(userID, userPassword, "", ID, sizeof ID);

	DIR *dir = opendir(spoolDir->getPath().getBytes());
	if (dir == NULL)
		throw new Exception("can't open spoolDir");

	int count = 0;
	struct dirent *dirEntry;
	while (count < max
		&& (dirEntry = readdir(dir)) != NULL) {
		char *name = dirEntry->d_name;
		if (strcmp(name, ".") == 0
			|| strcmp(name, "..") == 0)
			continue;

		if (strstr(name, ID) != name)
			continue;

		sessionIDs[count++] = strDup(&name[strlen(ID)]);
	}

	closedir(dir);
	return count;
}

char *Store::makeSessionID(const char *user, const char *password,
	const char *name,
	char *ID, int IDSize) {
	int i = 0;
/*	for (; i < IDSize - 2 && *password != 0; i++) {
		char ch = *password++;
		if (ch == '/' || ch == '-')
			ID[i]= '_';
		else
			ID[i] = ch;
	}
	ID[i++] = '-';
*/
	for (; i < IDSize - 2 && *user != 0; i++) {
		char ch = *user++;
		if (ch == '/' || ch == '-')
			ID[i]= '_';
		else
			ID[i] = ch;
	}
	ID[i++] = '-';

	for (; i < IDSize - 1 && *name != 0; i++) {
		char ch = *name++;
		if (ch == '/' || ch == '-')
			ID[i]= '_';
		else
			ID[i] = ch;
	}
	ID[i] = 0;

	return ID;
}

void Store::removeFile(const char *pathName) {
	waitUntilAllFIFOsFilled();

	struct stat statBuf;
	if (lstat(pathName, &statBuf) != 0)
		return;

	if (!S_ISDIR(statBuf.st_mode)) {
		unlink(pathName);
		return;
	}

	DIR *dir = opendir(pathName);
	if (dir == NULL)
		return;

	struct dirent *dirEntry;
	while ((dirEntry = readdir(dir)) != NULL) {
		if (strcmp(dirEntry->d_name, ".") == 0
			|| strcmp(dirEntry->d_name, "..") == 0)
			continue;

		char tmpPathName[strlen(pathName) + 1
			+ strlen(dirEntry->d_name) + 1];
		sprintf(tmpPathName, "%s/%s",
			pathName, dirEntry->d_name);
		removeFile(tmpPathName);
	}

	closedir(dir);
	rmdir(pathName);
}

void Store::removeSession(const char *ID) {
	try {
		pthread_mutex_lock(&mutex);
		if (isSessionIDInUse(ID))
			throw new Exception("Cannot remove an active session.");
		removeSession2(ID);
		pthread_mutex_unlock(&mutex);
	}
	catch (Exception *e) {
		pthread_mutex_unlock(&mutex);
		throw e;
	}
}

void Store::removeSession2(const char *ID) {
	File dir(config.getSpoolDir(), ID);
	if (Session::isSessionDir(dir)) {
		sessionsTotal--;
		sizeInKBytes -= Session::getSessionSize(dir);
		if (sizeInKBytes < 0)
			sizeInKBytes = 0;
	}

	log.put(3, S.e + "remove(" + dir.getPath() + ")");
	removeFile(dir.getPath().getBytes());
	log.put(3, S.e + "remove(" + dir.getPath() + ")");
}

void Store::removeSessionLRU() {
	DIR *dir = opendir(spoolDir->getPath().getBytes());
	if (dir == NULL)
		throw new Exception("can't open spoolDir");

	const char *ID_LRU = NULL;
	int t;
	struct dirent *dirEntry;
	while ((dirEntry = readdir(dir)) != NULL) {
		if (strcmp(dirEntry->d_name, ".") == 0
			|| strcmp(dirEntry->d_name, "..") == 0)
			continue;

		if (isSessionIDInUse(dirEntry->d_name))
			continue;

		File sessionDir(spoolDir, dirEntry->d_name);
		if (!Session::isSessionDir(sessionDir))
			continue;

		struct stat statBuf;
		if (stat(sessionDir.getPath().getBytes(), &statBuf) != 0)
			continue;

		if (ID_LRU == NULL || statBuf.st_mtime < t) {
			if (ID_LRU != NULL)
				delete[] ID_LRU;
			ID_LRU = strDup(dirEntry->d_name);
			t = statBuf.st_mtime;
		}
	}

	closedir(dir);

	if (ID_LRU != NULL) {
		removeSession2(ID_LRU);
		delete[] ID_LRU;
	}
	else
		throw new Exception("out of disk space");
}

int32 Store::sizeToBlocks(int64 size) {
	if (size > 0)
		return (size - 1) / fsBlockSize + 1;
	else
		return 0;
}

void Store::unregister(Session *session) {
	int i;
	for (i = 0; i < maxOpenSessions && sessions[i] != session; i++);
	if (i < maxOpenSessions) {
		sessions[i] = NULL;
		sessionsNum--;
	}
	else
		log.put("internal error in Store::unregister");

	queue->remove(session->getServer()->getNo());
}

void Store::waitUntilAllFIFOsFilled() {
	int quit = 0;
	while (!quit) {
		quit = 1;
		for (int i = 0; i < config.getNumOfWriters(); i++)
			if (Server::getWriterStatus(i) == writing
				&& Server::getWriterFifoPercent(i) < 100) {
				sleep(1);
				quit = 0;
				break;
			}
	}
}

void Store::write(const char *buf, int size, int fd) {
	pthread_mutex_lock(&mutex);
	try {
		waitUntilAllFIFOsFilled();

		if (size > 0)
			sizeInKBytes += (size - 1) / 1024 + 1;
		freeSpace();
		while (size > 0) {
			int n = ::write(fd, buf, size);
			if (n < 0)
			if (errno == EBADF)
				throw new Exception("bad file descriptor");
			else
			if (errno == EFBIG)
				throw new Exception("file to big");
			else
			if (errno == EIO)
				throw new Exception("hardware error");
			else
			if (errno == ENOSPC) {
				freeSpace();
				continue;
			}
			else
				throw new Exception("unknown error");
			buf += n;
			size -= n;
		}
		pthread_mutex_unlock(&mutex);
	}
	catch (Exception *e) {
		pthread_mutex_unlock(&mutex);
		throw e;
	}
}

Store *store;
