#!/usr/bin/perl

# Copyright (c) 2009 Landry Breuil <landry@openbsd.org>
#
# Permission to use, copy, modify, and distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

use strict;
use warnings;

my $m = OpenBSD::CMixer->new();
$m->run();

package OpenBSD::CMixer;
use Curses;
use Curses::UI;
use Curses::UI::Common;

sub new {
	my($class) = @_;
	my $self = bless {}, $class;
	$self->{name} ="CMixer v0.2";
	return $self;
}

sub read_mixerctl {
	my $self = shift;
	foreach (`mixerctl 2>&1`) {
		#mixerctl: /dev/mixer: Device not configured
		die "No mixer device found" if (/^mixerctl: \/dev\/mixer.: Device not configured$/);
		#outputs.master=255,255 volume
		if (/^(outputs|inputs|record)\.([^\.]*)=(\d+),(\d+)\s+/) {
			$self->{mixer}{$1}{$2}{volume}{left} = $3;
			$self->{mixer}{$1}{$2}{volume}{right} = $4;
		# outputs.mono=255 volume
		} elsif (/^(outputs|inputs|record)\.([^\.]*)=(\d+)\s+/) {
			$self->{mixer}{$1}{$2}{volume}{center} = $3;
		# outputs.master.mute=off  [ off on ]
		} elsif (/^(outputs|inputs|record)\.(.*)\.mute=(on|off)\s+/) {
			$self->{mixer}{$1}{$2}{mute} = $3;
		}
		#todo: record.source=mic  [ mic cd video aux line mixerout mixeroutmono phone ]
		#inputs.mix_source=mic,beep,hp  { mic mic2 beep hp }
	}
	foreach (`aucatctl 2>&1`) {
		die "Failed to connect to sndiod" if (/couldn't open MIDI device$/);
		#mplayer0=127
		#master=127
		if (/^(.*)=(\d+)$/) {
			$self->{mixer}{sndio}{$1}{volume}{center} = 2 * $2;
		}
	}
	die "No outputs found" unless (exists $self->{mixer}{outputs});
}

sub update_view {
	my $self = shift;
	foreach my $dir (keys %{$self->{mixer}}) {
		foreach my $wid (keys %{$self->{mixer}{$dir}}) {
			my $v = $self->{mixer}{$dir}{$wid}{volume}{center} // $self->{mixer}{$dir}{$wid}{volume}{right};
			$self->{wid}{"$dir.$wid.progress"}->pos($v);
			if ((exists $self->{mixer}{$dir}{$wid}{mute} && $self->{mixer}{$dir}{$wid}{mute} eq "on") ||
				(!exists $self->{mixer}{$dir}{$wid}{mute} && $v == 0))
				{ $self->{wid}{"$dir.$wid.mute"}->uncheck; }
			else
				{ $self->{wid}{"$dir.$wid.mute"}->check; }
		}
	}
}

sub update_vol {
	my ($self, $off, $dir, $wid) = @_;
	my $v = $self->{mixer}{$dir}{$wid}{volume}{center} // $self->{mixer}{$dir}{$wid}{volume}{right};
	# save previous value, needed when muting a widget without mute control
	$self->{mixer}{$dir}{$wid}{volume}{previous} = $v;
	$v += $off;
	$self->{wid}{"$dir.$wid.progress"}->pos($v);
	if ($dir eq "sndio") {
		$v /= 2;
		$v = 127 if ($v > 127);
		$v = 0 if ($v < 0);
		qx/aucatctl $wid=$v/;
		my $new = qx/aucatctl $wid/;
		# update vol with new value
		$self->{mixer}{$dir}{$wid}{volume}{center} = 2 * $1 if ($new =~ /^$wid=(\d+)/);
	} else {
		qx/mixerctl -q $dir.$wid=$v/;
		# update vol with new value
		my $new = qx/mixerctl $dir.$wid/;
		if ($new =~ /^$dir.$wid=(\d+),(\d+)\s+/) {
			$self->{mixer}{$dir}{$wid}{volume}{left} = $1;
			$self->{mixer}{$dir}{$wid}{volume}{right} = $2;
		} elsif ($new =~ /^$dir.$wid=(\d+)\s+/) {
			$self->{mixer}{$dir}{$wid}{volume}{center} = $1;
		}
	}
}

sub toggle_mute {
	my ($self, $dir, $wid) = @_;
	$self->{wid}{"$dir.$wid.mute"}->toggle;
	if (exists $self->{mixer}{$dir}{$wid}{mute}) {
		qx/mixerctl -tq $dir.$wid.mute/;
	# handle mute toggle when no mute ctrl is present
	} else {
		my $v = $self->{mixer}{$dir}{$wid}{volume}{center} // $self->{mixer}{$dir}{$wid}{volume}{right};
		my $oldv = $self->{mixer}{$dir}{$wid}{volume}{previous} // 255;
		# if vol is not 0 & now unchecked, and update_vol(0)
		if ($v != 0 && !$self->{wid}{"$dir.$wid.mute"}->get) {
			$self->update_vol(-$v,$dir,$wid);
		} elsif (defined $self->{mixer}{$dir}{$wid}{volume}{previous}){
		# else update_vol(prev value)
			$self->update_vol($self->{mixer}{$dir}{$wid}{volume}{previous},$dir,$wid);
		}
	}
}

sub loose_focus {
	my ($self, $dir, $off) = @_;
	$self->{"$dir.checkbox_current"} += $off;
	$self->{"$dir.checkbox_current"} %= @{$self->{wid}{"$dir.checkbox_list"}};
	$self->{wid}{"$dir.checkbox_list"}[$self->{"$dir.checkbox_current"}]->focus();
}

sub create_view {
	my $self = shift;
	$self->{wid}{cui} = new Curses::UI(
		-clear_on_exit => 1,
		-color_support => 1);
	$self->{wid}{main_win} = $self->{wid}{cui}->add(
		'main_win', 'Window',
		-ipad => 1,
		-title => $self->{name},
		-titlereverse => 0,
		-border => 1);
	my $h = $self->{wid}{main_win}->height;
	$self->{wid}{notebook} = $self->{wid}{main_win}->add(
		'notebook', 'Notebook',
		-y => 2); #40); # $self->{wid}{main_win}->height - 3);
	foreach my $dir (sort keys %{$self->{mixer}}) {
		my $y = 0;
		# create container for this dir list
		$self->{wid}{"$dir.page"} = $self->{wid}{notebook}->add_page(
			"$dir",
			-on_activate => sub {$self->loose_focus($dir, 0)});

		foreach my $wid (sort keys %{$self->{mixer}{$dir}}) {
			$self->{wid}{"$dir.$wid.label"} = $self->{wid}{"$dir.page"}->add(
				"$dir.$wid.label", "Label",
				-y => $y,
				-text => "$wid");
			$self->{wid}{"$dir.$wid.mute"} = $self->{wid}{"$dir.page"}->add(
				"$dir.$wid.mute", "Checkbox",
				-y => $y, -x => 8);
			$self->{wid}{"$dir.$wid.mute"}->set_binding( sub {$self->update_vol(16, $dir,$wid) }, ("l", KEY_RIGHT()));
			$self->{wid}{"$dir.$wid.mute"}->set_binding( sub {$self->update_vol(-16, $dir,$wid) }, ("h", KEY_LEFT()));
			$self->{wid}{"$dir.$wid.mute"}->clear_binding('loose-focus');
			$self->{wid}{"$dir.$wid.mute"}->set_binding( sub {$self->loose_focus($dir,1) }, ("j", KEY_DOWN(), CUI_TAB()));
			$self->{wid}{"$dir.$wid.mute"}->set_binding( sub {$self->loose_focus($dir,-1) }, ("k", KEY_UP()));
			$self->{wid}{"$dir.$wid.mute"}->clear_binding('toggle');
			$self->{wid}{"$dir.$wid.mute"}->set_binding( sub {$self->toggle_mute($dir,$wid) }, CUI_SPACE());
			push @{$self->{wid}{"$dir.checkbox_list"}}, $self->{wid}{"$dir.$wid.mute"};
			$self->{"$dir.checkbox_current"} = 0;

			$self->{wid}{"$dir.$wid.progress"} = $self->{wid}{"$dir.page"}->add(
				"$dir.$wid.progress", 'Progressbar', -border=>0, -sbborder =>1,
				-max => 255, -showvalue => 1, -x => 12,  -y => $y);
			$y += 2;
		}
	}
	$self->{wid}{notebook}->activate_page("outputs") if (exists $self->{mixer}{outputs});
	$self->{wid}{"help_label"} = $self->{wid}{main_win}->add(
		"help_label", "Label",
		-x => 2, -y => 0,
		-bold => 1,
		-text => "[up/down] prev/next [space] toggle mute [left/right] down/up volume [pgup/pgdown] prev/next tab [q] quit");
	$self->{wid}{cui}->set_binding( sub { exit}, "q");
}

sub run {
	my $self = shift;
	$self->read_mixerctl();
	$self->create_view();
	$self->update_view();
	# create timer & run C::UI
	$self->{wid}{cui}->set_timer('timer', sub { $self->read_mixerctl; $self->update_view }, 1);
	$self->{wid}{cui}->mainloop();
}

