The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package SDLx::Text;
use strict;
use warnings;
use SDL;
use SDL::Video;
use SDL::Config;
use SDL::TTF;
use SDL::TTF::Font;
use SDLx::Validate;
use List::Util qw(max sum);

use Carp ();

our $VERSION = 2.548;

sub new {
	my ($class, %options) = @_;
	unless ( SDL::Config->has('SDL_ttf') ) {
		Carp::cluck("SDL_ttf support has not been compiled");
	}  
	my $file = $options{'font'};
    if (!$file) {
        require File::ShareDir;
        $file = File::ShareDir::dist_file('SDL', 'GenBasR.ttf');
    }

	my $color = defined $options{'color'} ? $options{'color'} : [255, 255, 255];

	my $size = $options{'size'} || 24;

	my $shadow        = $options{'shadow'}        || 0;
	my $shadow_offset = $options{'shadow_offset'} || 1;

	my $shadow_color  = defined $options{'shadow_color'}
	                  ? $options{'shadow_color'}
	                  : [0, 0, 0]
	                  ;

	my $self = bless {}, ref($class) || $class;

	$self->{x} = $options{'x'} || 0;
	$self->{y} = $options{'y'} || 0;

	$self->{h_align} = $options{'h_align'} || 'left';
# TODO: validate
# TODO: v_align
	unless ( SDL::TTF::was_init() ) {
		Carp::cluck ("Cannot init TTF: " . SDL::get_error() )
		    unless SDL::TTF::init() == 0;
	}

	$self->size($size);
	$self->font($file);
	$self->color($color);
	$self->shadow($shadow);
	$self->shadow_color($shadow_color);
	$self->shadow_offset($shadow_offset);

    $self->bold($options{'bold'}) if exists $options{'bold'};
    $self->italic($options{'italic'}) if exists $options{'italic'};
    $self->underline($options{'underline'}) if exists $options{'underline'};
    $self->strikethrough($options{'strikethrough'}) if exists $options{'strikethrough'};

    # word wrapping
    $self->{word_wrap} = $options{'word_wrap'} || 0;

	$self->text( $options{'text'} ) if exists $options{'text'};

	return $self;
}

sub font {
	my ($self, $font_filename) = @_;

	if ($font_filename) {
		my $size = $self->size;

		$self->{_font} = SDL::TTF::open_font($font_filename, $size)
			or Carp::cluck "Error opening font '$font_filename': " . SDL::get_error;

	    $self->{_font_filename} = $font_filename;
	    $self->{_update_surfaces} = 1;
	}

	return $self->{_font};
}

sub font_filename {
    return $_[0]->{_font_filename};
}

sub color {
	my ($self, $color) = @_;

	if (defined $color) {
		$self->{_color} = SDLx::Validate::color($color);
	    $self->{_update_surfaces} = 1;
	}

	return $self->{_color};
}

sub size {
	my ($self, $size) = @_;

	if ($size) {
		$self->{_size} = $size;

		# reload the font using new size.
		# No need to set "_update_surfaces"
		# since font() already does it.
		$self->font( $self->font_filename );
	}

	return $self->{_size};
}

sub _style {
    my ($self, $flag, $enable) = @_;

    my $styles = SDL::TTF::get_font_style( $self->font );

    # do we have an enable flag?
    if (@_ > 2) {

        # we do! setup flags if we're enabling or disabling
        if ($enable) {
            $styles |= $flag;
        }
        else {
            $styles ^= $flag if $flag & $styles;
        }

        SDL::TTF::set_font_style( $self->font, $styles );

        # another run, returning true if value was properly set.
        return SDL::TTF::get_font_style( $self->font ) & $flag;
    }
    # no enable flag present, just return
    # whether the style is enabled/disabled
    else {
        return $styles & $flag;
    }
}

sub normal        { my $self = shift; $self->_style( TTF_STYLE_NORMAL,        @_ ) }
sub bold          { my $self = shift; $self->_style( TTF_STYLE_BOLD,          @_ ) }
sub italic        { my $self = shift; $self->_style( TTF_STYLE_ITALIC,        @_ ) }
sub underline     { my $self = shift; $self->_style( TTF_STYLE_UNDERLINE,     @_ ) }
sub strikethrough { my $self = shift; $self->_style( TTF_STYLE_STRIKETHROUGH, @_ ) }


sub h_align {
	my ($self, $align) = @_;

	if ($align) {
		$self->{h_align} = $align;
		$self->{_update_surfaces} = 1;
	}

	return $self->{h_align};
}

sub shadow {
	my ($self, $shadow) = @_;

	if ($shadow) {
	    $self->{shadow} = $shadow;
	    $self->{_update_surfaces} = 1;
	}

	return $self->{shadow};
}

sub shadow_color {
	my ($self, $shadow_color) = @_;

	if (defined $shadow_color) {
		$self->{shadow_color} = SDLx::Validate::color($shadow_color);
	    $self->{_update_surfaces} = 1;
	}

	return $self->{shadow_color};
}


sub shadow_offset {
	my ($self, $shadow_offset) = @_;

	if ($shadow_offset) {
	    $self->{shadow_offset} = $shadow_offset;
	    $self->{_update_surfaces} = 1;
	}

	return $self->{shadow_offset};
}

sub w {
    my $surface = $_[0]->{surface};
    return $surface->w unless $surface and ref $surface eq 'ARRAY';

    return max map { $_ ? $_->w() : 0 } @$surface;
}

sub h {
    my $surface = $_[0]->{surface};
    return $surface->h unless $surface and ref $surface eq 'ARRAY';

    return sum map { $_ ? $_->h() : 0 } @$surface;
}

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

	if (defined $x) {
		$self->{x} = $x;
	}
	return $self->{x};
}

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

	if (defined $y) {
		$self->{y} = $y;
	}
	return $self->{y};
}

sub text {
	my ($self, $text) = @_;

    return $self->{text} if scalar @_ == 1;

    if ( defined $text ) {
        $text = $self->_word_wrap($text) if $self->{word_wrap};
        my $font = $self->{_font};
        my $surface = _get_surfaces_for($font, $text, $self->{_color} )
            or Carp::croak 'TTF rendering error: ' . SDL::get_error;

	    if ($self->{shadow}) {
	        my $shadow_surface = _get_surfaces_for($font, $text, $self->{shadow_color})
	            or Carp::croak 'TTF shadow rendering error: ' . SDL::get_error;

            $shadow_surface = [ $shadow_surface ] unless ref $shadow_surface eq 'ARRAY';

	        $self->{_shadow_surface} = $shadow_surface;
	    }

        $self->{surface} = $surface;
        $self->{text} = $text;
    }
    else {
        $self->{surface} = undef;
    }


	return $self;
}

# Returns the TTF surface for the given text.
# If the text contains linebreaks, we split into
# several surfaces (since SDL can't render '\n').
sub _get_surfaces_for {
    my ($font, $text, $color) = @_;

    return SDL::TTF::render_utf8_blended($font, $text, $color)
        if index($text, "\n") == -1;

    my @surfaces = ();
    my @paragraphs = split /\n/ => $text;
    foreach my $paragraph (@paragraphs) {
        push @surfaces, SDL::TTF::render_utf8_blended($font, $paragraph, $color);
    }
    return \@surfaces;
}

sub _word_wrap {
    my ($self, $text) = @_;

    my $maxlen = $self->{word_wrap};
    my $font   = $self->{_font};

    # code heavily based on Text::Flow::Wrap
    my @paragraphs = split /\n/ => $text;
    my @output;

    foreach my $paragraph (@paragraphs) {
        my @paragraph_output = ('');
        my @words  = split /\s+/ => $paragraph;

        foreach my $word (@words) {
            my $padded    = $word . q[ ];
            my $candidate = $paragraph_output[-1] . $padded;
            my ($w) = @{ SDL::TTF::size_utf8($font, $candidate) };
            if ($w < $maxlen) {
                $paragraph_output[-1] = $candidate;
            }
            else {
                push @paragraph_output, $padded;
            }
        }
        chop $paragraph_output[-1] if substr( $paragraph_output[-1], -1, 1 ) eq q[ ];

        push @output, \@paragraph_output;

    }

    return join "\n" => map {
        join "\n" => @$_
    } @output;
}

sub surface {
	return $_[0]->{surface};
}

sub write_to {
	my ($self, $target, $text) = @_;

    if (@_ > 2) {
        $self->text($text);
        $self->{_update_surfaces} = 0;
    }
    $self->write_xy($target, $self->{x}, $self->{y});
}

sub write_xy {
	my ($self, $target, $x, $y, $text) = @_;

	if (@_ > 4) {
	    $self->text($text);
        $self->{_update_surfaces} = 0;
	}
	elsif ($self->{_update_surfaces}) {
	    $self->text( $self->text );
	    $self->{_update_surfaces} = 0;
	}

	if ( my $surfaces = $self->{surface} ) {

        $surfaces = [ $surfaces ] unless ref $surfaces eq 'ARRAY';
        my $linebreaks = 0;

        foreach my $i ( 0 .. $#{$surfaces}) {
            if (my $surface = $surfaces->[$i]) {
                $y += ($linebreaks * $surface->h);
                $linebreaks = 0;

                if ($self->{h_align} eq 'center' ) {
                    # $x = ($target->w / 2) - ($surface->w / 2);
                    $x -= $surface->w / 2;
                }
                elsif ($self->{h_align} eq 'right' ) {
                    # $x = $target->w - $surface->w;
                    $x -= $surface->w;
                }

                # blit the shadow
                if ($self->{shadow}) {
                    my $shadow = $self->{_shadow_surface}->[$i];
                    my $offset = $self->{shadow_offset};

                    SDL::Video::blit_surface(
                       $shadow, SDL::Rect->new(0,0,$shadow->w, $shadow->h),
                       $target, SDL::Rect->new($x + $offset, $y + $offset, 0, 0)
                    );
                }

                # blit the text
                SDL::Video::blit_surface(
                    $surface, SDL::Rect->new(0,0,$surface->w, $surface->h),
                    $target, SDL::Rect->new($x, $y, 0, 0)
                );
            }
            $linebreaks++;
        }

	}
	return;
}

1;