package App::SysVRcConf::Multistate;

use strict;
use Curses;
use Curses::UI::Label;
use Curses::UI::Widget;
use Curses::UI::Common;

use vars qw( $VERSION @ISA );

@ISA = qw( Curses::UI::ContainerWidget );

=head1 NAME

App::SysVRcConf::UI::Multistate - Create and manipulate multistate widgets


=head1 VERSION

Version 0.1

=cut

$VERSION = '0.1';

=head1 CLASS HIERARCHY

 Curses::UI::Widget
    |
    +----Curses::UI::Container
            |
            +----App::SysVRcConf::Multistate


=head1 SYNOPSIS

    use Curses::UI;
    my $cui = new Curses::UI;
    my $win = $cui->add('window_id', 'Window');

    my $tristate = $win->add(
        'mytristate', 'App::SysVRcConf::Multistate',
        -label     => 'Tristate question',
        -states    => 'n', 'y', 'm',
    );

    $tristate->focus();
    my $definite = $tristate->get_state() eq 'y';


=head1 DESCRIPTION

App::SysVRcConf::Multistate provides a multistate widget.

This module is an implementation detail of the sysv-rc-conf application.

A multistate box is a control for a multistate value. It consists of a box
which can be in one of multiple states.  Following this is a text label which
described the value being controlled.

    [x] This control is in the 'x' state

=cut

my %routines = ( 'loose-focus'   => \&loose_focus,
		 'toggle'        => \&toggle,
		 'set-state'     => \&set_state,
		 'cycle'         => \&cycle,
		 'reverse'       => \&reverse,
		 'shortcut'      => \&shortcut,
		 'mouse-button1' => \&mouse_button1,
	       );

my %bindings = ( KEY_ENTER() => 'loose-focus',
		 CUI_TAB()   => 'loose-focus',
		 KEY_BTAB()  => 'loose-focus',
		 CUI_SPACE() => 'toggle',
		 '<'         => 'reverse',
		 '>'         => 'cycle',
	       );

=head1 STANDARD OPTIONS

    -x  -y   -width    -height
    -pad     -padleft  -padright  -padtop  -padbottom
    -ipad    -ipadleft -ipadright -ipadtop -ipadbottom
    -title   -titlefullwidth      -titlereverse
    -onfocus -onblur
    -parent

See L<Curses::UI::Widget|Curses::UI::Widget> for an explanation of
these.


=head1 WIDGET-SPECIFIC OPTIONS

=head2 -label

Sets the initial label for the checkbox widget to the passed string or
value.

=head2 -states

Takes an ordered list of states for the multistate control to hold.
The default is C<'0'>, C<'1'> and C<'?'>, forming a tristate control. For most
purposes the interface deals in zero-based state indexes, which would
be C<0>, C<1> and C<2> respectively for the default tristate configuration.

=head2 -stateind

Index of the initial state (defaulting to C<0>, the first listed state).

=head2 -togglemap

A hash defining pairs of states for the toggle operation to override
its default behaviour.

=head2 -shortcuts

A hash defining state indexes to be selected for given keypresses.

=head2 -onchange

Expects a coderef and sets it as a callback for the widget. When the
control's state is changed, the given code will be executed.

=cut

sub new () {
    my $class = shift;

    my %userargs = @_;
    keys_to_lowercase(\%userargs);

    my %args = ( -parent    => undef,    # the parent window
		 -width     => undef,    # the width of the checkbox
		 -x         => 0,        # the horizontal pos. rel. to parent
		 -y         => 0,        # the vertical pos. rel. to parent
		 -label     => '',       # the label text
		 -onchange  => undef,    # event handler
		 -bg        => -1,
		 -fg        => -1,
		 -states    => [ '0', '1', '?' ],
		 -stateind  => 0,
		 -togglemap => {},
		 -shortcuts => {},
		 %userargs,
		 -bindings  => {%bindings},
		 -routines  => {%routines},

		 -focus     => 0,        # value init
		 -nocursor  => 0,        # this widget uses a cursor
		 -nstates   => 0,
		 -statemap  => {},
	       );

    # Populate states index
    foreach (@{$args{-states}}) {
        $args{-statemap}->{$_} = $args{-nstates}++;
    }

    # Populate bindings for shortcuts
    foreach (keys %{$args{-shortcuts}}) {
        $args{-bindings}->{$_} = 'shortcut';
    }

    die "initial state index out of range" if $args{-stateind} >= $args{-nstates};

    # Define a previous state for toggle purposes
    $args{-prevind} = 1 - $args{-stateind};
    $args{-prevind} = 0 if $args{-prevind} < 0;

    # The windowscr height should be 1.
    $args{-height} = height_by_windowscrheight(1, %args);

    # No width given? Then make the width the same size as the label +
    # checkbox.
    $args{-width} = width_by_windowscrwidth(4 + length($args{-label}),%args)
        unless defined $args{-width};

    my $this = $class->SUPER::new( %args );

    # Create the label on the widget.
    $this->add( 'label', 'Label',
		-text        => $this->{-label},
		-x           => 4,
		-y           => 0,
		-intellidraw => 0,
		-bg          => $this->{-bg},
		-fg          => $this->{-fg},
	      ) if $this->{-label};

    $this->layout;

    $this->set_mouse_binding('mouse-button1', BUTTON1_CLICKED())
      if ($Curses::UI::ncurses_mouse);

    return $this;
}


=head1 STANDARD METHODS

    layout draw    intellidraw
    focus  onFocus onBlur

See L<Curses::UI::Widget|Curses::UI::Widget> for an explanation of
these.

=cut

sub event_onblur() {
    my $this = shift;
    $this->SUPER::event_onblur;

    $this->{-focus} = 0;
    $this->draw();

    return $this;
}

sub layout() {
    my $this = shift;

    my $label = $this->getobj('label');
    if (defined $label) {
        my $lh = $label->{-height};
        $lh = 1 if $lh <= 0;
        $this->{-height} = $lh;
    }

    $this->SUPER::layout or return;
    return $this;
}

sub reverse() {
    my $this = shift;

    $this->{-prevind} = $this->{-stateind};
    if ($this->{-stateind} == 0) {
	$this->{-stateind} = $this->{-nstates} - 1;
    } else {
	$this->{-stateind}--;
    }
    $this->run_event('-onchange');
    $this->schedule_draw(1);
}

sub cycle() {
    my $this = shift;

    $this->{-prevind} = $this->{-stateind};
    $this->{-stateind}++;
    $this->{-stateind} = 0 if $this->{-stateind} == $this->{-nstates};
    $this->run_event('-onchange');
    $this->schedule_draw(1);
}

sub shortcut() {
    my $this = shift;
    my $key = shift;
    my $cur = $this->{-stateind};

    if (defined $this->{-shortcuts}) {
        $this->{-stateind} = $this->{-shortcuts}{$key};
        if ($cur != $this->{-stateind}) {
            $this->run_event('-onchange');
            $this->schedule_draw(1);
        }
    }
}

sub draw(;$) {
    my $this = shift;
    my $no_doupdate = shift || 0;

    # Draw the widget.
    $this->SUPER::draw(1) or return $this;

    # Draw the checkbox.
     if ($Curses::UI::color_support) {
	my $co = $Curses::UI::color_object;
	my $pair = $co->get_color_pair(
			     $this->{-fg},
			     $this->{-bg});
	$this->{-canvasscr}->attron(COLOR_PAIR($pair));
    }

    $this->{-canvasscr}->attron(A_BOLD) if $this->{-focus};
    $this->{-canvasscr}->addstr(0, 0, '[ ]');
    $this->{-canvasscr}->addstr(0, 1, $this->get_state());
    $this->{-canvasscr}->attroff(A_BOLD) if $this->{-focus};

    $this->{-canvasscr}->move(0,1);
    $this->{-canvasscr}->noutrefresh();
    doupdate() unless $no_doupdate;

    return $this;
}


=head1 WIDGET-SPECIFIC METHODS

=head2 get

Returns the index of the current state of the control.

=cut

sub get() {
    my $this = shift;
    return $this->{-stateind};
}

=head2 set

Sets the index of the current state of the control.

=cut

sub set() {
    my $this = shift;
    $this->{-stateind} = shift;
}

=head2 get_state

Returns the character representation of the current state of the control.

=cut

sub get_state () {
   my $this = shift;

   return $this->{-states}[$this->{-stateind}];
}

=head2 toggle

Flip-flops the control to an appropriate "other" state. Follows the
togglemap hash if defined, otherwise reverts to the previous setting.

=cut

sub toggle() {
    my $this = shift;
    my $prevind = $this->{-prevind};

    $this->{-prevind} = $this->{-stateind};
    $this->{-stateind} = $this->{-togglemap}->{$this->{-stateind}} // $prevind;
    $this->run_event('-onchange');
    $this->schedule_draw(1);
}

=head2 onChange

This method can be used to set the C<-onchange> event handler (see
above) after initialization of the checkbox. It expects a coderef as
its argument.

=cut

sub onChange(;$)  { shift()->set_event('-onchange',  shift()) }

sub mouse_button1($$$$;) {
    my $this  = shift;
    my $event = shift;
    my $x     = shift;
    my $y     = shift;

    $this->focus();

    return $this;
}

=head1 DEFAULT BINDINGS

=over

=item C<[TAB]>, C<[ENTER}>

Call the 'loose-focus' routine, causing the widget to lose focus.

=item C<[SPACE]>

Call the L</toggle> method. Override this if unwanted.

=item C<E<lt>>

Call the L</reverse> method.

=item C<E<gt>>

Call the L</cycle> method.

=back


=head1 SEE ALSO

L<Curses::UI|Curses::UI>,
L<Curses::UI::Widget|Curses::UI::Widget>,
L<Curses::UI::Common|Curses::UI::Common>
L<Curses::UI::Checkbox|Curses::UI::Checkbox>

=head1 AUTHOR

Shawn Boyette C<< <mdxi@cpan.org> >>,
Andrew Bower C<< <andrew@bower.uk> >>

Andrew Bower wrote the Multistate class basing it on the existing Checkbox
class.

=head1 COPYRIGHT & LICENSE

Copyright 2001-2002 Maurice Makaay; 2003-2006 Marcus Thiesen; 2007
Shawn Boyette; 2025 Andrew Bower.

This program is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

This package is free software and is provided "as is" without express
or implied warranty. It may be used, redistributed and/or modified
under the same terms as perl itself.

=cut

1; # end of App::SysVRcConf::Multistate
