/*
 * MOC - music on console
 * Copyright (C) 2004 - 2006 Damian Pietras <daper@daper.net>
 *
 * This program 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 2 of the License, or
 * (at your option) any later version.
 *
 */

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include <unistd.h>
#include <fcntl.h>
#include <sys/file.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>
#include <ctype.h>
#include <errno.h>
#include <assert.h>
#include <uchardet.h>
#include <iconv.h>
#include <libcue/libcue.h>
#include <dirent.h>

#define DEBUG

#include "common.h"
#include "playlist.h"
#include "playlist_file.h"
#include "log.h"
#include "files.h"
#include "options.h"
#include "interface.h"
#include "decoder.h"
#include "utf8.h"

int is_plist_file (const char *name)
{
	const char *ext = ext_pos (name);

	if (ext && (!strcasecmp(ext, "m3u") || !strcasecmp(ext, "pls")
				|| !strcasecmp(ext, "cue")))
		return 1;

	return 0;
}

static void make_path (char *buf, size_t buf_size, const char *cwd, char *path)
{
	if (file_type(path) == F_URL) {
		strncpy (buf, path, buf_size);
		buf[buf_size-1] = 0;
		return;
	}

	if (path[0] != '/')
		strcpy (buf, cwd);
	else
		strcpy (buf, "/");

	resolve_path (buf, buf_size, path);
}

/* Strip white chars from the end of a string. */
static void strip_string (char *str)
{
	char *c = str;
	char *last_non_white = str;

	while (*c) {
		if (!isblank(*c))
			last_non_white = c;
		c++;
	}

	if (c > last_non_white)
		*(last_non_white + 1) = 0;
}

/* Charset conversion for playlist files. */
static char *auto_conv (const char *in, const char* from)
{
	if (in == NULL)
		return NULL;
	if (from && strcasecmp(from, "UTF-8") && strcasecmp(from, "ASCII")) {
		char ibuf[PATH_MAX];
		char obuf[PATH_MAX];
		char *p = ibuf;
		char *q = obuf;
		size_t u = strlen(in), v = sizeof(obuf);
		iconv_t ic;

		strncpy(ibuf, in, sizeof(ibuf));
		ic = iconv_open("UTF-8", from);
		iconv(ic, &p, &u, &q, &v);
		*q = '\0';
		iconv_close(ic);
		return xstrdup (obuf);
	}
	return xstrdup (in);
}

/* Load M3U file into plist.  Return the number of items read. */
static int plist_load_m3u (struct plist *plist, const char *fname,
		const char *cwd, const int load_serial)
{
	FILE *file;
	char ch1, ch2, ch3;
	char *line = NULL;
	int last_added = -1;
	int after_extinf = 0;
	int added = 0;
	uchardet_t ud;
	char buf[1024];
	char charset[128];
	size_t len;
	struct flock read_lock = {.l_type = F_RDLCK, .l_whence = SEEK_SET};

	file = fopen (fname, "r");
	if (!file) {
		error_errno ("Can't open playlist file", errno);
		return 0;
	}

	/* Lock gets released by fclose(). */
	if (fcntl (fileno (file), F_SETLKW, &read_lock) == -1)
		log_errno ("Can't lock the playlist file", errno);

	/* Detect charset of the M3U file. */
	ud = uchardet_new();
	uchardet_reset(ud);
	while ((len = fread(buf, 1, sizeof(buf), file)) > 0) {
		uchardet_handle_data(ud, buf, len);
	}
	uchardet_data_end(ud);
	strncpy(charset, uchardet_get_charset(ud), sizeof(charset));
	charset[sizeof(charset) - 1] = '\0';
	uchardet_delete(ud);
	
	fseek(file, SEEK_SET, 0);

	/* Skip BOM */
	ch1 = fgetc (file);
	ch2 = fgetc (file);
	ch3 = fgetc (file);
	if (ch1 != (char)0xef || ch2 != (char)0xbb || ch3 != (char)0xbf) {
		ungetc (ch3, file);
		ungetc (ch2, file);
		ungetc (ch1, file);
	}

	while ((line = read_line (file))) {
		if (!strncmp (line, "#EXTINF:", sizeof("#EXTINF:") - 1)) {
			char *comma, *num_err;
			char time_text[10] = "";
			int time_sec;
			char *title_tags_utf8;

			if (after_extinf) {
				error ("Broken M3U file: double #EXTINF!");
				plist_delete (plist, last_added);
				goto err;
			}

			/* Find the comma */
			comma = strchr (line + (sizeof("#EXTINF:") - 1), ',');
			if (!comma) {
				error ("Broken M3U file: no comma in #EXTINF!");
				goto err;
			}

			/* Get the time string */
			time_text[sizeof(time_text) - 1] = 0;
			strncpy (time_text, line + sizeof("#EXTINF:") - 1,
			         MIN(comma - line - (sizeof("#EXTINF:") - 1),
			         sizeof(time_text)));
			if (time_text[sizeof(time_text) - 1]) {
				error ("Broken M3U file: wrong time!");
				goto err;
			}

			/* Extract the time. */
			time_sec = strtol (time_text, &num_err, 10);
			if (*num_err) {
				error ("Broken M3U file: time is not a number!");
				goto err;
			}

			after_extinf = 1;
			last_added = plist_add (plist, NULL);
			title_tags_utf8 = auto_conv (comma + 1, charset);
			plist_set_title_tags (plist, last_added, title_tags_utf8);
			free (title_tags_utf8);

			if (*time_text)
				plist_set_item_time (plist, last_added, time_sec);
		}
		else if (line[0] != '#') {
			char path[2 * PATH_MAX];

			strip_string (line);
			if (strlen (line) <= PATH_MAX) {
				file_t *f;
				char *l = auto_conv (line, charset);
				free (line);
				if (options_get_bool ("FileNamesIconv")) {
					line = files_iconv_r_str (l);
					free (l);
				}
				else
					line = l;
				make_path (path, sizeof(path), cwd, line);
				f = new_file_t (path, -1);

				if (plist_find_file (plist, f) == -1) {
					if (after_extinf)
						plist_set_fname (plist, last_added, path);
					else
						plist_add (plist, f);
					added += 1;
					free_file_t (f);
				}
				else if (after_extinf)
					plist_delete (plist, last_added);
			}
			else if (after_extinf)
				plist_delete (plist, last_added);

			after_extinf = 0;
		}
		else if (load_serial &&
		         !strncmp (line, "#MOCSERIAL: ", sizeof("#MOCSERIAL: ") - 1)) {
			char *serial_str = line + sizeof("#MOCSERIAL: ") - 1;

			if (serial_str[0]) {
				char *err;
				long serial;

				serial = strtol (serial_str, &err, 0);
				if (!*err) {
					plist_set_serial (plist, serial);
					logit ("Got MOCSERIAL tag with serial %ld", serial);
				}
			}
		}
		else if (!strncmp (line, "#MOCSTARTOFFSET: ", sizeof("#MOCSTARTOFFSET: ") - 1)) {
			char *start_offset_str = line + sizeof("#MOCSTARTOFFSET: ") - 1;

			if (start_offset_str[0]) {
				char *err;
				int start_offset;

				start_offset = strtol (start_offset_str, &err, 0);
				if (!*err) {
					plist_set_file_start_offset (plist, last_added, start_offset);
					logit ("Got MOCSTARTOFFSET tag with serial %d", start_offset);
				}
			}
		}
		free (line);
	}

err:
	free (line);
	fclose (file);
	return added;
}

/* Return 1 if the line contains only blank characters, 0 otherwise. */
static int is_blank_line (const char *l)
{
	while (*l && isblank(*l))
		l++;

	if (*l)
		return 0;
	return 1;
}

/* Read a value from the given section from .INI file.  File should be opened
 * and seeking will be performed on it.  Return the malloc()ed value or NULL
 * if not present or error occurred. */
static char *read_ini_value (FILE *file, const char *section, const char *key)
{
	char *line = NULL;
	int in_section = 0;
	char *value = NULL;
	int key_len;

	if (fseek(file, 0, SEEK_SET)) {
		error_errno ("File fseek() error", errno);
		return NULL;
	}

	key_len = strlen (key);

	while ((line = read_line(file))) {
		if (line[0] == '[') {
			if (in_section) {

				/* we are outside of the interesting section */
				free (line);
				break;
			}
			else {
				char *close = strchr (line, ']');

				if (!close) {
					error ("Parse error in the INI file");
					free (line);
					break;
				}

				if (!strncasecmp(line + 1, section,
							close - line - 1))
					in_section = 1;
			}
		}
		else if (in_section && line[0] != '#' && !is_blank_line(line)) {
			char *t, *t2;

			t2 = t = strchr (line, '=');

			if (!t) {
				error ("Parse error in the INI file");
				free (line);
				break;
			}

			/* go back to the last char in the name */
			while (t2 >= t && (isblank(*t2) || *t2 == '='))
				t2--;

			if (t2 == t) {
				error ("Parse error in the INI file");
				free (line);
				break;
			}

			if (!strncasecmp(line, key,
						MAX(t2 - line + 1, key_len))) {
				value = t + 1;

				while (isblank(value[0]))
					value++;

				if (value[0] == '"') {
					char *q = strchr (value + 1, '"');

					if (!q) {
						error ("Parse error in the INI file");
						free (line);
						break;
					}

					*q = 0;
				}

				value = xstrdup (value);
				free (line);
				break;
			}
		}

		free (line);
	}

	return value;
}

/* Load PLS file into plist. Return the number of items read. */
static int plist_load_pls (struct plist *plist, const char *fname,
		const char *cwd)
{
	FILE *file;
	char ch1, ch2, ch3;
	char *e, *line = NULL;
	long i, nitems, added = 0;
	uchardet_t ud;
	char buf[1024];
	char charset[128];
	size_t len;

	file = fopen (fname, "r");
	if (!file) {
		error_errno ("Can't open playlist file", errno);
		return 0;
	}

	/* Detect charset of the PLS file. */
	ud = uchardet_new();
	uchardet_reset(ud);
	while ((len = fread(buf, 1, sizeof(buf), file)) > 0) {
		uchardet_handle_data(ud, buf, len);
	}
	uchardet_data_end(ud);
	strncpy(charset, uchardet_get_charset(ud), sizeof(charset));
	charset[sizeof(charset) - 1] = '\0';
	uchardet_delete(ud);
	
	fseek(file, SEEK_SET, 0);

	/* Skip BOM */
	ch1 = fgetc (file);
	ch2 = fgetc (file);
	ch3 = fgetc (file);
	if (ch1 != (char)0xef || ch2 != (char)0xbb || ch3 != (char)0xbf) {
		ungetc (ch3, file);
		ungetc (ch2, file);
		ungetc (ch1, file);
	}

	line = read_ini_value (file, "playlist", "NumberOfEntries");
	if (!line) {

		/* Assume that it is a pls file version 1 - plist_load_m3u()
		 * should handle it like an m3u file without the m3u extensions. */
		fclose (file);
		return plist_load_m3u (plist, fname, cwd, 0);
	}

	nitems = strtol (line, &e, 10);
	if (*e) {
		error ("Broken PLS file");
		goto err;
	}

	for (i = 1; i <= nitems; i++) {
		int time, last_added;
		char *pls_file, *pls_title, *pls_length;
		char key[32], path[2 * PATH_MAX];

		sprintf (key, "File%ld", i);
		pls_file = read_ini_value (file, "playlist", key);
		if (!pls_file) {
			error ("Broken PLS file");
			goto err;
		}

		sprintf (key, "Title%ld", i);
		pls_title = read_ini_value (file, "playlist", key);

		sprintf (key, "Length%ld", i);
		pls_length = read_ini_value (file, "playlist", key);

		if (pls_length) {
			time = strtol (pls_length, &e, 10);
			if (*e)
				time = -1;
		}
		else
			time = -1;

		if (strlen (pls_file) <= PATH_MAX) {
			file_t *file;
			char *f = auto_conv (pls_file, charset);
			free (pls_file);
			if (options_get_bool ("FileNamesIconv")) {
				pls_file = files_iconv_r_str (f);
				free (f);
			}
			else
				pls_file = f;
			make_path (path, sizeof(path), cwd, pls_file);
			file = new_file_t (path, -1);
			if (plist_find_file (plist, file) == -1) {
				last_added = plist_add (plist, file);

				if (pls_title && pls_title[0]) {
					char *t = pls_title;
					pls_title = auto_conv (pls_title, charset);
					free (t);
					plist_set_title_tags (plist, last_added, pls_title);
				}

				if (time > 0) {
					plist->items[last_added].tags = tags_new ();
					plist->items[last_added].tags->time = time;
					plist->items[last_added].tags->filled |= TAGS_TIME;
				}
			}
			free_file_t (file);
		}

		free (pls_file);
		if (pls_title)
			free (pls_title);
		if (pls_length)
			free (pls_length);
		added += 1;
	}

err:
	free (line);
	fclose (file);
	return added;
}

/* Load CUE sheet into plist.  Return the number of items read. */
static int plist_load_cue (struct plist *plist, const char *fname,
		const char *cwd)
{
	FILE *file;
	uchardet_t ud;
	char buf[1024];
	char charset[128];
	Cd *cd;
	Cdtext *text;
	const char *album;
	char *album_cvt = NULL;
	int ntrack;
	int added = 0;
	int i;
	size_t len;
	int last_added = -1;
	struct flock read_lock = {.l_type = F_RDLCK, .l_whence = SEEK_SET};

	file = fopen (fname, "r");
	if (!file) {
		error_errno ("Can't open CUE sheet", errno);
		return 0;
	}

	/* Lock gets released by fclose(). */
	if (fcntl (fileno (file), F_SETLKW, &read_lock) == -1)
		log_errno ("Can't lock the playlist file", errno);

	/* Detect charset of the CUE sheet */
	ud = uchardet_new();
	uchardet_reset(ud);
	while ((len = fread(buf, 1, sizeof(buf), file)) > 0) {
		uchardet_handle_data(ud, buf, len);
	}
	uchardet_data_end(ud);
	strncpy(charset, uchardet_get_charset(ud), sizeof(charset));
	charset[sizeof(charset) - 1] = '\0';
	uchardet_delete(ud);
	
	fseek(file, SEEK_SET, 0);
	/* Parse CUE sheet */
	do { /* Suppress error output from libcue */
		int fd_err = dup(2);
		int fd_null = open("/dev/null", O_WRONLY);
		dup2(fd_null, 2);
		close(fd_null);
		cd = cue_parse_file(file);
		dup2(fd_err, 2);
		close(fd_err);
	} while (false);
	fclose(file);
	if (cd == NULL) {
		error ("Broken CUE sheet.");
		goto err;
	}
	text = cd_get_cdtext(cd);
	album = text ? cdtext_get(PTI_TITLE, text) : "";
	album_cvt = auto_conv(album ? album : "", charset);

	ntrack = cd_get_ntrack(cd);
	for (i = 1; i <= ntrack; i++) {
		Track *track;
		file_t tr_file;
		int prev_added;
		char path[2 * PATH_MAX];
		const char *fname = NULL;
		char *fname_cnv = NULL;
		int tr_start;
		int tr_length;
		tr_file.name = path;

		track = cd_get_track(cd, i);
		if (track == NULL) {
			error ("Broken CUE sheet (invalid track): %d", i);
			if (added)
				plist_delete (plist, added);
			goto err;
		}

		fname = track_get_filename(track);
		if (!fname) {
			error("No file name on the track.");
			continue;
		}
		fname_cnv = auto_conv(fname, charset);
		if (options_get_bool ("FileNamesIconv")) {
			char *f = files_iconv_r_str (fname_cnv);
			free (fname_cnv);
			fname_cnv = f;
		}
		make_path (path, sizeof(path), cwd, fname_cnv);
		if (!file_exists(path)) {
			/* Search the same file name with different extention. */
			int found = 0;
			DIR *dir = opendir(cwd);
			struct dirent *dirent;
			while ((dirent = readdir(dir)) != NULL) {
				char *p;
				make_path (path, sizeof(path), cwd, dirent->d_name);
				if (file_type(path) != F_SOUND)
					continue;
				if ((p = strrchr(dirent->d_name, '.'))) {
					int basename_len = p - dirent->d_name;
					if (!strncmp (dirent->d_name, fname_cnv, basename_len)) {
						found = 1;
						break;
					}
				}
			}
			closedir(dir);
			if (!found) {
				error ("No such file: %s", fname_cnv);
				free(fname_cnv);
				continue;
			}
		}
		free(fname_cnv);

		tr_start = track_get_start(track);
		tr_file.start_offset = tr_start / 75;
		prev_added = last_added;
		last_added = plist_add (plist, &tr_file);
		if (last_added >= 0)
			++added;
		else {
			error ("Cannot add file: %s", fname);
			continue;
		}
		plist->items[last_added].tags = tags_new ();
		text = track_get_cdtext(track);
		if (text) {
			const char *title = cdtext_get(PTI_TITLE, text);
			const char *artist = cdtext_get(PTI_PERFORMER, text);
			if (title) {
				char *title_cnv = auto_conv(title, charset);
				plist->items[last_added].tags->title = xstrdup (title_cnv);
				plist->items[last_added].tags->filled |= TAGS_COMMENTS;
				free(title_cnv);
			}
			if (artist) {
				char *artist_cnv = auto_conv(artist, charset);
				plist->items[last_added].tags->artist = xstrdup (artist_cnv);
				free(artist_cnv);
			}
		}

		if (prev_added >= 0 &&
			!strcmp(plist->items[prev_added].file.name, tr_file.name)) {
			/* Fix length of previsous track. Otherwise, the gap
			   between INDEX 0 and INDEX 1 will be lost. */
			tr_length = tr_file.start_offset
							- plist->items[prev_added].file.start_offset;
			plist_set_item_time (plist, prev_added, tr_length);
		}
		tr_length = track_get_length(track);
		if (tr_length > 0)
			tr_length = (tr_start + tr_length) / 75 - tr_file.start_offset;
		else {
			void *data = NULL;
			struct decoder *f = get_decoder(tr_file.name);
			if (f)
				data = f->open(tr_file.name);
			if (data) {
				tr_length = f->get_duration(data);
				tr_length = tr_length - tr_file.start_offset;
				f->close(data);
			} else {
				error ("Cannot get track length: %d. Skip this track.", i);
				plist_delete(plist, last_added);
				--added;
			}
		}
		plist_set_item_time (plist, last_added, tr_length);
		plist->items[last_added].tags->track = i;
		plist->items[last_added].tags->album = xstrdup (album_cvt);
		make_tags_title (plist, last_added);
	}

err:
	free (album_cvt);
	return added;
}

/* Load a playlist into plist. Return the number of items on the list. */
/* The playlist may have deleted items. */
int plist_load (struct plist *plist, const char *fname, const char *cwd,
		const int load_serial)
{
	int num, read_tags;
	const char *ext;

	read_tags = options_get_bool ("ReadTags");
	ext = ext_pos (fname);

	if (ext && !strcasecmp(ext, "pls"))
		num = plist_load_pls (plist, fname, cwd);
	else if (ext && !strcasecmp(ext, "cue"))
		num = plist_load_cue (plist, fname, cwd);
	else
		num = plist_load_m3u (plist, fname, cwd, load_serial);

	if (read_tags)
		switch_titles_tags (plist);
	else
		switch_titles_file (plist);

	return num;
}

/* Save the playlist into the file in m3u format.  If save_serial is not 0,
 * the playlist serial is saved in a comment. */
int plist_save (struct plist *plist, const char *fname, const int save_serial)
{
	FILE *file = NULL;
	int i, ret, result = 0;
	struct flock write_lock = {.l_type = F_WRLCK, .l_whence = SEEK_SET};

	debug ("Saving playlist to '%s'", fname);

	file = fopen (fname, "w");
	if (!file) {
		error_errno ("Can't save playlist", errno);
		return 0;
	}

	/* Lock gets released by fclose(). */
	if (fcntl (fileno (file), F_SETLKW, &write_lock) == -1)
		log_errno ("Can't lock the playlist file", errno);

	if (fprintf (file, "#EXTM3U\r\n") < 0) {
		error_errno ("Error writing playlist", errno);
		goto err;
	}

	if (save_serial && fprintf (file, "#MOCSERIAL: %d\r\n",
	                                  plist_get_serial (plist)) < 0) {
		error_errno ("Error writing playlist", errno);
		goto err;
	}

	for (i = 0; i < plist->num; i++) {
		if (!plist_deleted (plist, i)) {

			/* EXTM3U */
			if (plist->items[i].tags)
				ret = fprintf (file, "#EXTINF:%d,%s\r\n"
						"#MOCSTARTOFFSET: %d\r\n",
						plist->items[i].tags->time,
						plist->items[i].title_tags ?
						plist->items[i].title_tags
						: plist->items[i].title_file,
						plist->items[i].file.start_offset);
			else
				ret = fprintf (file, "#EXTINF:%d,%s\r\n"
						"#MOCSTARTOFFSET: %d\r\n", 0,
						plist->items[i].title_file,
						plist->items[i].file.start_offset);

			/* file */
			if (ret >= 0) {
				char *fname;
				if (options_get_bool ("FileNamesIconv"))
					fname = files_iconv_str (plist->items[i].file.name);
				else
					fname = xstrdup (plist->items[i].file.name);
				ret = fprintf (file, "%s\r\n", fname);
				free (fname);
			}

			if (ret < 0) {
				error_errno ("Error writing playlist", errno);
				goto err;
			}
		}
	}

	ret = fclose (file);
	file = NULL;
	if (ret)
		error_errno ("Error writing playlist", errno);
	else
		result = 1;

err:
	if (file)
		fclose (file);
	return result;
}
