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

#include <dirent.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include "Config.h"
#include "Support.h"
#include "Log.h"
#include "MP3ToWAV.h"
#include "OggToWAV.h"
#include "Queue.h"
#include "Server.h"
#include "Session.h"
#include "Store.h"
#include "Wave.h"
#include "WAVToWAV.h"

const char
	CDWserverInfo[] = "CDWserver.info";

Session::Session(Server *server, const char *ID, const char *dir):
		currentFile("") {
	this->server = server;
	this->ID = strDup(ID);
	this->dir = new File(dir);
	filesNum = tracksNum = sizeInKBytes = 0;
	fileOutputStream = NULL;
	currentTrack = NULL;
	currentTrackNo = 0;
	linksNum = 0;
	audioToWAV = NULL;

	if (this->dir->exists())
		readInfo();
	else
		create();
}

Session::~Session() {
	if (audioToWAV != NULL)
		closeAudioTrack();

	if (fileOutputStream != NULL)
		putFileEnd();

	writeInfo();

	store->unregister(this);

	if (config.getRemoveSessions())
		store->removeSession(ID);

	if (currentTrack != NULL)
		delete currentTrack;

	delete dir;
	delete[] ID;
}

void Session::checkContainsNoLink(const char *fileName) {
	if (linksNum == 0)
		return;

	if (currentTrack == NULL
			|| !currentTrack->isDirectory())
		return;

	File file = new File(*currentTrack);
	int i = 0;
	while (1) {
		while (fileName[i] == '/')
			i++;

		if (fileName[i] == 0)
			break;

		char str[strlen(fileName) + 1];
		int j = 0;
		while (fileName[i] != 0 && fileName[i] != '/')
			str[j++] = fileName[i++];
		str[j] = 0;

		file = new File(file, str);
		if (file.isLink())
			throw new Exception("file name contains link");
	}
}

void Session::closeAudioTrack() {
	log.debug("audioToWAV", "Session::closeAudioTrack()");
	if (audioToWAV == NULL) {
		log.put(1, "Session::closeAudioTrack(): audioToWAV == NULL");
		return;
	}

	audioToWAV->end();
	FILE *outStream = fopen(currentTrack->getPath().getBytes(), "r+");
	if (outStream != NULL) {
		fseek(outStream, 0, SEEK_END);
		try {
			while (1) {
				const char *buf;
				int n = audioToWAV->get(&buf);
				if (n == 0)
					break;
				copy(buf, outStream, n);
			}
		} catch (Exception *e) {
			delete e;
		}
		audioToWAV->updateHeader(outStream);
		fclose(outStream);
	}

	delete audioToWAV;
	audioToWAV = NULL;

	log.debug("audioToWAV", "Session::closeAudioTrack() end");
}

void Session::copy(InputStream &inStream, FILE *outStream, int length) {
	try {
		copy2(inStream, outStream, length);
	}
	catch (Exception *e) {
		inStream.skip(length);
		throw e;
	}
}

void Session::copy(const char *buf, FILE *outStream, int length) {
	int KBytes = sizeToKBytes(length);
	if (sizeInKBytes + KBytes > 1024 * config.getMaxMBytesPerSession())
		throw new Exception("session to big");

	int fd = fileno(outStream);
	if (fd == -1)
		throw new Exception("Session::copy: fd == -1");

	sizeInKBytes += sizeToKBytes(length);
	store->write(buf, length, fd);
}

void Session::copy2(InputStream &inStream, FILE *outStream, int &length) {
	int KBytes = sizeToKBytes(length);
	if (sizeInKBytes + KBytes > 1024 * config.getMaxMBytesPerSession())
		throw new Exception("session to big");

	int fd = fileno(outStream);
	if (fd == -1)
		throw new Exception("Session::copy2: fd == -1");

	while (length > 0) {
		char buf[16 * 1024];
		int n = inStream.read(buf, 0, min(length, sizeof buf));
		if (n <= 0)
			throw new Exception("unexpected end of inStream");
		length -= n;
		sizeInKBytes += sizeToKBytes(n);
		store->write(buf, n, fd);
	}
}

void Session::countFile(const char *pathName) {
	struct stat statBuf;
	if (lstat(pathName, &statBuf) != 0)
		return;

	if (!S_ISDIR(statBuf.st_mode)) {
		if (!S_ISREG(statBuf.st_mode))
			return;
		filesNum++;
		sizeInKBytes += sizeToKBytes(statBuf.st_size);
		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);
		countFile(tmpPathName);
	}

	closedir(dir);
}

void Session::create() {
	File infoFile(*dir, CDWserverInfo);
	infoFile.mkdirsParent();
	FILE *outStream = fopen(infoFile.getPath().getBytes(), "w");
	if (outStream == NULL)
		throw new Exception("internal error: Cannot create infofile.");
	fclose(outStream);
}

File *Session::getFirstTrack() {
	if (audioToWAV != NULL)
		closeAudioTrack();

	currentTrackNo = 0;
	return getNextTrack();
}

void Session::getInfo(const char *ID,
	int &numOfFiles, int &numOfTracks, int &size_KB) {
	File *dir = store->getSessionDir(ID);
	log.put(3, S.e + "dir = " + dir->getPath());
	if (dir->exists())
		readInfo(dir, numOfFiles, numOfTracks, size_KB);
	else
		numOfFiles = numOfTracks = size_KB = 0;
	delete dir;
}

int Session::getSessionSize(File &dir) {
	int sizeInKBytes = 0;
	File file(dir, CDWserverInfo);
	FILE *inStream = fopen(file.getPath().getBytes(), "r");
	if (inStream == NULL)
		throw new Exception("internal error: can't read sessionInfo");

	char lineStr[1024];
	while (fgets(lineStr, sizeof lineStr, inStream) != NULL) {
		char str1[1024], str2[1024];
		Config::parseLine(lineStr, str1, str2);

		if (strlen(str1) == 0 && strlen(str2) == 0)
			continue;
		else
		if (strcasecmp(str1, "sizeInKBytes") == 0)
			sscanf(str2, "%d", &sizeInKBytes);
	}
	fclose(inStream);
	return sizeInKBytes;
}

File *Session::getNextTrack() {
	File *nextTrack = NULL;

	DIR *dir = opendir(this->dir->getPath().getBytes());
	if (dir == NULL)
		return NULL;

	struct dirent *dirEntry;
	int n;
	while ((dirEntry = readdir(dir)) != NULL) {
		String name = dirEntry->d_name;
		if (!name.endsWith(".inf"))
		if (sscanf(dirEntry->d_name, "track%d", &n) == 1)
			if (n == currentTrackNo + 1) {
				nextTrack = new File(
					this->dir, dirEntry->d_name);
				break;
			}
	}

	closedir(dir);

	if (nextTrack == NULL)
		return NULL;

	if (nextTrack->exists()) {
		if (currentTrack != NULL)
			delete currentTrack;
		currentTrack = nextTrack;
		currentTrackNo++;
		return currentTrack;
	}
	else {
		delete nextTrack;
		return NULL;
	}
}

int Session::isSessionDir(File &dir) {
	File file(dir, CDWserverInfo);
	return file.exists();
}

void Session::linkFile(File &file, const char *linkName) {
	if (currentTrack == NULL
			|| !currentTrack->isDirectory())
		newTrack("dir");

	checkContainsNoLink(linkName);

	File link = new File(currentTrack, linkName);
	link.mkdirsParent();
	symlink(file.getPath().getBytes(), link.getPath().getBytes());
	linksNum++;
}

void Session::linkTrack(File &track, const char *type) {
	int useableWAV = 0;
	if (strcasecmp(type, "wav") == 0) {
		try {
			Wave wave(track);
			useableWAV = wave.getAudioChannels() == 2
				&& wave.getAudioSampleBits() == 16
				&& wave.getAudioSampleRate() == 44100;
		}
		catch (Exception *e) {
		}
	}

	if (strcasecmp(type, "mp3") == 0
			|| strcasecmp(type, "ogg") == 0
			|| (strcasecmp(type, "wav") == 0 && !useableWAV)) {
		int in = open(track.getPath().getBytes(), O_RDONLY);
		if (in < 0)
			throw new Exception(S.e + "Cannot open track "
				+ track.getPath() + " on the server");
		InputStream inStream(in);
		try {
			newTrack(type);
			putTrack(inStream, track.length());
		}
		catch (Exception *e) {
			close(in);
			throw e;
		}
		close(in);
	}
	else {
		newTrack(type, 1);
		symlink(track.getPath().getBytes(),
			currentTrack->getPath().getBytes());
	}
}

void Session::makeDir(const char *fileName, int mode, int time) {
	if (currentTrack == NULL
			|| !currentTrack->isDirectory())
		newTrack("dir");

	checkContainsNoLink(fileName);

	File dir = new File(currentTrack, fileName);
	dir.mkdirs();
	dir.setLastModified(time);
}

void Session::newTrack(const char *trackType, int link) {
	if (audioToWAV != NULL)
		closeAudioTrack();

	const char *extension = "";
	if (strcasecmp(trackType, "image") == 0
		|| strcasecmp(trackType, "iso") == 0)
		extension = ".iso";
	else
	if (strcasecmp(trackType, "inf") == 0) {
		if (currentTrack != NULL) {
			String name = currentTrack->getName();
			if (name.length() > 3) {
				name = name.substring(0, name.length() - 4);
				name = name + ".inf";
				*currentTrack = new File(dir, name.getBytes());
				return;
			}
		}
	}
	else
	if (strcasecmp(trackType, "wav") == 0) {
		extension = ".wav";
		if (!link)
			audioToWAV = new WAVToWAV();
	}
	else
	if (strcasecmp(trackType, "mp3") == 0) {
		extension = ".wav";
		if (!link)
			audioToWAV = new MP3ToWAV();
	}
	else
	if (strcasecmp(trackType, "ogg") == 0) {
		extension = ".wav";
		if (!link)
			audioToWAV = new OggToWAV();
	}

	tracksNum++;
	char nextTrackName[20];
	sprintf(nextTrackName, "track%03d%s", tracksNum, extension);
	if (currentTrack != NULL) {
		delete currentTrack;
		currentTrack = NULL;
	}
	currentTrack = new File(dir, nextTrackName);
	currentTrackNo = tracksNum;
}

void Session::putDummy(String &head,
		const char *fileName, int length, int mode, int time) {
	putFileStart(fileName);

	const int bufSize = 64 * 1024;
	char *buf = new char[bufSize];
	try {
		copy(head.getBytes(), fileOutputStream, head.length());
		length -= head.length();

//		char buf[16 * 1024];
		for (int i = 0; i < bufSize; i++)
			buf[i] = 0;
		while (length > 0) {
			int n = bufSize;
			if (n > length)
				n = length;
			copy(buf, fileOutputStream, n);
			length -= n;
		}
		delete[] buf;
	}
	catch (Exception *e) {
		delete[] buf;
		putFileEnd(mode, time);
		throw e;
	}

	putFileEnd(mode, time);
}

void Session::putFile(InputStream &inStream,
		const char *fileName, int length, int mode, int time) {
	try {
		putFileStart(fileName);
	}
	catch (Exception *e) {
		inStream.skip(length);
		throw e;
	}

	try {
		putFileContinue(inStream, length);
	}
	catch (Exception *e) {
		putFileEnd(mode, time);
		throw e;
	}

	putFileEnd(mode, time);
}

void Session::putFileContinue(InputStream &inStream, int length) {
	if (fileOutputStream == NULL) {
		inStream.skip(length);
		putFileContinue((const char *)NULL, 0);
	}

	copy(inStream, fileOutputStream, length);
}

void Session::putFileContinue(const char *buf, int length) {
	if (fileOutputStream == NULL) {
		log.put("Session::putFileContinue: no open file");
		throw new Exception("no open file");
	}

	copy(buf, fileOutputStream, length);
}

void Session::putFileEnd(int mode, int time) {
	if (fileOutputStream == NULL)
		return;

	fclose(fileOutputStream);
	fileOutputStream = NULL;

	if (mode >= 0) {
		chmod(currentFile.getPath().getBytes(), mode);
		currentFile.setLastModified(time);
	}
}

void Session::putFileStart(const char *fileName) {
	if (fileOutputStream != NULL)
		putFileEnd();

	if (currentTrack == NULL
		|| !currentTrack->isDirectory())
		newTrack("dir");

	checkContainsNoLink(fileName);

	currentFile = new File(currentTrack, fileName);
	currentFile.mkdirsParent();
	currentFile.remove();

	fileOutputStream = fopen(currentFile.getPath().getBytes(), "w");
	if (fileOutputStream == NULL) {
		log.put("Session::putFileStart: fopen");
		throw new Exception("cannot open file");
	}

	filesNum++;
}

void Session::putTrack(InputStream &inStream, int length) {
	FILE *outStream = fopen(currentTrack->getPath().getBytes(), "a");
	if (outStream == NULL) {
		inStream.skip(length);
		throw new Exception("cannot store track");
	}

	try {
		if (audioToWAV != NULL) {
			audioToWAV->setInStream(&inStream);
			audioToWAV->put(length);
			while (1) {
				const char *buf;
				int n = audioToWAV->get(&buf);
				if (n == 0)
					break;
				try {
					copy(buf, outStream, n);
				}
				catch (Exception *e) {
					while (audioToWAV->get(&buf) > 0);
					throw e;
				}
			}
		}
		else
			copy(inStream, outStream, length);
	}
	catch (Exception *e) {
		fclose(outStream);
		throw e;
	}
	fclose(outStream);
}

void Session::readInfo() {
	readInfo(dir, filesNum, tracksNum, sizeInKBytes);
}

void Session::readInfo(File *dir,
	int &numOfFiles, int &numOfTracks, int &size_KB) {
	File file(dir, CDWserverInfo);
	FILE *inStream = fopen(file.getPath().getBytes(), "r");
	if (inStream == NULL)
		throw new Exception("internal error: Cannot read sessionInfo.");

	char lineStr[1024];
	while (fgets(lineStr, sizeof lineStr, inStream) != NULL) {
		char str1[1024], str2[1024];
		Config::parseLine(lineStr, str1, str2);

		if (strlen(str1) == 0 && strlen(str2) == 0)
			continue;
		else
		if (strcasecmp(str1, "files") == 0)
			sscanf(str2, "%d", &numOfFiles);
		else
		if (strcasecmp(str1, "sizeInKBytes") == 0)
			sscanf(str2, "%d", &size_KB);
		else
		if (strcasecmp(str1, "tracks") == 0)
			sscanf(str2, "%d", &numOfTracks);
	}
	fclose(inStream);

	log.put("read previous session: "
		"files=%d, tracks=%d, size_KB=%d",
		numOfFiles, numOfTracks, size_KB);
}

void Session::remove(const char *fileName) {
	if (currentTrack == NULL
			|| !currentTrack->isDirectory())
		newTrack("dir");

	checkContainsNoLink(fileName);

	File file = new File(currentTrack, fileName);
	file.mkdirsParent();
	file.remove();
}

void Session::writeInfo() {
	File file = new File(dir, CDWserverInfo);
	FILE *outStream = fopen(file.getPath().getBytes(), "w");
	if (outStream != NULL) {
		fprintf(outStream, "sizeInKBytes=%d\n", sizeInKBytes);
		fprintf(outStream, "files=%d\n", filesNum);
		fprintf(outStream, "tracks=%d\n", tracksNum);
		fclose(outStream);
	}
}
