package Prima::Menus;

use strict;
use warnings;
use Prima;

package Prima::Menu::Item;
use base qw(Prima::MenuItem);

sub new
{
	my ( $class, $menu, $id, %opt ) = @_;
	my $self = $class->SUPER::new($menu, $id);
	%$self = ( %opt, %$self );
	return $self;
}

sub y_offset      { 5 }

sub selectable    { 1 }
sub vertical      { $_[0]->{vertical} }

sub new_from_itemid
{
	my ( $self, $menu, $itemid, %xopt ) = @_;

	my $class;
	unless ( defined $itemid ) {
		$class = 'Guillemots';
	} elsif ( $menu-> is_custom($itemid)) {
		$class = 'Custom';
	} elsif ( $menu-> is_separator($itemid)) {
		$class = 'Separator';
	} elsif ( $menu-> image($itemid)) {
		$class = 'Image';
	} else {
		$class = 'Text';
	}
	return "Prima::Menu::Item::$class"->new( $menu, $itemid, %xopt );
}

sub size { 0,0 }
sub draw {}
sub finalize {}

package Prima::Menu::Item::Separator;
use base qw(Prima::Menu::Item);

sub selectable { 0 }
sub size { 0, shift->y_offset * 2 }

sub draw
{
	my ( $self, $owner, $canvas, $colors, $x, $y, $w, $h, $selected ) = @_;
	$y += $h / 2 - 1;
	$canvas->color($$colors[ci::Light3DColor]);
	$canvas->line( $x - 1, $y, $x + $w - 1, $y);
	$canvas->color($$colors[ci::Dark3DColor]);
	$canvas->line( $x - 1, $y + 1, $x + $w - 1, $y + 1);
}

sub is_separator { 1 }

package Prima::Menu::Item::Simple;
use base qw(Prima::Menu::Item);

sub is_separator { 0 }

# horizontal: (CHECK=2*EM) EM/2 TEXT (EM/2 ACCEL)? EM/2
# vertical:    CHECK=2*EM  EM/2 TEXT (EM/2 ACCEL)? EM/2 DOWN=EM

sub em { $_[0]-> {em_width} }

sub finalize
{
	my ( $self, $storage ) = @_;
	$self-> {icon_width} = $storage->{icon_width};
}

sub size
{
	my ( $self, $canvas, $storage ) = @_;
	my @sz = $self->entry_size($canvas);
	my $fh = $canvas-> font-> height;
	$self->{em_width} = $canvas-> font-> width;

	my $em = $self->em;
	my $icon_width = 0;
	if ( my $icon = $self-> icon ) {
		my @asz = map { $_ / 4 } $::application-> size;
		$icon_width = $icon-> width;
		$icon_width = $asz[0] if $icon_width > $asz[0];

		my $h = $icon-> height;
		$h = $asz[1] if $h > $asz[1];
		$sz[1] = $h if $sz[1] < $h;
	} else {
		$icon_width = 2 * $em # space for *
			if $self->vertical || $self->checked || $self->autoToggle;
	}

	$storage->{icon_width} //= 0;
	$storage->{icon_width} = $icon_width if $storage->{icon_width} < $icon_width;

	$sz[0] += $icon_width;
	$sz[0] += $em; # half before, half after

	$sz[1] = $fh if $sz[1] < $fh;
	$sz[1] += $self-> y_offset * 2;

	$self->{accel_width} = length($self->accel) ? 
		$canvas->get_text_width( $self-> accel, 1 ) :
		0;
	$sz[0] += $em * 0.5 + $self->{accel_width}
		if $self->{accel_width} > 0;
	$sz[0] += $em if $self->vertical; 

	return @sz;
}

sub draw_bitmap
{
	my ( $self, $owner, $canvas, $colors, $icon, $x, $y, $w, $h) = @_;

	my $key = "$icon";

	my $cache;
	my $disabled = !$self-> enabled;
	unless ( $self->{cache}->{$key}) {
		$cache = $self->{cache}->{$key} = {};
		my $can_argb = $::application-> get_system_value( sv::LayeredWidgets );
		my $is_icon  = $icon->isa('Prima::Icon');

		my @asz = map { $_ / 4 } $::application-> size;
		my @sz  = $icon->size;
		if ( $sz[0] > $asz[0] || $sz[1] > $asz[1] ) {
			$icon = $icon->dup;
			$icon-> stretch(
				( $sz[0] > $asz[0]) ? $asz[0] : $sz[0],
				( $sz[1] > $asz[1]) ? $asz[1] : $sz[1],
			);
		}

		if ($is_icon && (( $icon->type & im::BPP ) == 1 ) && $icon-> maskType == 1 ) {
			$cache->{mono} = 1;
			my ($xor, undef) = $icon->split;
			$xor->type(im::BW);
			$cache->{bitmap} = $xor->bitmap;
		} elsif ( !$icon && !$disabled ) {
			goto FALLBACK;
		} elsif ( $can_argb && (
			$canvas->has_alpha_layer ||
			$disabled ||
			($is_icon && $icon->maskType == 8)
		)) {
			my ($xor, $and);
			if ( $is_icon ) {
				($xor, $and) = $icon->split;
				$xor->type(im::RGB);
				if ( $and->type != im::Byte ) {
					$and->data( ~$and->data);
					$and->type(im::Byte);
				}
				if ( $disabled ) {
					$and->set(
						color => 0,
						alpha => 0x80,
						rop   => rop::SrcOver,
					);
					$and->bar(0,0,$and->size);
				}
				my $i = Prima::Icon->create_combined($xor, $and);
				$i->premultiply_alpha;
				$cache->{bitmap} = $i->bitmap;
			} else {
				$xor = $icon;
				$xor = $xor->clone(type => im::RGB) if $xor-> type != im::RGB;
				$and = Prima::Image->new(
					size  => [ $icon-> size ],
					type  => im::Byte,
					color => ($disabled ? 0x808080 : 0xffffff),
				);
				$and->bar(0,0,$and->size);
				my $i = Prima::Icon->create_combined($xor, $and);
				$cache->{bitmap} = $i->bitmap;
			}
		} elsif ( $is_icon ) {
			$cache->{bitmap} = $icon->dup;
			$cache->{stipple} = $disabled;
		} else {
			$cache->{stipple} = $disabled;
		FALLBACK:
			$cache->{bitmap} = $icon->dup;
		}
	}
	$cache = $self->{cache}->{$key};
	$icon = $cache->{bitmap};

	my @sz = $icon-> size;
	my @src = ($x + ($w - $sz[0]) / 2, $y + ($h - $sz[1]) / 2);

	if ( $cache->{mono}) {
		if ( $disabled ) {
			$canvas->set( color => cl::Light3DColor, backColor => cl::Clear);
			$canvas->put_image( $src[0] + 1, $src[1] - 1, $icon, rop::OrPut );
		}
		$canvas->set( color => cl::Clear, backColor => cl::Set);
		$canvas->put_image( @src, $icon, rop::AndPut );
		$canvas->set( color => $$colors[$disabled ? ci::DisabledText : ci::NormalText ], backColor => cl::Clear);
		$canvas->put_image( @src, $icon, rop::XorPut );
	} else {
		$canvas->put_image( @src, $icon );
	}

	if ( $cache->{stipple}) {
		$canvas->set(
			color => cl::Set,
			backColor => cl::Clear,
			fillPattern => fp::SimpleDots,
			rop         => rop::AndPut,
		);
		$canvas-> bar( @src, $src[0] + $sz[0], $src[1] + $sz[1]);
		$canvas->set(
			fillPattern => fp::Solid,
			rop         => rop::CopyPut,
		);
	}
}

sub draw
{
	my ( $self, $owner, $canvas, $colors, $x, $y, $w, $h, $selected ) = @_;
	my $enabled    = $self->enabled;
	my $vertical   = $self->vertical;
	my $checked    = $self->checked;
	my $autotoggle = $self->autoToggle;
	if ( $selected ) {
		$canvas->backColor( $$colors[$enabled ? ci::Hilite : ci::Disabled ]);
		$canvas->clear( $x, $y, $x + $w - 1, $y + $h - 1 );
	}

	my $em = $self-> em;
	my $y_offset = $self-> y_offset;

	my ($draw_check, $draw_accel, $draw_submenu, $draw_check_aa);

	my $lw = 3;
	if ( my $icon = $self-> icon ) {
		$self-> draw_bitmap( $owner, $canvas, $colors, $icon, $x + 2, $y + $y_offset, $self->{icon_width}, $h - $y_offset * 2 );
	} elsif ( $checked ) {
		my $font = $canvas->font;
		my $Dx = $em * 0.25;
		my $Dy = ($font->{ascent} - $font->{internalLeading}) / 2;
		my $Y  = $y + $h / 2 + $font->{descent};
		my $X  = $x + $lw / 2 + $Dx / 2 + ( $self->{icon_width} - $em) / 2;
		my $Xw = $font->{height} / 6;
		$Xw = 3 if $Xw < 3;
		$Xw /= 2;
		$Y -= $Xw;
		$draw_check    = [$X, $Y, $X + $Dx, $Y - $Dy, $X + $Dx * 3, $Y + $Dy];
		$draw_check_aa = [
			$X - $Xw, $Y,
			$X + $Dx, $Y - $Dy - $Xw,
			$X + $Dx * 3 + $lw, $Y + $Dy,
			$X + $Dx, $Y - $Dy + $Xw,
			$X, $Y - $Xw,
		];
	} elsif ( $autotoggle ) {
		my $Y  = $y + $h / 2;
		my $D  = $em * 0.5;
		my $X  = $x + $D / 4 + ($self->{icon_width} - $em) / 2;
		my @r = ( $X, $Y - $D, $X + $D * 2, $Y + $D);
		my $c1 = $$colors[$enabled ? ci::Dark3DColor : ci::DisabledText];
		if ( $owner->skin eq 'flat') {
			my $lw = $canvas->font->{height} / 20;
			$lw = 1 if $lw < 1;
			$canvas-> lineWidth( $lw );
			$canvas-> color($c1);
			$canvas-> lineJoin(lj::Miter);
			$canvas-> rectangle(@r);
			$canvas-> lineWidth(1.0);
		} else {
			my $c2 = $enabled ? 0x404040 : $$colors[ci::Light3DColor];
			$canvas-> rect3d( @r, 1, $c1, $c2);
			$canvas-> rect3d( $r[0]+1,$r[1]+1,$r[2]-1,$r[3]-1, 1, $$colors[ci::Light3DColor], $c1);
		}
	}

	$x += $self-> {icon_width} + $em / 2;
	$w -= $self-> {icon_width} + $em / 2;

	$y += $y_offset;
	$h -= $y_offset * 2;
	if ( $self->{accel_width} > 0 ) {
		my $X = $x + $w - $self->{accel_width} - $em * ( $vertical ? 1.5 : 0.5 );
		$draw_accel = [ $self->accel, $X, $y ];
		$w -= $self-> {accel_width} + $em * 0.5;
	}

	if ( $vertical && $self-> is_submenu ) {
		my $Y  = $y + $h / 2;
		my $D  = $em / 2;
		my $X  = $x + $w - $D * 1.85;
		$draw_submenu = [$X, $Y - $D, $X + $D, $Y, $X, $Y + $D];
	}
	$w -= $em * ($vertical ? 1.5 : 0.5 );

	if ( $draw_check || $draw_accel || $draw_submenu ) {
		return unless $canvas->graphic_context_push;
		$canvas->antialias(1) if $draw_check || $draw_submenu;
		if ($draw_check) {
			unless ( $canvas-> antialias ) {
				$canvas->lineWidth($lw);
				$canvas->lineEnd(le::Round);
				undef $draw_check_aa;
			}
		}
		unless ( $self-> enabled ) {
			$canvas->color( $$colors[ci::Light3DColor] );
			my @tr = $canvas->translate;
			if ( $draw_check ) {
				if ( $draw_check_aa ) {
					$canvas->fillpoly($draw_check_aa);
				} else {
					$canvas->polyline($draw_check);
				}
			}
			$canvas->text_out( @$draw_accel) if $draw_accel;
			$canvas->fillpoly($draw_submenu) if $draw_submenu;
			$canvas->translate(@tr);
			$canvas->color( $$colors[ci::DisabledText] );
		} else {
			$canvas->color( $$colors[$selected ? ci::HiliteText : ci::NormalText] );
		}
		if ( $draw_check ) {
			if ( $draw_check_aa ) {
				$canvas->fillpoly($draw_check_aa);
			} else {
				$canvas->polyline($draw_check);
			}
		}
		$canvas->text_out( @$draw_accel ) if $draw_accel;
		$canvas->fillpoly($draw_submenu)  if $draw_submenu;
		$canvas->graphic_context_pop;
	}

	$self-> draw_entry( $owner, $canvas, $colors, $x, $y, $w, $h, $selected);
}

sub entry_size { 0,0 }

package Prima::Menu::Item::Text;
use base qw(Prima::Menu::Item::Simple);

sub entry_size
{
	my ( $self, $canvas ) = @_;
	my @lines = @{$canvas-> text_wrap( $self->text,
		1000000, tw::ReturnLines | tw::CalcMnemonic | tw::CollapseTilde, 1)};
	return $canvas->get_text_width($lines[0], 1), $canvas->font->height;
}

sub draw_entry
{
	my ( $self, $owner, $canvas, $colors, $x, $y, $w, $h, $selected ) = @_;

	my $text = $self-> text;
	$y += ( $h - $canvas-> font-> height ) / 2;
	unless ( $self-> enabled ) {
		$canvas->color( $$colors[ci::Light3DColor] );
		$canvas->draw_text( $text,
			$x + 1, $y - 1, $x + $w + 1, $y + $h - 1,
			dt::DrawMnemonic | dt::NoWordWrap | dt::Bottom, 1
		);
		$canvas->color( $$colors[ci::DisabledText] );
	} else {
		$canvas->color( $$colors[$selected ? ci::HiliteText : ci::NormalText] );
	}

	$canvas->draw_text( $text,
		$x, $y, $x + $w, $y + $h,
		dt::DrawMnemonic | dt::NoWordWrap | dt::Bottom, 1
	);
}

package Prima::Menu::Item::Guillemots;
use base qw(Prima::Menu::Item::Text);

sub text       { ">>" }
sub enabled    { 1 }
sub checked    { 0 }
sub autoToggle { 0 }
sub accel      { "" }
sub key        { 0 }
sub data       { undef }
sub icon       { undef }
sub image      { undef }
sub submenu    { undef }
sub items      { undef }
sub check      { }
sub uncheck    { }
sub remove     { }
sub toggle     { }
sub enable     { }
sub disable    { }
sub id         { }
sub execute    { }
sub children   { }
sub is_separator { 0 }
sub is_submenu   { 0 }

package Prima::Menu::Item::Image;
use base qw(Prima::Menu::Item::Simple);

sub entry_size
{
	my $self = shift;
	my @sz = $self->image->size;
	my @ds = map { $_ / 4 } $::application->size;
	for ( 0, 1 ) {
		$sz[$_] = $ds[$_] if $sz[$_] > $ds[$_];
	}
	return @sz;
}

sub draw_entry
{
	my ( $self, $owner, $canvas, $colors, $x, $y, $w, $h, $selected ) = @_;
	$self-> draw_bitmap( $owner, $canvas, $colors, $self-> image, $x, $y, $w, $h );
}

package Prima::Menu::Item::Custom;
use base qw(Prima::Menu::Item);

sub size
{
	my ($self, $canvas) = @_;
	my $cb   = $self->options->{onMeasure} or return;
	my @pt = (0,0);
	$cb->( $canvas-> menu, $self, \@pt);
	return @pt;
}

sub draw
{
	my ( $self, $owner, $canvas, $colors, $x, $y, $w, $h, $selected ) = @_;
	my $cb   = $self->options->{onPaint} or return;
	return unless $canvas->graphic_context_push;
	$cb->( $canvas-> menu, $self, $canvas, $selected, $x, $y, $x + $w - 1, $y + $h - 1);
	$canvas->graphic_context_pop;
}

package Prima::Menu::Common;

# cached entries
use constant X             => 0;
use constant Y             => 1;
use constant WIDTH         => 2;
use constant HEIGHT        => 3;
use constant ITEMID        => 4;
use constant OBJECT        => 5;

use vars qw(@ISA);
@ISA = qw(Prima::Widget);

sub _export_constants
{
	my $pkg = (caller())[0];
	no strict 'refs';
	*{ $pkg . "::$_" } = \&$_ for qw(
		X Y WIDTH HEIGHT ITEMID OBJECT
	);
}

{
my %RNT = (
	%{Prima::Widget-> notification_types()},
	Submenu      => nt::Default,
	ItemChanged  => nt::Default,
);

sub notification_types { return \%RNT; }
}

sub profile_default
{
	my %def = %{$_[ 0]-> SUPER::profile_default};
	return {
		%def,
		borderSize  => 2,
		parent      => undef,
		parentIndex => 0,
	}
}

sub init
{
	my $self = shift;

	$self->{selectedItem} = -1;
	for ( qw( borderSize ))
		{ $self-> {$_} = 1; }
	$self->{lock_reset} = 1;
	my %profile = $self-> SUPER::init(@_);

	for ( qw( borderSize parent parentIndex ))
		{ $self-> $_( $profile{ $_}); }
	delete $self->{lock_reset};

	return %profile;
}

sub skin
{
	return $_[0]->SUPER::skin unless $#_;
	my $self = shift;
	$self->SUPER::skin($_[1]);
	$self->borderSize(1) if $self->SUPER::skin eq 'flat';
	$self->repaint;
}

sub borderSize
{
	return $_[0]->{borderSize} unless $#_;
	my ( $self, $borderSize ) = @_;
	$self->{borderSize} = $borderSize;
	$self->reset;
	$self->repaint;
}

sub parent
{
	return $_[0]->{parent} unless $#_;
	my ( $self, $parent ) = @_;
	$self->{parent} = $parent;
	$self->reset;
	$self->repaint;
}

sub parentIndex
{
	return $_[0]->{parentIndex} unless $#_;
	my ( $self, $parentIndex ) = @_;
	$self->{parentIndex} = $parentIndex;
	$self->reset;
	$self->repaint;
}

sub vertical    { $_[0]->{vertical} }

sub get_item_rect
{
	my ( $self, $i ) = @_;
	my $c = $self->{cache}->[$i];
	return @{$c}[X, Y], $$c[X] + $$c[WIDTH], $$c[Y] + $$c[HEIGHT];
}

sub navigate_selection
{
	my ( $self, $desired_item, $direction_if_not_selectable) = @_;
	my $c = $self->{cache};
	my $n = @$c - 1;
	$desired_item = $n + 1 + $desired_item if $desired_item < 0;
	$desired_item = $n if $desired_item < 0;
	$desired_item = 0 if $desired_item > $n;
	while (1) {
		my $object = $c->[$desired_item]->[OBJECT];
		last if $object && $object->selectable && $object->enabled;

		$desired_item += $direction_if_not_selectable;
		if ($desired_item < 0 || $desired_item > $n) {
			$desired_item = 0 if $desired_item > $n;
			$desired_item = $n if $desired_item < 0;
			while (1) {
				my $object = $c->[$desired_item]->[OBJECT];
				last if $object && $object->selectable && $object->enabled;

				$desired_item += $direction_if_not_selectable;
				return if $desired_item < 0 || $desired_item > $n;
			}
		}
	}

	return if $desired_item == $self->selectedItem;
	$self->selectedItem($desired_item);
	return 1;
}

sub selectedItem
{
	return $_[0]->{selectedItem} unless $#_;
	my ( $self, $i) = @_;
	my $n = @{ $self->{cache} } - 1;
	$i = -1 if $i < 0;
	$i = $n if $i > $n;

	return if $i == $self->{selectedItem};
	my $old = $self->{selectedItem};
	$self->{selectedItem} = $i;
	$self->invalidate_rect( $self->get_item_rect($old) ) if $old >= 0;
	$self->invalidate_rect( $self->get_item_rect($i) )   if $i   >= 0;
}

sub enter_submenu
{
	my $self = shift;
	my $i = $self->selectedItem;
	return if $i < 0;

	if ( $self->{submenu} ) {
		return if $i == $self->{submenu_index};
		$self->{submenu}->cancel;
		undef $self->{submenu};
		undef $self->{submenu_index};
	}

	my ($itemid, $index);
	if ( defined ( $itemid = $self->{cache}->[$i]->[ITEMID] )) {
		return unless $self-> menu-> is_submenu($itemid);
		$index = 0;
	} else {
		$itemid = $self-> parent;
		$index  = $self-> parentIndex + @{ $self->{cache} } - 1;
	}

	$self->notify(qw(Submenu), $i);
	return unless $self->alive;

	$self->{submenu_index} = $i;

	my $submenu = $self->{submenu} = $self->insert( $self->root->popupClass,
		visible        => 0,
		root           => $self->root,
		parent         => $itemid,
		parentIndex    => $index,
		menu           => $self->menu,
		ownerColor     => 1,
		ownerBackColor => 1,
		ownerFont      => 1,
		( map { $_ , $self->$_() } 
			qw(hiliteColor hiliteBackColor disabledColor disabledBackColor)),
	);
	$submenu->origin($self->position_submenu);
	$submenu->show;
	$submenu->bring_to_front;
}

sub execute_selected
{
	my $self = shift;
	my $i = $self->selectedItem;
	return if $i < 0;

	my $cache = $self->{cache}->[$i];
	return $self-> enter_submenu if ! defined $cache->[ITEMID] or $self-> menu-> is_submenu($cache->[ITEMID]);

	$self->root->cancel;
	$cache->[OBJECT]-> execute if $cache->[OBJECT];
}

sub execute_item
{
	my ($self, $itemid) = @_;
	return unless defined $itemid;
	return $self-> enter_submenu if $self-> menu-> is_submenu($itemid);

	$self->root->cancel;
	$self->new_from_itemid( $itemid )->execute;
}

sub xy2item
{
	my ( $self, $x, $y ) = @_;

	my $c = $self->{cache};
	for ( my $i = 0; $i < @$c; $i++) {
		my ($X1,$Y1,$X2,$Y2) = @{ $c->[$i] };
		$X2 += $X1;
		$Y2 += $Y1;
		return $i if $x >= $X1 && $y >= $Y1 && $x <= $X2 && $y < $Y2;
	}

	return -1;
}

sub on_keydown
{
	my ( $self, $code, $key, $mod, $repeat) = @_;

	my $submenu = $self;
	my $level = 0;
	my @hierarchy;
	while ($submenu->{submenu}) {
		push @hierarchy, $submenu;
		$submenu = $submenu->{submenu};
		$level++;
	}

	if ($key == kb::Esc) {
		$submenu->cancel;
		$self->clear_event;
		return;
	}

	my ($ok, $skip);

	if ( $level == 1 ) {
		if ( $key == kb::Left ) {
			$ok = $self-> navigate_selection( $self->selectedItem-1, -1 );
			$skip = 1;
		} elsif ( $key == kb::Right ) {
			$ok = $self-> navigate_selection( $self->selectedItem+1, 1 );
			$skip = 1;
		} elsif ( $key == kb::Home ) {
			$ok = $self-> navigate_selection( 0, 1 );
			$skip = 1;
		} elsif ( $key == kb::End ) {
			$ok = $self-> navigate_selection( -1, -1 );
			$skip = 1;
		}
		return $self-> enter_submenu if $ok;
	}

	if ( $skip ) {
		#
	} elsif ( $key == kb::Left || $key == kb::Up ) {
		$ok = $submenu-> navigate_selection( $submenu->selectedItem-1, -1 );
		$skip = 1;
	} elsif ( $key == kb::Right || $key == kb::Down ) {
		$ok = $submenu-> navigate_selection( $submenu->selectedItem+1, 1 );
		$skip = 1;
	} elsif ( $key == kb::Home || $key == kb::PgUp ) {
		$ok = $submenu-> navigate_selection( 0, 1 );
		$skip = 1;
	} elsif ( $key == kb::End || $key == kb::PgDn ) {
		$ok = $submenu-> navigate_selection( -1, -1 );
		$skip = 1;
	} elsif ( $key == kb::Enter ) {
		$submenu-> execute_selected;
		$ok = 1;
		return unless $self->alive;
	}

	# shortcuts
	my $m = $self->menu;
	if ( defined( my $itemid = $m->find_item_by_key(
		$m->translate_key($code, $key, $mod)
	))) {
		if ( $m->enabled($itemid)) {
			$self-> execute_item($itemid);
			$ok = 1;
		}
	}

	# immediate ~ hotkeys
	if ( !$ok && chr($code) =~ /^[0-9a-z]$/i) {
		MENU: for my $upper ( $submenu, reverse @hierarchy ) {
			my $idx = -1;
			for my $c ( @{ $upper->{cache} }) {
				$idx++;
				next unless defined $c->[ITEMID];
				my $text = $m->text($c->[ITEMID]);
				next unless defined $text;
				next unless $text =~ m/(?<!~)~([a-z0-9])/i;
				next unless lc(chr($code)) eq lc($1);
				if ($self-> menu-> is_submenu($c->[ITEMID])) {
					$upper->selectedItem($idx);
					$upper->enter_submenu;
				} else {
					$self-> execute_item($c->[ITEMID]);
				}
				$ok = 1;
				last MENU;
 			}
		}
	}

	$ok = 1 if $skip && $level > 0;

	$self->clear_event if $ok && $self->alive;
}

sub on_fontchanged
{
	my $self = shift;
	$self-> reset;
}

sub on_size
{
	my $self = shift;
	$self-> reset;
}

sub on_itemchanged
{
	my $self = shift;
	$self-> reset;
}

sub on_paint
{
	my ( $self, $canvas ) = @_;
	my @c = map { $self->colorIndex($_) } ci::NormalText .. ci::Dark3DColor;
	@c[ci::NormalText, ci::Normal] = @c[ci::DisabledText, ci::Disabled] unless $self->enabled;
	my @sz = $self->size;
	my $caches = $self->{cache};
	if ( $self->{vertical} ) {
		if ( $self-> skin eq 'flat') {
			$canvas-> graphic_context( sub {
				$canvas-> color( $self-> dark3DColor ); 
				$canvas-> rectangle(0, 0, $sz[0]-1, $sz[1]-1);
				$canvas-> color( $c[ci::Normal] );
				$canvas-> bar( 1, 1, $sz[0]-2, $sz[1]-2);
			});
		} else {
			$self->rect_bevel( $canvas, 0, 0, $sz[0]-1, $sz[1]-1,
				width => $self->{borderSize}, 
				fill => $c[ci::Normal],
			);
		}
	} else {
		$self->clear;
	}

	for ( my $i = 0; $i < @$caches; $i++) {
		my $cache = $caches->[$i];
		next unless $cache->[OBJECT];
		my @o = @{$cache}[X, Y];
		my @s = @{$cache}[WIDTH, HEIGHT];
		$cache->[OBJECT]->draw( $self, $canvas, \@c, @$cache[X,Y,WIDTH,HEIGHT], $i == $self->{selectedItem});
	}
}

sub new_from_itemid
{
	my ( $self, $itemid ) = @_;
	return $self->root->{itemClass}-> new_from_itemid(
		$self-> root->menu, $itemid,
		vertical => $self->vertical,
	);
}

sub new_guillemots
{
	my ( $self ) = @_;
	return $self->root->{itemClass}-> new_from_itemid(
		$self->root->menu, undef,
		vertical => $self->vertical,
	);
}

package Prima::Menu::Transient;
use vars qw(@ISA);
@ISA = qw(Prima::Menu::Common);

BEGIN { Prima::Menu::Common::_export_constants(); };

sub profile_default
{
	my %def = %{$_[ 0]-> SUPER::profile_default};
	return {
		%def,
		font           => Prima::Widget->get_default_popup_font,
		widgetClass    => wc::Popup,
		clipOwner      => 0,
		selectable     => 0,
	}
}

sub profile_check_in
{
	my ( $self, $p, $default) = @_;
	$p->{font} //= Prima::Widget->get_default_popup_font;
	$self-> SUPER::profile_check_in( $p, $default);
}

sub init
{
	my $self = shift;
	$self->{vertical} = 1;
	my %profile = $self-> SUPER::init(@_);
	$self->root($profile{root});
	$self->reset;
	return %profile;
}

sub root        { $#_ ? $_[0]->{root}        = $_[1] : $_[0]->{root} }
sub menu        { shift->root->menu }

sub cancel
{
	my $self = shift;
	my $o = $self->owner;
	if ( $o && $o->isa('Prima::Menu::Common')) {
		undef $o->{submenu};
		$self->destroy;
	}
}

sub reset
{
	my $self = shift;
	return if $self->{lock_reset};

	my @cache;
	my @size = $self-> size;
	$self-> begin_paint_info;
	my $space_width = $self->get_text_width(' ');
	my $menu = $self-> menu or return;

	my $items = $menu-> get_children( $self-> parent );
	splice( @$items, 0, $self->parentIndex ) if $self->parentIndex > 0;
	my $borderSize = $self->{borderSize};
	my ($x, $y) = (0,0);
	my @ds = $::application->size;
	my $r = $ds[0] - $borderSize * 2;
	my $h = $ds[1] - $borderSize * 2;
	my @max = (0,0);
	my %storage;
	for my $itemid ( @$items ) {
		my $object = $self->new_from_itemid( $itemid );
		my ($W, $H) = $object-> size($self, \%storage);
		if ( $y + $H > $h ) {
			$object = $self->new_guillemots;
			($W, $H) = $object-> size($self);
			$max[0] = $W if $max[0] < $W;
			if ( @cache ) {
				@{$cache[-1]}[WIDTH,HEIGHT,ITEMID,OBJECT] = ($W, $H, undef, $object);
			} else {
				push @cache, [ $x, $y, $W, $H, undef, $object ];
			};
			last;
		}
		push @cache, [ $x, $y, $W, $H, $itemid, $object ];
		$y += $H;
		$max[0] = $W if $max[0] < $W;
	}
	$self-> end_paint_info;

	$self->{cache} = \@cache;
	if ( @cache ) {
		$max[1] = $cache[-1]->[Y] + $cache[-1]->[HEIGHT];
		$max[0] = $r if $max[0] > $r;

		for (@cache) {
			$_->[X] += $borderSize;
			$_->[WIDTH] = $max[0];
			$_->[Y] = $max[1] - $_->[Y] - $_->[HEIGHT] + $borderSize;
			$_->[OBJECT]-> finalize(\%storage);
		}
		$max[$_] += $borderSize * 2 for 0,1;
		if ( $size[0] != $max[0] || $size[1] != $max[1] ) {
			local $self->{lock_reset} = 1;
			$self->size(@max);
		}
	}
}

sub position_submenu
{
	my $self = shift;
	my $sel = $self->selectedItem;
	my @r = $self-> client_to_screen($self->get_item_rect( $sel ));
	my @sz = $self->{submenu}->size;
	my @dp = $::application->size;
	my @pos;

	if ( $r[2] + $sz[0] < $dp[0]) {
		$pos[0] = $r[2];
	} elsif ( $r[0] > $sz[0] ) {
		$pos[0] = $r[0] - $sz[0];
	} else {
		$pos[0] = 0;
	}

	if ( $r[3] > $sz[1] ) {
		$pos[1] = $r[3] - $sz[1];
	} elsif ( $r[1] + $sz[1] < $dp[1] ) {
		$pos[1] = $r[1];
	} else {
		$pos[1] = 0;
	}

	return @pos;
}

sub on_mousedown
{
	my ( $self, $btn, $mod, $x, $y) = @_;

	my $i = $self-> xy2item($x,$y);

	return if $i < 0;
	my $o = $self->{cache}->[$i]->[OBJECT];
	return unless $o && $o->selectable && $o->enabled;

	$self->selectedItem($i);
	$self->execute_selected;
}

sub on_mousemove
{
	my ( $self, $mod, $x, $y) = @_;

	my $i = $self-> xy2item($x,$y);
	return if $i < 0;
	my $o = $self->{cache}->[$i]->[OBJECT];
	return unless $o && $o->selectable && $o->enabled;

	if ( $i != $self->selectedItem ) {
		$self->selectedItem($i);
		$self->enter_submenu;
	}
}

sub on_mouseleave
{
	my ( $self ) = @_;
	$self->selectedItem(-1);
}

package Prima::Menu::Root;

use Prima::Utils qw(alarm);

sub on_menuchange
{
	my ( $self, $menu, $what, @params) = @_;
	$self->reset;
}

sub set_menu
{
	my ( $self, $menu ) = @_;
	if ($self->{menu}) {
		if ($self->{menu_hook}) {
			$self-> {menu}-> remove_notification( $self-> {menu_hook} );
			undef $self->{menu_hook};
		}
		$self->cancel;
	}
	$self->{menu} = $menu;
	$self->{menu_hook} = $menu-> add_notification( Change => \&on_menuchange, $self )
		if $menu;
	$self->reset;
	$self->repaint;
}

sub on_menuenter
{
	my $self = shift;
	my $owner = $self->owner;

	$self-> {hooks} = [];
	my $cancel = sub { shift-> cancel };
	while ( $owner && $owner != $::application) {
		push @{$self->{hooks}}, $owner, $owner->add_notification( Move => $cancel, $self);
		$owner = $owner->owner;
	}
}

sub on_menuleave
{
	my $self = shift;
	my $hooks = $self->{hooks} // [];
	for ( my $i = 0; $i < @$hooks; $i += 2 ) {
		my ( $owner, $id ) = @{$hooks}[$i,$i+1];
		$owner->remove_notification($id);
	}
	$self->{hooks} = undef;
}

sub on_leave
{
	my $self = shift;

	alarm( 0.2, sub {
		my $f = $::application->get_focused_widget;
		$self->cancel unless $f && $f->isa('Prima::Menu::Common') && $f->root == $self->root;
	});
}

sub on_destroy { undef $_[0]->{hooks} }

sub itemClass   { $#_ ? $_[0]->{itemClass}   = $_[1] : $_[0]->{itemClass}  }
sub popupClass  { $#_ ? $_[0]->{popupClass}  = $_[1] : $_[0]->{popupClass}  }

package Prima::Menu::Popup;
use vars qw(@ISA);
@ISA = qw(Prima::Menu::Transient Prima::Menu::Root);

{
my %RNT = (
	%{Prima::Menu::Common-> notification_types()},
	MenuEnter  => nt::Default,
	MenuLeave  => nt::Default,
);

sub notification_types { return \%RNT; }
}

sub profile_default
{
	my %def = %{$_[ 0]-> SUPER::profile_default};
	return {
		%def,
		visible        => 0,
		itemClass      => 'Prima::Menu::Item',
		popupClass     => 'Prima::Menu::Transient',
		parent         => "",
		parentIndex    => 0,
		selectable     => 1,
	}
}

sub init
{
	my $self = shift;

	my %profile = $self-> SUPER::init(@_);
	$self->$_($profile{$_}) for qw(itemClass popupClass menu);
	return %profile;
}

sub root { $_[0] }
sub menu { $#_ ? $_[0]->set_menu($_[1]) : $_[0]->{menu} }

sub cancel
{
	my $self = shift;
	my $s = $self->{submenu};
	while ( $s ) {
		my $ss = $s->{submenu};
		$s->destroy;
		$s = $ss;
	}
	$self->{submenu} = undef;
	$self->{selectedItem} = -1;
	$self-> notify(q(MenuLeave));
	$self-> hide;
}

sub popup
{
	my ( $self, $x, $y ) = @_;
	unless ( defined($x) && defined($y)) {
		my @p = $self->owner->pointerPos;
		$x //= $p[0];
		$y //= $p[1];
	}
	my @sz = $self->size;
	my @dp = $::application->size;

	if ( $x + $sz[0] > $dp[0]) {
		if ( $x > $sz[0] ) {
			$x -= $sz[0];
		} else {
			$x = 0;
		}
	}

	if ( $y > $sz[1] ) {
		$y -= $sz[1];
	} elsif ( $y + $sz[1] > $dp[1] ) {
		$y = 0;
	}

	$self-> notify(q(MenuEnter));
	$self-> origin($x, $y);
	$self-> show;
	$self-> bring_to_front;
	$self-> focus;
}

package Prima::Menu::Bar;
use vars qw(@ISA);
@ISA = qw(Prima::Menu::Common Prima::Menu::Root);

BEGIN { Prima::Menu::Common::_export_constants(); };

{
my %RNT = (
	%{Prima::Menu::Common-> notification_types()},
	MenuEnter  => nt::Default,
	MenuLeave  => nt::Default,
);

sub notification_types { return \%RNT; }
}

sub profile_default
{
	my %def = %{$_[ 0]-> SUPER::profile_default};
	return {
		%def,
		font           => Prima::Window->get_default_menu_font,
		autoHeight     => 1,
		itemClass      => 'Prima::Menu::Item',
		popupClass     => 'Prima::Menu::Transient',
		parent         => "", # root menu item name
		selectable     => 1,
		widgetClass    => wc::Menu,
	}
}

sub profile_check_in
{
	my ( $self, $p, $default) = @_;
	$p-> {autoHeight} = 0 if !exists $p->{autoHeight} && (
		exists $p-> {height} || exists $p-> {size} || exists $p-> {rect} || ( exists $p-> {top} && exists $p-> {bottom})
	);
	$p->{font} //= Prima::Window->get_default_menu_font;
	$self-> SUPER::profile_check_in( $p, $default);
}

sub init
{
	my $self = shift;

	$self->{autoHeight} = 1;
	$self->{vertical} = 0;
	my %profile = $self-> SUPER::init(@_);
	$self-> {lock_reset} = 1;
	$self->$_($profile{$_}) for qw(autoHeight menu itemClass popupClass);
	delete $self-> {lock_reset};
	$self->reset;

	return %profile;
}

sub root { $_[0] }
sub menu { $#_ ? $_[0]->set_menu($_[1]) : $_[0]->{menu} }

sub autoHeight
{
	return $_[0]->{autoHeight} unless $#_;
	my ( $self, $autoHeight ) = @_;
	$self->{autoHeight} = $autoHeight;
	$self-> geomHeight( $self-> font-> height + 10 + $self-> {borderSize} * 2 )
		if $autoHeight;
}

sub cancel
{
	my $self = shift;
	my $s = $self->{submenu};
	while ( $s ) {
		my $ss = $s->{submenu};
		$s->destroy;
		$s = $ss;
	}
	$self->{submenu} = undef;
	$self->{selectedItem} = -1;
	return unless $self->alive;
	$self->focused(0);
	$self->repaint;
	$self-> notify(q(MenuLeave));
}


sub reset
{
	my $self = shift;
	return if $self->{lock_reset};

	my @cache;
	my @size = $self-> size;
	$self-> begin_paint_info;
	my $space_width = $self->get_text_width(' ');
	my $menu = $self->menu or return;

	my $items = $menu-> get_children( $self-> parent );
	splice( @$items, 0, $self->parentIndex ) if $self->parentIndex > 0;
	my $right_align;
	my ($x, $y) = ($self->{borderSize}) x 2;
	my $h = $size[1] - $y * 2;
	my $r = $size[0] - $x * 2;
	my %storage;
	for my $itemid ( @$items ) {
		if ( $self-> menu-> is_separator($itemid)) {
			push @cache, $right_align //= [0,0,0,0,undef,undef];
			next;
		}

		my $object = $self->new_from_itemid($itemid);
		my ($W, $H) = $object-> size($self, \%storage);
		if ( $x + $W > $r ) {
			$object = $self->new_guillemots;
			($W, $H) = $object-> size($self);
			undef $right_align;
			if ( @cache && $x + $W > $r) {
				pop @cache while @cache && !defined $cache[-1]->[OBJECT];
			}
			if ( @cache && $x + $W > $r) {
				@{$cache[-1]}[WIDTH,HEIGHT,ITEMID,OBJECT] = ($W, $H, undef, $object);
			} else {
				push @cache, [ $x, $y, $W, $H, undef, $object ];
			}
			last;
		}
		push @cache, [ $x, $y, $W, $H, $itemid, $object ];
		$x += $W;
	}

	if ( $right_align) {
		my $dx = $r - $cache[-1]->[X] - $cache[-1]->[WIDTH];
		my $got_align;
		for my $cache ( @cache ) {
			$got_align = 1 if $cache == $right_align;
			next unless $got_align;
			$cache->[X] += $dx;
		}
	}
	$_->[OBJECT]-> finalize(\%storage) for grep { defined $_->[OBJECT] } @cache;

	$self-> end_paint_info;
	$self->{cache} = \@cache;
}

sub position_submenu
{
	my $self = shift;
	my $sel = $self->selectedItem;
	my @r = $self-> client_to_screen($self->get_item_rect( $sel ));
	my @sz = $self->{submenu}->size;
	my @dp = $::application->size;
	my @pos;

	my $right_aligned;
	for ( my $i = 0; $i < $sel; $i++) {
		next if $self->{cache}->[$i]->[OBJECT];
		$right_aligned = 1;
		last;
	}

	$pos[0] = 0;
	if ( $right_aligned ) {
		if ( $r[2] > $sz[0] ) {
			$pos[0] = $r[2] - $sz[0];
		} elsif ( $r[0] + $sz[0] < $dp[0]) {
			$pos[0] = $r[0];
		}
	} else {
		if ( $r[0] + $sz[0] < $dp[0]) {
			$pos[0] = $r[0];
		} elsif ( $r[2] > $sz[0] ) {
			$pos[0] = $r[2] - $sz[0];
		}
	}

	if ( $r[1] > $sz[1] ) {
		$pos[1] = $r[1] - $sz[1];
	} elsif ( $r[3] + $sz[1] < $dp[1] ) {
		$pos[1] = $r[3];
	} else {
		$pos[1] = 0;
	}

	return @pos;
}

sub selectedItem
{
	return shift->SUPER::selectedItem unless $#_;
	my ( $self, $i ) = @_;

	my $old = $self->{selectedItem};
	$self->SUPER::selectedItem($i);
	if ( $old < 0 && $self->{selectedItem} >= 0 ) {
		$self-> notify(q(MenuEnter));
	}
}

sub on_mousedown
{
	my ( $self, $btn, $mod, $x, $y) = @_;

	my $i = $self-> xy2item($x,$y);

	return if $i < 0;
	my $o = $self->{cache}->[$i]->[OBJECT];
	return unless $o && $o->selectable && $o->enabled;

	$self->selectedItem($i);
	$self->execute_selected;
}

sub on_mousemove
{
	my ( $self, $mod, $x, $y) = @_;

	return if $self->selectedItem < 0;

	my $i = $self-> xy2item($x,$y);
	return if $i < 0;
	my $o = $self->{cache}->[$i]->[OBJECT];
	return unless $o && $o->selectable && $o->enabled;

	if ( $i != $self->selectedItem ) {
		$self->selectedItem($i);
		$self->enter_submenu;
	}
}

sub on_enter
{
	my $self = shift;
	$self->selectedItem(0) if
		$self->selectedItem < 0 && $self->enabled && @{ $self->{cache} // [] };
	$self->repaint;
}

sub on_disable
{
	my $self = shift;
	$self->cancel;
	$self->repaint;
}

sub on_enable
{
	my $self = shift;
	$self->repaint;
}

sub on_size
{
	my $self = $_[0];
	$self->cancel;
	$self->reset;
}

1;

=pod

=head1 NAME

Prima::Menus - menu widgets

=head1 DESCRIPTION

This module contains classes that can create menu widgets used as regular
widgets, without any special considerations about system-depended menus.

=head1 SYNOPSIS

	use Prima qw(Application Menus);
	my $w = Prima::MainWindow->new(
		accelItems => [['~File' => [
			['Exit' => sub { exit } ],
		]]],
		onMouseDown => sub {
			Prima::Menu::Popup->new(menu => $_[0]-> accelTable)->popup;
		},
		height => 100,
	);
	$w->insert( 'Prima::Menu::Bar',
		pack  => { fill => 'x', expand => 1},
		menu  => $w-> accelTable,
	);
	run Prima;

=for podview <img src="Prima/menu.gif">

=for html <p><img src="https://raw.githubusercontent.com/dk/Prima/master/pod/Prima/menu.gif">

=head1 AUTHOR

Dmitry Karasik, E<lt>dmitry@karasik.eu.orgE<gt>.

=head1 SEE ALSO

L<Prima>, L<Prima::Menu>, F<examples/menu.pl>

=cut
