/* 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 <pthread.h>
#include <sys/param.h>
#include <syslog.h>

#include "cras_audio_area.h"
#include "cras_config.h"
#include "cras_iodev.h"
#include "cras_iodev_list.h"
#include "cras_rstream.h"
#include "cras_types.h"
#include "utlist.h"

#define EMPTY_BUFFER_SIZE (16 * 1024)
#define EMPTY_FRAME_SIZE 4
#define EMPTY_FRAMES (EMPTY_BUFFER_SIZE / EMPTY_FRAME_SIZE)

static size_t empty_supported_rates[] = {
	44100, 48000, 0
};

static size_t empty_supported_channel_counts[] = {
	1, 2, 0
};

static snd_pcm_format_t empty_supported_formats[] = {
	SND_PCM_FORMAT_S16_LE,
	SND_PCM_FORMAT_S24_LE,
	SND_PCM_FORMAT_S32_LE,
	SND_PCM_FORMAT_S24_3LE,
	0
};

struct empty_iodev {
	struct cras_iodev base;
	int open;
	uint8_t *audio_buffer;
	unsigned int buffer_level;
	struct timespec last_buffer_access;
};

/* Current level of the audio buffer.  This is made up based on what has been
 * read/written and how long it has been since then.  Simulates audio hardware
 * running at the given sample rate.
 */
static unsigned int current_level(const struct cras_iodev *iodev)
{
	struct empty_iodev *empty_iodev = (struct empty_iodev *)iodev;
	unsigned int frames, frames_since_last;

	frames = empty_iodev->buffer_level;
	frames_since_last = cras_frames_since_time(
			&empty_iodev->last_buffer_access,
			iodev->format->frame_rate);

	if (iodev->direction == CRAS_STREAM_INPUT)
		return (frames + frames_since_last) % EMPTY_FRAMES;

	/* output */
	if (frames <= frames_since_last)
		return 0;
	return frames - frames_since_last;
}

/*
 * iodev callbacks.
 */

static int frames_queued(const struct cras_iodev *iodev,
			 struct timespec *tstamp)
{
	clock_gettime(CLOCK_MONOTONIC_RAW, tstamp);
	return current_level(iodev);
}

static int delay_frames(const struct cras_iodev *iodev)
{
	return 0;
}

static int close_dev(struct cras_iodev *iodev)
{
	struct empty_iodev *empty_iodev = (struct empty_iodev *)iodev;

	empty_iodev->open = 0;
	free(empty_iodev->audio_buffer);
	empty_iodev->audio_buffer = NULL;
	cras_iodev_free_audio_area(iodev);
	return 0;
}

static int open_dev(struct cras_iodev *iodev)
{
	struct empty_iodev *empty_iodev = (struct empty_iodev *)iodev;

	if (iodev->format == NULL)
		return -EINVAL;

	cras_iodev_init_audio_area(iodev, iodev->format->num_channels);
	empty_iodev->open = 1;
	empty_iodev->audio_buffer = calloc(1, EMPTY_BUFFER_SIZE);
	empty_iodev->buffer_level = 0;

	clock_gettime(CLOCK_MONOTONIC_RAW, &empty_iodev->last_buffer_access);

	return 0;
}

static int get_buffer(struct cras_iodev *iodev,
		      struct cras_audio_area **area,
		      unsigned *frames)
{
	struct empty_iodev *empty_iodev = (struct empty_iodev *)iodev;
	unsigned int avail, current;

	if (iodev->direction == CRAS_STREAM_OUTPUT) {
		avail = EMPTY_FRAMES - current_level(iodev);
		*frames = MIN(*frames, avail);
	} else {
		current = current_level(iodev);
		*frames = MIN(*frames, current);
	}

	iodev->area->frames = *frames;
	cras_audio_area_config_buf_pointers(iodev->area, iodev->format,
			empty_iodev->audio_buffer);
	*area = iodev->area;
	return 0;
}

static int put_buffer(struct cras_iodev *iodev, unsigned frames)
{
	struct empty_iodev *empty_iodev = (struct empty_iodev *)iodev;

	empty_iodev->buffer_level = current_level(iodev);

	clock_gettime(CLOCK_MONOTONIC_RAW, &empty_iodev->last_buffer_access);

	if (iodev->direction == CRAS_STREAM_OUTPUT) {
		empty_iodev->buffer_level += frames;
		empty_iodev->buffer_level %= EMPTY_FRAMES;
	} else {
		/* Input */
		if (empty_iodev->buffer_level > frames)
			empty_iodev->buffer_level -= frames;
		else
			empty_iodev->buffer_level = 0;
	}

	return 0;
}

static int flush_buffer(struct cras_iodev *iodev)
{
	struct empty_iodev *empty_iodev = (struct empty_iodev *)iodev;

	empty_iodev->buffer_level = current_level(iodev);
	if (iodev->direction == CRAS_STREAM_INPUT) {
		empty_iodev->buffer_level = 0;
		clock_gettime(CLOCK_MONOTONIC_RAW,
			      &empty_iodev->last_buffer_access);
	}
	return 0;
}

static void update_active_node(struct cras_iodev *iodev, unsigned node_idx,
			       unsigned dev_enabled)
{
}

/*
 * Exported Interface.
 */

struct cras_iodev *empty_iodev_create(enum CRAS_STREAM_DIRECTION direction)
{
	struct empty_iodev *empty_iodev;
	struct cras_iodev *iodev;
	struct cras_ionode *node;

	if (direction != CRAS_STREAM_INPUT && direction != CRAS_STREAM_OUTPUT)
		return NULL;

	empty_iodev = calloc(1, sizeof(*empty_iodev));
	if (empty_iodev == NULL)
		return NULL;
	iodev = &empty_iodev->base;
	iodev->direction = direction;

	iodev->supported_rates = empty_supported_rates;
	iodev->supported_channel_counts = empty_supported_channel_counts;
	iodev->supported_formats = empty_supported_formats;
	iodev->buffer_size = EMPTY_FRAMES;

	iodev->open_dev = open_dev;
	iodev->close_dev = close_dev;
	iodev->frames_queued = frames_queued;
	iodev->delay_frames = delay_frames;
	iodev->get_buffer = get_buffer;
	iodev->put_buffer = put_buffer;
	iodev->flush_buffer = flush_buffer;
	iodev->update_active_node = update_active_node;
	iodev->no_stream = cras_iodev_default_no_stream_playback;

	/* Create a dummy ionode */
	node = (struct cras_ionode *)calloc(1, sizeof(*node));
	node->dev = iodev;
	node->type = CRAS_NODE_TYPE_UNKNOWN;
	node->volume = 100;
	strcpy(node->name, "(default)");
	cras_iodev_add_node(iodev, node);
	cras_iodev_set_active_node(iodev, node);

	/* Finally add it to the appropriate iodev list. */
	if (direction == CRAS_STREAM_INPUT) {
		snprintf(iodev->info.name,
			 ARRAY_SIZE(iodev->info.name),
			 "Silent record device.");
		iodev->info.name[ARRAY_SIZE(iodev->info.name) - 1] = '\0';
		iodev->info.idx = SILENT_RECORD_DEVICE;
	} else {
		snprintf(iodev->info.name,
			 ARRAY_SIZE(iodev->info.name),
			 "Silent playback device.");
		iodev->info.name[ARRAY_SIZE(iodev->info.name) - 1] = '\0';
		iodev->info.idx = SILENT_PLAYBACK_DEVICE;
	}

	return iodev;
}

void empty_iodev_destroy(struct cras_iodev *iodev)
{
	struct empty_iodev *empty_iodev = (struct empty_iodev *)iodev;

	if (iodev->direction == CRAS_STREAM_INPUT)
		cras_iodev_list_rm_input(iodev);
	else
		cras_iodev_list_rm_output(iodev);
	free(iodev->active_node);
	cras_iodev_free_resources(iodev);
	free(empty_iodev);
}