/*
 * Copyright 2008-2013 Various Authors
 * Copyright 2008 Timo Hirvonen
 *
 * 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.
 *
 * This program 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 this program; if not, see <http://www.gnu.org/licenses/>.
 */

#include "utils.h"
#include "job.h"
#include "worker.h"
#include "cache.h"
#include "xmalloc.h"
#include "debug.h"
#include "load_dir.h"
#include "path.h"
#include "editable.h"
#include "pl.h"
#include "play_queue.h"
#include "lib.h"
#include "utils.h"
#include "file.h"
#include "cache.h"
#include "player.h"
#include "discid.h"
#include "xstrjoin.h"
#include "ui_curses.h"
#include "cue_utils.h"
#include "pl_env.h"
#include "misc.h"

#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/mman.h>

enum job_result_var {
	JOB_RES_ADD,
	JOB_RES_UPDATE,
	JOB_RES_UPDATE_CACHE,
	JOB_RES_PL_DELETE,
};

enum update_kind {
	UPDATE_NONE = 0,
	UPDATE_REMOVE = 1,
	UPDATE_MTIME_CHANGED = 2,
};

struct job_result {
	struct list_head node;

	enum job_result_var var;
	union {
		struct {
			add_ti_cb add_cb;
			size_t add_num;
			struct track_info **add_ti;
			void *add_opaque;
		};
		struct {
			size_t update_num;
			struct track_info **update_ti;
			enum update_kind *update_kind;
		};
		struct {
			size_t update_cache_num;
			struct track_info **update_cache_ti;
		};
		struct {
			void (*pl_delete_cb)(struct playlist *);
			struct playlist *pl_delete_pl;
		};
	};
};

int job_fd;
static int job_fd_priv;

static LIST_HEAD(job_result_head);
static pthread_mutex_t job_mutex = CMUS_MUTEX_INITIALIZER;

#define TI_CAP 32
static struct track_info **ti_buffer;
static size_t ti_buffer_fill;
static struct add_data *jd;

#define job_lock() cmus_mutex_lock(&job_mutex)
#define job_unlock() cmus_mutex_unlock(&job_mutex)

void job_init(void)
{
	init_pipes(&job_fd, &job_fd_priv);

	worker_init();
}

void job_exit(void)
{
	worker_remove_jobs_by_type(JOB_TYPE_ANY);
	worker_exit();

	close(job_fd);
	close(job_fd_priv);
}

static void job_push_result(struct job_result *res)
{
	job_lock();
	list_add_tail(&res->node, &job_result_head);
	job_unlock();

	notify_via_pipe(job_fd_priv);
}

static struct job_result *job_pop_result(void)
{
	struct job_result *res = NULL;

	job_lock();
	if (!list_empty(&job_result_head)) {
		struct list_head *item = job_result_head.next;
		list_del(item);
		res = container_of(item, struct job_result, node);
	}
	job_unlock();

	return res;
}

static void flush_ti_buffer(void)
{
	struct job_result *res = xnew(struct job_result, 1);

	res->var = JOB_RES_ADD;
	res->add_cb = jd->add;
	res->add_num = ti_buffer_fill;
	res->add_ti = ti_buffer;
	res->add_opaque = jd->opaque;

	job_push_result(res);

	ti_buffer_fill = 0;
	ti_buffer = NULL;
}

static void add_ti(struct track_info *ti)
{
	if (ti_buffer_fill == TI_CAP)
		flush_ti_buffer();
	if (!ti_buffer)
		ti_buffer = xnew(struct track_info *, TI_CAP);
	ti_buffer[ti_buffer_fill++] = ti;
}

static int add_file_cue(const char *filename);

static void add_file(const char *filename, int force)
{
	struct track_info *ti;

	if (!is_cue_url(filename)) {
		if (force || lookup_cache_entry(filename, hash_str(filename)) == NULL) {
			int done = add_file_cue(filename);
			if (done)
				return;
		}
	}

	cache_lock();
	ti = cache_get_ti(filename, force);
	cache_unlock();

	if (ti)
		add_ti(ti);
}

static int add_file_cue(const char *filename)
{
	if (!is_cue(filename))
		return 0;

	int *track_nums;
	int n = cue_get_track_nums(filename, &track_nums);
	if (n == -1)
		return 0;

	for (int i = 0; i < n; i++) {
		char *url = construct_cue_url(filename, track_nums[i]);
		add_file(url, 0);
		free(url);
	}

	free(track_nums);
	return 1;
}

static void add_url(const char *url)
{
	add_file(url, 0);
}

static void add_cdda(const char *url)
{
	char *disc_id = NULL;
	int start_track = 1, end_track = -1;

	parse_cdda_url(url, &disc_id, &start_track, &end_track);

	if (end_track != -1) {
		int i;
		for (i = start_track; i <= end_track; i++) {
			char *new_url = gen_cdda_url(disc_id, i, -1);
			add_file(new_url, 0);
			free(new_url);
		}
	} else
		add_file(url, 0);
	free(disc_id);
}

struct dir_list_entry {
	struct list_head node;

	char *path;
};

static void dir_list_truncate(struct list_head *head, struct list_head *new_tail)
{
	if (head->prev == new_tail)
		return;

	struct list_head to_remove;
	to_remove.next = new_tail->next;
	new_tail->next->prev = &to_remove;
	to_remove.prev = head->prev;
	head->prev->next = &to_remove;

	head->prev = new_tail;
	new_tail->next = head;

	struct dir_list_entry *d, *next;
	list_for_each_entry_safe(d, next, &to_remove, node) {
		free(d->path);
		free(d);
	}
}

static void dir_list(const char *dirname, const char *root,
		struct ptr_array *files, struct list_head *dirs)
{
	struct directory dir;
	const char *name;
	struct list_head *dirs_backup = list_prev(dirs);
	int files_backup = files->count;

	if (dir_open(&dir, dirname)) {
		d_print("error: opening %s: %s\n", dirname, strerror(errno));
		return;
	}
	while ((name = dir_read(&dir))) {
		if (strcmp(name, ".nomusic") == 0 || strcmp(name, ".nomedia") == 0) {
			ptr_array_truncate(files, files_backup);
			dir_list_truncate(dirs, dirs_backup);
			break;
		}

		if (name[0] == '.')
			continue;

		if (S_ISDIR(dir.st.st_mode)) {
			if (!dir.is_link) {
				struct dir_list_entry *ent;
				ent = xnew(struct dir_list_entry, 1);
				ent->path = path_absolute_cwd(name, dirname);
				list_add_tail(&ent->node, dirs);
			}
			continue;
		}

		ptr_array_add(files, path_absolute_cwd(name, dirname));
	}
	dir_close(&dir);
}

static void dir_list_recursive(const char *dirname, const char *root,
		struct ptr_array *files)
{
	struct list_head dirs;
	list_init(&dirs);

	struct dir_list_entry *ent;
	ent = xnew(struct dir_list_entry, 1);
	ent->path = xstrdup(dirname);
	list_add_tail(&ent->node, &dirs);

	while (!list_empty(&dirs)) {
		struct list_head *tail = list_prev(&dirs);
		ent = list_entry(tail, struct dir_list_entry, node);

		dir_list(ent->path, root, files, &dirs);

		list_del(tail);
		free(ent->path);
		free(ent);
	}
}

static void handle_cue_files(struct ptr_array *files)
{
	int i, j, k;
	char **ents = files->ptrs;
	bool *to_remove = xnew0(bool, files->count);

	for (i = 0; i < files->count; i++) {
		if (!ents[i] || !is_cue(ents[i]))
			continue;

		char **files_in_cue;
		int n = cue_get_files(ents[i], &files_in_cue);
		if (n == -1) 
			continue;
		char *cue_dir = path_dirname(ents[i]);

		for (j = 0; j < n; j++) {
			char *path = path_absolute_cwd(files_in_cue[j], cue_dir);
			k = ptr_array_bsearch(&path, files, strptrcmp);
			if (k >= 0)
				to_remove[k] = true;
			free(path);
			free(files_in_cue[j]);
		}
		free(cue_dir);
		free(files_in_cue);
	}

	for (i = 0; i < files->count; i++) {
		if (to_remove[i]) {
			free(ents[i]);
			ents[i] = NULL;
		}
	}
	free(to_remove);
}

static void add_dir(const char *dirname, const char *root)
{
	PTR_ARRAY(files);

	dir_list_recursive(dirname, root, &files);

	ptr_array_sort(&files, strptrcmp);
	ptr_array_unique(&files, strptrcmp);

	handle_cue_files(&files);

	int i;
	char **ents = files.ptrs;
	if (jd->add == play_queue_prepend) {
		for (i = files.count - 1; i >= 0 && !worker_cancelling(); i--)
			if (ents[i])
				add_file(ents[i], 0);
	} else {
		for (i = 0; i < files.count && !worker_cancelling(); i++)
			if (ents[i])
				add_file(ents[i], 0);
	}

	ptr_array_clear(&files);
}

static int handle_line(void *data, const char *line)
{
	if (worker_cancelling())
		return 1;

	if (is_http_url(line) || is_cue_url(line)) {
		add_url(line);
	} else {
		char *absolute = pl_env_var(line, NULL)
			? pl_env_expand(line)
			: path_absolute_cwd(line, data);
		add_file(absolute, 0);
		free(absolute);
	}

	return 0;
}

static void add_pl(const char *filename)
{
	char *buf;
	ssize_t size;
	int reverse;

	buf = mmap_file(filename, &size);
	if (size == -1)
		return;

	if (buf) {
		char *cwd = xstrjoin(filename, "/..");
		/* beautiful hack */
		reverse = jd->add == play_queue_prepend;

		cmus_playlist_for_each(buf, size, reverse, handle_line, cwd);
		free(cwd);
		munmap(buf, size);
		add_ti(NULL); // marks end of load
	}
}

static void do_add_job(void *data)
{
	jd = data;
	switch (jd->type) {
	case FILE_TYPE_URL:
		add_url(jd->name);
		break;
	case FILE_TYPE_CDDA:
		add_cdda(jd->name);
		break;
	case FILE_TYPE_PL:
		add_pl(jd->name);
		break;
	case FILE_TYPE_DIR:
		add_dir(jd->name, jd->name);
		break;
	case FILE_TYPE_FILE:
		add_file(jd->name, jd->force);
		break;
	case FILE_TYPE_INVALID:
		break;
	}
	if (ti_buffer)
		flush_ti_buffer();
	jd = NULL;
}

static void free_add_job(void *data)
{
	struct add_data *d = data;
	free(d->name);
	free(d);
}

static void job_handle_add_result(struct job_result *res)
{
	for (size_t i = 0; i < res->add_num; i++) {
		res->add_cb(res->add_ti[i], res->add_opaque);
		if (res->add_ti[i] != NULL)
			track_info_unref(res->add_ti[i]);
	}

	free(res->add_ti);
}

void job_schedule_add(int type, struct add_data *data)
{
	worker_add_job(type | JOB_TYPE_ADD, do_add_job, free_add_job, data);
}

static void do_update_job(void *data)
{
	struct update_data *d = data;
	int i;
	enum update_kind *kind = xnew(enum update_kind, d->used);
	struct job_result *res;

	for (i = 0; i < d->used; i++) {
		struct track_info *ti = d->ti[i];
		struct stat s;
		int rc;

		rc = stat(ti->filename, &s);
		if (rc || d->force || ti->mtime != s.st_mtime || ti->duration == 0) {
			kind[i] = UPDATE_NONE;
			if (!is_cue_url(ti->filename) && !is_http_url(ti->filename) && rc)
				kind[i] |= UPDATE_REMOVE;
			else if (ti->mtime != s.st_mtime)
				kind[i] |= UPDATE_MTIME_CHANGED;
		} else {
			track_info_unref(ti);
			d->ti[i] = NULL;
		}
	}

	res = xnew(struct job_result, 1);

	res->var = JOB_RES_UPDATE;
	res->update_num = d->used;
	res->update_ti = d->ti;
	res->update_kind = kind;

	job_push_result(res);

	d->ti = NULL;
}

static void free_update_job(void *data)
{
	struct update_data *d = data;

	if (d->ti) {
		for (size_t i = 0; i < d->used; i++)
			track_info_unref(d->ti[i]);
		free(d->ti);
	}
	free(d);
}

static void job_handle_update_result(struct job_result *res)
{
	for (size_t i = 0; i < res->update_num; i++) {
		struct track_info *ti = res->update_ti[i];
		int force;

		if (!ti)
			continue;

		lib_remove(ti);

		cache_lock();
		cache_remove_ti(ti);
		cache_unlock();

		if (res->update_kind[i] & UPDATE_REMOVE) {
			d_print("removing dead file %s\n", ti->filename);
		} else {
			if (res->update_kind[i] & UPDATE_MTIME_CHANGED)
				d_print("mtime changed: %s\n", ti->filename);
			force = ti->duration == 0;
			cmus_add(lib_add_track, ti->filename, FILE_TYPE_FILE,
					JOB_TYPE_LIB, force, NULL);
		}

		track_info_unref(ti);
	}

	free(res->update_kind);
	free(res->update_ti);
}

void job_schedule_update(struct update_data *data)
{
	worker_add_job(JOB_TYPE_LIB | JOB_TYPE_UPDATE, do_update_job,
			free_update_job, data);
}

static void do_update_cache_job(void *data)
{
	struct update_cache_data *d = data;
	int count;
	struct track_info **tis;
	struct job_result *res;

	cache_lock();
	tis = cache_refresh(&count, d->force);
	cache_unlock();

	res = xnew(struct job_result, 1);
	res->var = JOB_RES_UPDATE_CACHE;
	res->update_cache_ti = tis;
	res->update_cache_num = count;
	job_push_result(res);
}

static void free_update_cache_job(void *data)
{
	free(data);
}

static void job_handle_update_cache_result(struct job_result *res)
{
	for (size_t i = 0; i < res->update_cache_num; i++) {
		struct track_info *new, *old = res->update_cache_ti[i];

		if (!old)
			continue;

		new = old->next;
		if (lib_remove(old) && new)
			lib_add_track(new, NULL);
		pl_update_track(old, new);
		editable_update_track(&pq_editable, old, new);
		if (player_info.ti == old && new) {
			track_info_ref(new);
			player_file_changed(new);
		}

		track_info_unref(old);
		if (new)
			track_info_unref(new);
	}
	free(res->update_cache_ti);
}

void job_schedule_update_cache(int type, struct update_cache_data *data)
{
	worker_add_job(type | JOB_TYPE_UPDATE_CACHE, do_update_cache_job,
			free_update_cache_job, data);
}

static void do_pl_delete_job(void *data)
{
	/*
	 * If PL jobs are canceled this function won't run. Hence we push the
	 * result in the free function.
	 */
}

static void free_pl_delete_job(void *data)
{
	struct pl_delete_data *pdd = data;
	struct job_result *res;

	res = xnew(struct job_result, 1);
	res->var = JOB_RES_PL_DELETE;
	res->pl_delete_cb = pdd->cb;
	res->pl_delete_pl = pdd->pl;
	job_push_result(res);

	free(pdd);
}

static void job_handle_pl_delete_result(struct job_result *res)
{
	res->pl_delete_cb(res->pl_delete_pl);
}

void job_schedule_pl_delete(struct pl_delete_data *data)
{
	worker_add_job(JOB_TYPE_PL | JOB_TYPE_DELETE, do_pl_delete_job,
			free_pl_delete_job, data);
}

static void job_handle_result(struct job_result *res)
{
	switch (res->var) {
	case JOB_RES_ADD:
		job_handle_add_result(res);
		break;
	case JOB_RES_UPDATE:
		job_handle_update_result(res);
		break;
	case JOB_RES_UPDATE_CACHE:
		job_handle_update_cache_result(res);
		break;
	case JOB_RES_PL_DELETE:
		job_handle_pl_delete_result(res);
		break;
	}
	free(res);
}

void job_handle(void)
{
	clear_pipe(job_fd, -1);

	struct job_result *res;
	while ((res = job_pop_result()))
		job_handle_result(res);
}
