/* vifm
 * Copyright (C) 2011 xaizek.
 *
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
 */

#include "term_title.h"

#ifdef _WIN32
#include <windows.h>
#else
#include <curses.h> /* for <term.h> */
#include <term.h> /* cur_term del_curterm() setupterm() tgoto() tigetstr() */
#endif

#if !defined(_WIN32) && defined(HAVE_X11)
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#ifdef DYN_X11
#include <dlfcn.h> /* dlopen() dlsym() dlclose() */
#endif
#endif

/* Headers above define these macros which messes up config.h. */
#ifdef lines
# undef lines
#endif
#ifdef columns
# undef columns
#endif

#include <stddef.h> /* NULL size_t */
#include <stdio.h> /* stdout fflush() */
#include <stdlib.h> /* atol() free() */
#include <string.h> /* strcmp() */

#include "../cfg/config.h"
#include "../utils/env.h"
#include "../utils/macros.h"
#include "../utils/str.h"
#include "../utils/test_helpers.h"
#include "../utils/utf8.h"
#include "../utils/utils.h"
#include "../vifm.h"

/* Kind of title we're working with. */
typedef enum
{
	TK_ABSENT,  /* No title support. */
	TK_REGULAR, /* Normal title that can be saved and restored. */
	TK_SCREEN,  /* GNU screen compatible. */
}
TitleKind;

enum
{
	/* Limit on the length of title message (variable part of the title).  It
	 * would be smaller if it wasn't specified for a UTF-8 string. */
	MAX_TITLE_MSG_LEN = 700,
};

static void ensure_initialized(void);
static TitleKind query_title_kind(void);
TSTATIC TitleKind title_kind_for_termenv(const char term[]);
static void apply_term_guess(TitleKind kind);
static void save_term_title(void);
static void restore_term_title(void);
#if !defined(_WIN32) && defined(HAVE_X11)
static int get_x11_disp_and_win(Display **disp, Window *win);
static void get_x11_window_title(Display *disp, Window win, char *buf,
		size_t buf_len);
static int x_error_check(Display *dpy, XErrorEvent *error_event);
#endif
static void set_terminal_title(const char path[]);

#if !defined(_WIN32) && defined(HAVE_X11)
#ifdef DYN_X11

static void try_dynload_xlib(void);
static int dynload_xlib(void);
static void set_xlib_stubs(void);
static void unload_xlib(void);

static Display *XOpenDisplayStub(_Xconst char name[]);
static Status XGetWMNameStub(Display *display, Window w,
	XTextProperty* text_prop_return);
static XErrorHandler XSetErrorHandlerStub(XErrorHandler handler);
static int XFreeStub(void* data);

/* Handle of dynamically loaded xlib. */
static void *xlib_handle;

/* Indirection level for functions of the xlib. */
static typeof(XOpenDisplay) *XOpenDisplayWrapper;
static typeof(XGetWMName) *XGetWMNameWrapper;
static typeof(XSetErrorHandler) *XSetErrorHandlerWrapper;
static typeof(XFree) *XFreeWrapper;

#else

/* Indirection level for functions of the xlib. */
static typeof(XOpenDisplay) *XOpenDisplayWrapper = &XOpenDisplay;
static typeof(XGetWMName) *XGetWMNameWrapper = &XGetWMName;
static typeof(XSetErrorHandler) *XSetErrorHandlerWrapper = &XSetErrorHandler;
static typeof(XFree) *XFreeWrapper = &XFree;

#endif
#endif

/* Holds state of the unit. */
static struct
{
	int initialized; /* Whether state has already been initialized. */
	TitleKind kind;  /* Type of title output. */

#ifndef _WIN32
	char *tsl; /* Sequence that starts title. */
	char *fsl; /* Sequence that ends title. */
#endif

	/* Original title or an empty string if failed to determine. */
#ifndef _WIN32
	char title[512];
#else
	wchar_t title[512];
#endif
}
title_state;

int
term_title_restorable(void)
{
	ensure_initialized();

	return (title_state.title[0] != '\0');
}

void
term_title_update(const char title_part[])
{
	ensure_initialized();

	if(title_state.kind == TK_ABSENT)
	{
		return;
	}

	if(title_part == NULL)
	{
		restore_term_title();
	}
	else if(cfg.set_title)
	{
		char *escaped = escape_unreadable(title_part);
		set_terminal_title(escaped);
		free(escaped);
	}
}

/* Makes sure that global title_state structure is initialized with valid
 * values. */
static void
ensure_initialized(void)
{
	if(title_state.initialized)
	{
		return;
	}

	title_state.kind = query_title_kind();
	if(title_state.kind == TK_ABSENT)
	{
		title_state.kind = title_kind_for_termenv(env_get_def("TERM", ""));
		apply_term_guess(title_state.kind);
	}

	if(title_state.kind == TK_REGULAR)
	{
		save_term_title();
	}
	title_state.initialized = 1;
}

/* Determines kind of title in a reliable, but not always available way.
 * Returns the kind. */
static TitleKind
query_title_kind(void)
{
#ifndef _WIN32
	int need_cleanup = 0;
	if(cur_term == NULL && !vifm_testing())
	{
		(void)setupterm((char *)env_get("TERM"), 1, (int *)0);
		need_cleanup = 1;
	}

	int success = 1;

	const char *tsl = tigetstr("tsl");
	if(tsl == NULL || tsl == (char *)-1)
	{
		success = 0;
	}
	else
	{
		update_string(&title_state.tsl, tsl);
	}

	const char *fsl = tigetstr("fsl");
	if(tsl == NULL || tsl == (char *)-1)
	{
		success = 0;
	}
	else
	{
		update_string(&title_state.fsl, fsl);
	}

	if(need_cleanup)
	{
		del_curterm(cur_term);
	}

	if(!success)
	{
		update_string(&title_state.tsl, NULL);
		update_string(&title_state.fsl, NULL);
		return TK_ABSENT;
	}
#endif

	return TK_REGULAR;
}

/* Guesses how we can alter terminal emulator title based on the value of $TERM.
 * Returns kind of terminal that was found. */
TSTATIC TitleKind
title_kind_for_termenv(const char term[])
{
#ifdef _WIN32
	return TK_REGULAR;
#else
	if(strcmp(term, "xterm") == 0 || starts_with_lit(term, "xterm-") ||
			strcmp(term, "rxvt") == 0 || starts_with_lit(term, "rxvt-") ||
			strcmp(term, "aterm") == 0 || strcmp(term, "Eterm") == 0 ||
			strcmp(term, "foot") == 0 || starts_with_lit(term, "foot-"))
	{
		return TK_REGULAR;
	}

	if(strcmp(term, "screen") == 0 || starts_with_lit(term, "screen-"))
	{
		return TK_SCREEN;
	}

	return TK_ABSENT;
#endif
}

/* Adjusts title_state according to guessed terminal type. */
static void
apply_term_guess(TitleKind kind)
{
#ifndef _WIN32
	switch(kind)
	{
		case TK_ABSENT:
			break;
		case TK_REGULAR:
			update_string(&title_state.tsl, "\033]2;");
			update_string(&title_state.fsl, "\007");
			break;
		case TK_SCREEN:
			update_string(&title_state.tsl, "\033k");
			update_string(&title_state.fsl, "\033\134");
			break;
	}
#endif
}

/* Stores current terminal title into title_state.title. */
static void
save_term_title(void)
{
#ifdef _WIN32
	GetConsoleTitleW(title_state.title, ARRAY_LEN(title_state.title));
#else
#ifdef HAVE_X11
	Display *x11_display = 0;
	Window x11_window = 0;

#ifdef DYN_X11
	try_dynload_xlib();
#endif

	/* Use X to determine current window title. */
	if(get_x11_disp_and_win(&x11_display, &x11_window))
		get_x11_window_title(x11_display, x11_window, title_state.title,
				sizeof(title_state.title));
#endif
#endif
}

/* Restores terminal title from title_state.title. */
static void
restore_term_title(void)
{
#ifdef _WIN32
	if(title_state.title[0] != L'\0')
		SetConsoleTitleW(title_state.title);
#else
	if(title_state.title[0] != '\0')
	{
		/* Apparently tsl can take column in some cases. */
		putp(tgoto(title_state.tsl, 0, 0));
		putp(title_state.title);
		putp(title_state.fsl);
		fflush(stdout);
	}

#if defined(HAVE_X11) && defined(DYN_X11)
	unload_xlib();
#endif
#endif
}

#if !defined(_WIN32) && defined(HAVE_X11)
/* Loads X specific variables. */
static int
get_x11_disp_and_win(Display **disp, Window *win)
{
	const char *winid;

	if(*win == 0 && (winid = env_get("WINDOWID")) != NULL)
		*win = (Window)atol(winid);

	if(*win != 0 && *disp == NULL)
		*disp = XOpenDisplayWrapper(NULL);
	if(*win == 0 || *disp == NULL)
		return 0;

	return 1;
}

/* Gets terminal title using X. */
static void
get_x11_window_title(Display *disp, Window win, char *buf, size_t buf_len)
{
	int (*old_handler)(Display *, XErrorEvent *);
	XTextProperty text_prop;

	old_handler = XSetErrorHandlerWrapper(x_error_check);
	if(!XGetWMNameWrapper(disp, win, &text_prop))
	{
		(void)XSetErrorHandlerWrapper(old_handler);
		return;
	}

	(void)XSetErrorHandlerWrapper(old_handler);
	copy_str(buf, buf_len, (char *)text_prop.value);
	XFreeWrapper(text_prop.value);
}

/* Callback function for reporting X errors, should return 0 on success. */
static int
x_error_check(Display *dpy, XErrorEvent *error_event)
{
	return 0;
}
#endif

/* Does all the real work on setting terminal title. */
static void
set_terminal_title(const char path[])
{
#ifdef _WIN32
	wchar_t *utf16;
	char title[2048];
	snprintf(title, sizeof(title), "%s - VIFM", path);

	utf16 = utf8_to_utf16(title);
	SetConsoleTitleW(utf16);
	free(utf16);
#else
	char *const fmt = (title_state.kind == TK_REGULAR)
	                ? "\033]2;%.*s - VIFM\007"
	                : "\033k%.*s - VIFM\033\134";
	char *const title = format_str(fmt, MAX_TITLE_MSG_LEN, path);

	putp(title);
	fflush(stdout);

	free(title);
#endif
}

#if !defined(_WIN32) && defined(HAVE_X11) && defined(DYN_X11)

/* Tries to load xlib dynamically, falls back to stubs that do nothing in case
 * of failure. */
static void
try_dynload_xlib(void)
{
	const int xlib_dynloaded_successfully = dynload_xlib();
	if(!xlib_dynloaded_successfully)
	{
		set_xlib_stubs();
	}
}

/* Loads libX11 library.  Returns non-zero when library is loaded
 * successfully. */
static int
dynload_xlib(void)
{
	xlib_handle = dlopen("libX11.so.6", RTLD_NOW);
	if(xlib_handle == NULL)
	{
		return 0;
	}
	XOpenDisplayWrapper = dlsym(xlib_handle, "XOpenDisplay");
	XGetWMNameWrapper = dlsym(xlib_handle, "XGetWMName");
	XSetErrorHandlerWrapper = dlsym(xlib_handle, "XSetErrorHandler");
	XFreeWrapper = dlsym(xlib_handle, "XFree");
	return XOpenDisplayWrapper != NULL && XGetWMNameWrapper != NULL &&
		XSetErrorHandlerWrapper != NULL && XFreeWrapper != NULL;
}

/* Sets xlib function pointers to stubs. */
static void
set_xlib_stubs(void)
{
	XOpenDisplayWrapper = &XOpenDisplayStub;
	XGetWMNameWrapper = &XGetWMNameStub;
	XSetErrorHandlerWrapper = &XSetErrorHandlerStub;
	XFreeWrapper = &XFreeStub;
}

/* Unloads previously loaded libX11 library. */
static void
unload_xlib(void)
{
	if(xlib_handle != NULL)
	{
		(void)dlclose(xlib_handle);
		xlib_handle = NULL;
	}
}

/* XOpenDisplay() function stub, which does nothing. */
static Display *
XOpenDisplayStub(_Xconst char name[])
{
	return NULL;
}

/* XGetWMName() function stub, which does nothing. */
static Status
XGetWMNameStub(Display *display, Window w, XTextProperty* text_prop_return)
{
	return 0;
}

/* XSetErrorHandler() function stub, which does nothing. */
static XErrorHandler
XSetErrorHandlerStub(XErrorHandler handler)
{
	return NULL;
}

/* XFree() function stub, which does nothing. */
static int
XFreeStub(void* data)
{
	return 0;
}

#endif

/* vim: set tabstop=2 softtabstop=2 shiftwidth=2 noexpandtab cinoptions-=(0 : */
/* vim: set cinoptions+=t0 filetype=c : */
