/* Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#include <fcntl.h>
#include <pthread.h>
#include <string.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <syslog.h>

#include "cras_alsa_card.h"
#include "cras_config.h"
#include "cras_device_blacklist.h"
#include "cras_observer.h"
#include "cras_shm.h"
#include "cras_system_state.h"
#include "cras_tm.h"
#include "cras_types.h"
#include "cras_util.h"
#include "utlist.h"

struct card_list {
	struct cras_alsa_card *card;
	struct card_list *prev, *next;
};

/* The system state.
 * Members:
 *    exp_state - The exported system state shared with clients.
 *    shm_name - Name of posix shm region for exported state.
 *    shm_fd - fd for shm area of system_state struct.
 *    shm_fd_ro - fd for shm area of system_state struct, opened read-only.
 *    shm_size - Size of the shm area.
 *    device_config_dir - Directory of device configs where volume curves live.
 *    internal_ucm_suffix - The suffix to append to internal card name to
 *        control which ucm config file to load.
 *    device_blacklist - Blacklist of device the server will ignore.
 *    cards - A list of active sound cards in the system.
 *    update_lock - Protects the update_count, as audio threads can update the
 *      stream count.
 *    tm - The system-wide timer manager.
 */
static struct {
	struct cras_server_state *exp_state;
	char shm_name[NAME_MAX];
	int shm_fd;
	int shm_fd_ro;
	size_t shm_size;
	const char *device_config_dir;
	const char *internal_ucm_suffix;
	struct cras_device_blacklist *device_blacklist;
	struct card_list *cards;
	pthread_mutex_t update_lock;
	struct cras_tm *tm;
	/* Select loop callback registration. */
	int (*fd_add)(int fd, void (*cb)(void *data),
		      void *cb_data, void *select_data);
	void (*fd_rm)(int fd, void *select_data);
	void *select_data;
} state;

/*
 * Exported Interface.
 */

void cras_system_state_init(const char *device_config_dir)
{
	struct cras_server_state *exp_state;
	int rc;

	state.shm_size = sizeof(*exp_state);

	snprintf(state.shm_name, sizeof(state.shm_name), "/cras-%d", getpid());
	state.shm_fd = cras_shm_open_rw(state.shm_name, state.shm_size);
	if (state.shm_fd < 0)
		exit(state.shm_fd);

	/* mmap shm. */
	exp_state = mmap(NULL, state.shm_size,
			 PROT_READ | PROT_WRITE, MAP_SHARED,
			 state.shm_fd, 0);
	if (exp_state == (struct cras_server_state *)-1)
		exit(-ENOMEM);

	/* Open a read-only copy to dup and pass to clients. */
	state.shm_fd_ro = cras_shm_reopen_ro(state.shm_name, state.shm_fd);
	if (state.shm_fd_ro < 0)
		exit(state.shm_fd_ro);

	/* Initial system state. */
	exp_state->state_version = CRAS_SERVER_STATE_VERSION;
	exp_state->volume = CRAS_MAX_SYSTEM_VOLUME;
	exp_state->mute = 0;
	exp_state->mute_locked = 0;
	exp_state->suspended = 0;
	exp_state->capture_gain = DEFAULT_CAPTURE_GAIN;
	exp_state->capture_gain_target = DEFAULT_CAPTURE_GAIN;
	exp_state->capture_mute = 0;
	exp_state->capture_mute_locked = 0;
	exp_state->min_volume_dBFS = DEFAULT_MIN_VOLUME_DBFS;
	exp_state->max_volume_dBFS = DEFAULT_MAX_VOLUME_DBFS;
	exp_state->min_capture_gain = DEFAULT_MIN_CAPTURE_GAIN;
	exp_state->max_capture_gain = DEFAULT_MAX_CAPTURE_GAIN;
	exp_state->num_streams_attached = 0;

	if ((rc = pthread_mutex_init(&state.update_lock, 0) != 0)) {
		syslog(LOG_ERR, "Fatal: system state mutex init");
		exit(rc);
	}

	state.exp_state = exp_state;

	/* Directory for volume curve configs.
	 * Note that device_config_dir does not affect device blacklist.
	 * Device blacklist is common to all boards so we do not need
	 * to change device blacklist at run time. */
	state.device_config_dir = device_config_dir;
	state.internal_ucm_suffix = NULL;

	state.tm = cras_tm_init();
	if (!state.tm) {
		syslog(LOG_ERR, "Fatal: system state timer init");
		exit(-ENOMEM);
	}

	/* Read config file for blacklisted devices. */
	state.device_blacklist =
		cras_device_blacklist_create(CRAS_CONFIG_FILE_DIR);
}

void cras_system_state_set_internal_ucm_suffix(const char *internal_ucm_suffix)
{
	state.internal_ucm_suffix = internal_ucm_suffix;
}

void cras_system_state_deinit()
{
	/* Free any resources used.  This prevents unit tests from leaking. */

	cras_device_blacklist_destroy(state.device_blacklist);

	cras_tm_deinit(state.tm);

	if (state.exp_state) {
		munmap(state.exp_state, state.shm_size);
		cras_shm_close_unlink(state.shm_name, state.shm_fd);
		if (state.shm_fd_ro != state.shm_fd)
			close(state.shm_fd_ro);
	}

	pthread_mutex_destroy(&state.update_lock);
}

void cras_system_set_volume(size_t volume)
{
	if (volume > CRAS_MAX_SYSTEM_VOLUME)
		syslog(LOG_DEBUG, "system volume set out of range %zu", volume);

	state.exp_state->volume = MIN(volume, CRAS_MAX_SYSTEM_VOLUME);
	cras_observer_notify_output_volume(state.exp_state->volume);
}

size_t cras_system_get_volume()
{
	return state.exp_state->volume;
}

void cras_system_set_capture_gain(long gain)
{
	/* Adjust targeted gain to be in supported range. */
	state.exp_state->capture_gain_target = gain;
	gain = MAX(gain, state.exp_state->min_capture_gain);
	gain = MIN(gain, state.exp_state->max_capture_gain);
	state.exp_state->capture_gain = gain;
	cras_observer_notify_capture_gain(state.exp_state->capture_gain);
}

long cras_system_get_capture_gain()
{
	return state.exp_state->capture_gain;
}

void cras_system_notify_mute(void)
{
	cras_observer_notify_output_mute(state.exp_state->mute,
					 state.exp_state->user_mute,
					 state.exp_state->mute_locked);
}

void cras_system_set_user_mute(int mute)
{
	if (state.exp_state->user_mute == !!mute)
		return;

	state.exp_state->user_mute = !!mute;
	cras_system_notify_mute();
}

void cras_system_set_mute(int mute)
{
	if (state.exp_state->mute_locked)
		return;

	if (state.exp_state->mute == !!mute)
		return;

	state.exp_state->mute = !!mute;
	cras_system_notify_mute();
}

void cras_system_set_mute_locked(int locked)
{
	if (state.exp_state->mute_locked == !!locked)
		return;

	state.exp_state->mute_locked = !!locked;
	cras_system_notify_mute();
}

int cras_system_get_mute()
{
	return state.exp_state->mute || state.exp_state->user_mute;
}

int cras_system_get_user_mute()
{
	return state.exp_state->user_mute;
}

int cras_system_get_system_mute()
{
	return state.exp_state->mute;
}

int cras_system_get_mute_locked()
{
	return state.exp_state->mute_locked;
}

void cras_system_notify_capture_mute(void)
{
	cras_observer_notify_capture_mute(state.exp_state->capture_mute,
					  state.exp_state->capture_mute_locked);
}

void cras_system_set_capture_mute(int mute)
{
	if (state.exp_state->capture_mute_locked)
		return;

	state.exp_state->capture_mute = !!mute;
	cras_system_notify_capture_mute();
}

void cras_system_set_capture_mute_locked(int locked)
{
	state.exp_state->capture_mute_locked = !!locked;
	cras_system_notify_capture_mute();
}

int cras_system_get_capture_mute()
{
	return state.exp_state->capture_mute;
}

int cras_system_get_capture_mute_locked()
{
	return state.exp_state->capture_mute_locked;
}

int cras_system_get_suspended()
{
	return state.exp_state->suspended;
}

void cras_system_set_suspended(int suspended)
{
	state.exp_state->suspended = suspended;
	cras_observer_notify_suspend_changed(suspended);
}

void cras_system_set_volume_limits(long min, long max)
{
	state.exp_state->min_volume_dBFS = min;
	state.exp_state->max_volume_dBFS = max;
}

long cras_system_get_min_volume()
{
	return state.exp_state->min_volume_dBFS;
}

long cras_system_get_max_volume()
{
	return state.exp_state->max_volume_dBFS;
}

void cras_system_set_capture_gain_limits(long min, long max)
{
	state.exp_state->min_capture_gain = MAX(min, DEFAULT_MIN_CAPTURE_GAIN);
	state.exp_state->max_capture_gain = max;
	/* Re-apply target gain subjected to the new supported range. */
	cras_system_set_capture_gain(state.exp_state->capture_gain_target);
}

long cras_system_get_min_capture_gain()
{
	return state.exp_state->min_capture_gain;
}

long cras_system_get_max_capture_gain()
{
	return state.exp_state->max_capture_gain;
}

int cras_system_add_alsa_card(struct cras_alsa_card_info *alsa_card_info)
{
	struct card_list *card;
	struct cras_alsa_card *alsa_card;
	unsigned card_index;

	if (alsa_card_info == NULL)
		return -EINVAL;

	card_index = alsa_card_info->card_index;

	DL_FOREACH(state.cards, card) {
		if (card_index == cras_alsa_card_get_index(card->card))
			return -EINVAL;
	}
	alsa_card = cras_alsa_card_create(
			alsa_card_info,
			state.device_config_dir,
			state.device_blacklist,
			(alsa_card_info->card_type == ALSA_CARD_TYPE_INTERNAL)
				? state.internal_ucm_suffix
				: NULL);
	if (alsa_card == NULL)
		return -ENOMEM;
	card = calloc(1, sizeof(*card));
	if (card == NULL)
		return -ENOMEM;
	card->card = alsa_card;
	DL_APPEND(state.cards, card);
	return 0;
}

int cras_system_remove_alsa_card(size_t alsa_card_index)
{
	struct card_list *card;

	DL_FOREACH(state.cards, card) {
		if (alsa_card_index == cras_alsa_card_get_index(card->card))
			break;
	}
	if (card == NULL)
		return -EINVAL;
	DL_DELETE(state.cards, card);
	cras_alsa_card_destroy(card->card);
	free(card);
	return 0;
}

int cras_system_alsa_card_exists(unsigned alsa_card_index)
{
	struct card_list *card;

	DL_FOREACH(state.cards, card)
		if (alsa_card_index == cras_alsa_card_get_index(card->card))
			return 1;
	return 0;
}

int cras_system_set_select_handler(int (*add)(int fd,
					      void (*callback)(void *data),
					      void *callback_data,
					      void *select_data),
				   void (*rm)(int fd, void *select_data),
				   void *select_data)
{
	if (state.fd_add != NULL || state.fd_rm != NULL)
		return -EEXIST;
	state.fd_add = add;
	state.fd_rm = rm;
	state.select_data = select_data;
	return 0;
}

int cras_system_add_select_fd(int fd,
			      void (*callback)(void *data),
			      void *callback_data)
{
	if (state.fd_add == NULL)
		return -EINVAL;
	return state.fd_add(fd, callback, callback_data,
			    state.select_data);
}

void cras_system_rm_select_fd(int fd)
{
	if (state.fd_rm != NULL)
		state.fd_rm(fd, state.select_data);
}

void cras_system_state_stream_added(enum CRAS_STREAM_DIRECTION direction)
{
	struct cras_server_state *s;

	s = cras_system_state_update_begin();
	if (!s)
		return;

	s->num_active_streams[direction]++;
	s->num_streams_attached++;

	cras_system_state_update_complete();
	cras_observer_notify_num_active_streams(
		direction, s->num_active_streams[direction]);
}

void cras_system_state_stream_removed(enum CRAS_STREAM_DIRECTION direction)
{
	struct cras_server_state *s;
	unsigned i, sum;


	s = cras_system_state_update_begin();
	if (!s)
		return;

	sum = 0;
	for (i=0; i < CRAS_NUM_DIRECTIONS; i++)
		sum += s->num_active_streams[i];

	/* Set the last active time when removing the final stream. */
	if (sum == 1)
		cras_clock_gettime(CLOCK_MONOTONIC_RAW,
				   &s->last_active_stream_time);
	s->num_active_streams[direction]--;

	cras_system_state_update_complete();
	cras_observer_notify_num_active_streams(
		direction, s->num_active_streams[direction]);
}

unsigned cras_system_state_get_active_streams()
{
	unsigned i, sum;
	sum = 0;
	for (i=0; i < CRAS_NUM_DIRECTIONS; i++)
		sum += state.exp_state->num_active_streams[i];
	return sum;
}

unsigned cras_system_state_get_active_streams_by_direction(
	enum CRAS_STREAM_DIRECTION direction)
{
	return state.exp_state->num_active_streams[direction];
}

void cras_system_state_get_last_stream_active_time(struct cras_timespec *ts)
{
	*ts = state.exp_state->last_active_stream_time;
}

int cras_system_state_get_output_devs(const struct cras_iodev_info **devs)
{
	*devs = state.exp_state->output_devs;
	return state.exp_state->num_output_devs;
}

int cras_system_state_get_input_devs(const struct cras_iodev_info **devs)
{
	*devs = state.exp_state->input_devs;
	return state.exp_state->num_input_devs;
}

int cras_system_state_get_output_nodes(const struct cras_ionode_info **nodes)
{
	*nodes = state.exp_state->output_nodes;
	return state.exp_state->num_output_nodes;
}

int cras_system_state_get_input_nodes(const struct cras_ionode_info **nodes)
{
	*nodes = state.exp_state->input_nodes;
	return state.exp_state->num_input_nodes;
}

struct cras_server_state *cras_system_state_update_begin()
{
	if (pthread_mutex_lock(&state.update_lock)) {
		syslog(LOG_ERR, "Failed to lock stream mutex");
		return NULL;
	}

	__sync_fetch_and_add(&state.exp_state->update_count, 1);
	return state.exp_state;
}

void cras_system_state_update_complete()
{
	__sync_fetch_and_add(&state.exp_state->update_count, 1);
	pthread_mutex_unlock(&state.update_lock);
}

struct cras_server_state *cras_system_state_get_no_lock()
{
	return state.exp_state;
}

key_t cras_sys_state_shm_fd()
{
	return state.shm_fd_ro;
}

struct cras_tm *cras_system_state_get_tm()
{
	return state.tm;
}