#
# This file is part of Curses-Toolkit
#
# This software is copyright (c) 2011 by Damien "dams" Krotkine.
#
# This is free software; you can redistribute it and/or modify it under
# the same terms as the Perl 5 programming language system itself.
#
use warnings;
use strict;

package Curses::Toolkit::Object::Coordinates;
BEGIN {
  $Curses::Toolkit::Object::Coordinates::VERSION = '0.206';
}
# ABSTRACT: simple coordinates class

# We don't use Moose for speed purpose

use Params::Validate qw(SCALAR ARRAYREF HASHREF CODEREF GLOB GLOBREF SCALARREF HANDLE BOOLEAN UNDEF validate validate_pos);

use parent qw(Curses::Toolkit::Object);

use overload
    '+'  => '_clone_add',
    '-'  => '_clone_subtract',
    '""' => '_stringify',
    '==' => '_equals';


# -- attributes


# -- constructor, builder & initializer


# called before object is built, to normalize the arguments.
sub new {
    my $class  = shift;

    my %params;

    if ( ref($_[0]) && $_[0]->isa(__PACKAGE__) ) {
        # case: Coordinates->new( $clone );
        my $c    = $_[0];
        %params = (
            x1 => $c->{x1}, y1 => $c->{y1},
            x2 => $c->{x2}, y2 => $c->{y2},
            normalize => $c->{normalize},
        );
    } else {
        %params = @_;
        if ( exists $params{width} || exists $params{height} ) {
            # case: width and height arguments
            %params = validate( @_,
                {   x1        => { type => SCALAR }, y1     => { type => SCALAR },
                    width     => { type => SCALAR }, height => { type => SCALAR },
                    normalize => { type => BOOLEAN, default => 1 },
                }
            );
            $params{x2} = $params{x1} + $params{width};
            $params{y2} = $params{y1} + $params{height};
        } else {
            %params = validate( @_,
                {   x1 => { type => SCALAR|CODEREF }, y1 => { type => SCALAR|CODEREF },
                    x2 => { type => SCALAR|CODEREF }, y2 => { type => SCALAR|CODEREF },
                    normalize => { type => BOOLEAN, default => 1 },
                }
            );
        }

        foreach (qw( x1 x2 y1 y2 )) {
            ref $params{$_} eq 'CODE'
              or $params{$_} = int($params{$_});
        }
    }
    my $self = bless \%params, $class;
    $self->_normalize();
    return $self;
}


sub new_zero {
    my ($class) = @_;
    return $class->new(
        x1 => 0, y1 => 0,
        x2 => 0, y2 => 0
    );
}



sub clone {
    my ($self) = @_;
    return ref($self)->new($self);
}

sub get_x1 { my $v = shift->{x1}; ref $v eq 'CODE' ? $v->() : $v }
sub get_y1 { my $v = shift->{y1}; ref $v eq 'CODE' ? $v->() : $v }
sub get_x2 { my $v = shift->{x2}; ref $v eq 'CODE' ? $v->() : $v }
sub get_y2 { my $v = shift->{y2}; ref $v eq 'CODE' ? $v->() : $v }
sub get_normalize { shift->{normalize} }

# -- public methods


sub set {
    my $self   = shift;

    # checks on params
    my %params = validate(
        @_,
        {   x1 => { type => SCALAR | CODEREF, optional => 1 }, y1 => { type => SCALAR | CODEREF, optional => 1 },
            x2 => { type => SCALAR | CODEREF, optional => 1 }, y2 => { type => SCALAR | CODEREF, optional => 1 },
            normalize => { type => BOOLEAN, default => 1 },
        }
    );
    keys %params or die "One of (x1, y1, x2, y2, normalize) argument must be passed";

    # set the new coords
    foreach my $k ( keys %params ) {
        $self->{$k} = $params{$k};
    }
    $self->_normalize();
    return $self;
}



sub width {
    my ($self) = @_;
    return $self->get_x2() - $self->get_x1();
}



sub height {
    my ($self) = @_;
    return $self->get_y2() - $self->get_y1();
}



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

    # FIXME: callbacks loose their coderef status

    if ( !ref $c ) {
        # argument is a constant
        @{$self}{qw(x1 y1 x2 y2)} = (
            $self->get_x1 + $c, $self->get_y1 + $c,
            $self->get_x2 + $c, $self->get_y2 + $c,
        );

    } elsif ( ref $c eq __PACKAGE__ ) {
        # argument is a coordinate object
        @{$self}{qw(x1 x2 y1 y2)} = (
            $self->get_x1 + $c->get_x1, $self->get_x2 + $c->get_x2,
            $self->get_y1 + $c->get_y1, $self->get_y2 + $c->get_y2,
        );

    } elsif ( ref $c eq 'HASH' ) {
        # argument is a hash
        while ( my ( $k, $v ) = each %$c ) {
            my $meth = "get_$k";
            $self->{$k} = $self->$meth + $v;
        }

    } else {
        die "Argument type ('" . ref($c) . "') is not supported in Coordinate addition";
    }
    $self->_normalize();
    return $self;
}



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

    # FIXME: callbacks loose their coderef status

    if ( !ref $c ) {
        # argument is a constant
        @{$self}{qw(x1 y1 x2 y2)} = (
            $self->get_x1 - $c, $self->get_y1 - $c,
            $self->get_x2 - $c, $self->get_y2 - $c,
        );

    } elsif ( ref $c eq __PACKAGE__ ) {
        # argument is a coordinate object
        @{$self}{qw(x1 x2 y1 y2)} = (
            $self->get_x1 - $c->get_x1, $self->get_x2 - $c->get_x2,
            $self->get_y1 - $c->get_y1, $self->get_y2 - $c->get_y2,
        );

    } elsif ( ref $c eq 'HASH' ) {

        # argument is a hash
        while ( my ( $k, $v ) = each %$c ) {
            my $meth = "get_$k";
            $self->{$k} = $self->$meth - $v;
        }

    } else {
        die "Argument type ('" . ref($c) . "') is not supported in Coordinate addition";
    }
    $self->_normalize();
    return $self;
}



sub restrict_to {
    my $self = shift;
    my ($c) = validate_pos( @_, { isa => 'Curses::Toolkit::Object::Coordinates' } );

    $self->get_x1 < $c->get_x1 and $self->{x1} = $c->{x1};
    $self->get_x1 > $c->get_x2 and $self->{x1} = $c->{x2};

    $self->get_x2 > $c->get_x2 and $self->{x2} = $c->{x2};
    $self->get_x2 < $c->get_x1 and $self->{x2} = $c->{x1};

    $self->get_y1 < $c->get_y1 and $self->{y1} = $c->{y1};
    $self->get_y1 > $c->get_y2 and $self->{y1} = $c->{y2};

    $self->get_y2 > $c->get_y2 and $self->{y2} = $c->{y2};
    $self->get_y2 < $c->get_y1 and $self->{y2} = $c->{y1};

    $self->_normalize();

    return $self;
}



sub grow_to {
    my $self = shift;
    my ($c) = validate_pos( @_, { isa => 'Curses::Toolkit::Object::Coordinates' } );

    $self->get_x1 > $c->get_x1 and $self->{x1} = $c->{x1};
    $self->get_x2 < $c->get_x2 and $self->{x2} = $c->{x2};

    $self->get_y1 > $c->get_y1 and $self->{y1} = $c->{y1};
    $self->get_y2 < $c->get_y2 and $self->{y2} = $c->{y2};

    $self->_normalize();

    return $self;
}



sub translate {
    my $self = shift;

    # FIXME: callbacks loose their coderef status

    my %params = validate(
        @_,
        {   x => { type => SCALAR, optional => 1 },
            y => { type => SCALAR, optional => 1 },
        }
    );
    defined $params{x} || $params{y}
        or die "needs at least one of 'x' or 'y'";

    if ( defined $params{x} ) {
        $self->{x1} += $params{x};
        $self->{x2} += $params{x};
    }
    if ( defined $params{y} ) {
        $self->{y1} += $params{y};
        $self->{y2} += $params{y};
    }

    $self->_normalize();

    return $self;
}



sub translate_up {
    my ( $self, $value ) = @_;
    return $self->translate( y => -abs $value );
}



sub translate_down {
    my ( $self, $value ) = @_;
    return $self->translate( y => abs $value );
}



sub translate_left {
    my ( $self, $value ) = @_;
    return $self->translate( x => -abs $value );
}



sub translate_right {
    my ( $self, $value ) = @_;
    return $self->translate( x => abs $value );
}



sub contains {
    my $self = shift;
    my ($c) = validate_pos( @_, { isa => 'Curses::Toolkit::Object::Coordinates' } );
    return
           $self->get_x1() <= $c->get_x1()
        && $self->get_y1() <= $c->get_y1()
        && $self->get_x2() >= $c->get_x2()
        && $self->get_y2() >= $c->get_y2();
}



sub is_inside {
    my $self = shift;
    my ($c) = validate_pos( @_, { isa => 'Curses::Toolkit::Object::Coordinates' } );
    return $c->contains($self);
}



sub is_in_widget {
    my ( $self, $widget ) = @_;
    my $w_coord = $widget->get_coordinates();
    return
           $w_coord->get_x1 <= $self->get_x1
        && $w_coord->get_x2 >= $self->get_x2
        && $w_coord->get_y1 <= $self->get_y1
        && $w_coord->get_y2 >= $self->get_y2;
}


# -- private methods

#
# my $c3 = $c1->_clone_add( $c2 );
# my $c3 = $c1 + $c2;   # overloaded
#
# clone a coord and add another to the new object.
#
sub _clone_add {
    my $self  = shift;
    my $clone = $self->clone();
    $clone->add(@_);
    return $clone;
}


#
# my $c3 = $c1->_clone_subtract( $c2 );
# my $c3 = $c1 - $c2;   # overloaded
#
# clone a coord and subtract another to the new object.
#
sub _clone_subtract {
    my $self  = shift;
    my $clone = $self->clone();
    $clone->subtract(@_);
    return $clone;
}


#
# my $bool = $c1->_equals( $c2 );
# my $bool = $c1 == $c2;    # overloaded
#
# return true if both $c1 and $c2 point to the same coords. they can
# point to different objects, though.
#
sub _equals {
    my ( $c1, $c2 ) = @_;
    return
           $c1->get_x1 == $c2->get_x1
        && $c1->get_y1 == $c2->get_y1
        && $c1->get_x2 == $c2->get_x2
        && $c1->get_y2 == $c2->get_y2;
}


#
# my $str = $self->_stringify;
# my $str = "$self";     # overloaded
#
# return the string 'WxH+XxY' with:
#   W = width
#   H = height,
#   X = top left x coord
#   Y = top left y coord
#
sub _stringify {
    my ($self) = @_;
    return $self->width . 'x' . $self->height . '+' . $self->get_x1 . 'x' . $self->get_y1;
}


#
# $self->_normalize;
#
# make sure the coordinate is positive. in effect:
#  - swap x1 and x2 to make sure x1 <= x2
#  - swap y1 and y2 to make sure y1 <= y2
#
sub _normalize {
    my ($self) = @_;
    $self->get_normalize() or return;
    $self->get_x1() <= $self->get_x2() or ( $self->{x1}, $self->{x2} ) = ( $self->{x2}, $self->{x1} );
    $self->get_y1() <= $self->get_y2() or ( $self->{y1}, $self->{y2} ) = ( $self->{y2}, $self->{y1} );
    return;
}

1;


=pod

=head1 NAME

Curses::Toolkit::Object::Coordinates - simple coordinates class

=head1 VERSION

version 0.206

=head1 DESCRIPTION

Trivial class to hold 2 points.

+ and - are properly overloaded.

=head1 ATTRIBUTES

=head2 x1

Top left x coordinate of the object. Can be either an integer or a
coderef.

=head2 y1

Top left y coordinate of the object. Can be either an integer or a
coderef.

=head2 x2

Bottom right x coordinate of the object. Can be either an integer or a
coderef.

=head2 y2

Bottom right y coordinate of the object. Can be either an integer or a
coderef.

=head2 normalize

A boolean to swap coordinates to make sure x1 < x2 and y1 < y2 if set to
true (default). If set to false, the coordinates will be untouched.

=head1 METHODS

=head2 new

    my $coord = CTO::Coordinates->new( @attributes );
    my $coord = CTO::Coordinates->new( $coord_to_clone );
    my $coord = CTO::Coordinates->new( x1=>$x1, y1=>$y1, width=>$w, height=>$h );

Constructor for the class. Acceps various kind of attributes.

  input  : x1 : top left x
           y1 : top left y
           x2 : right bottom x
           y2 : right bottom y
           [ normalize ] : optional
    OR
  input  : x1 : top left x
           y1 : top left y
           width : width
           height : height
           [ normalize ] : optional
    OR
  input  : x1 : sub { ... } # returns top left x
           y1 : sub { ... } # returns top left y
           x2 : sub { ... } # returns right bottom x
           y2 : sub { ... } # returns right bottom y
           [ normalize ] : optional
    OR
  input  : a Curses::Toolkit::Object::Coordinates object

  output : a Curses::Toolkit::Object::Coordinates object

=head2 new_zero

    my $coord = CTO::Coordinates->new_zero;

Creates a new coordinates with all individual coords set to zero.

  input  : none
  output : a Curses::Toolkit::Object::Coordinates object

=head2 clone

    my $c2 = $c1->clone;

Clone a coordinates object.

  input  : none
  output : a Curses::Toolkit::Object::Coordinates object

=head2 set

    $coord->set( x1=>$x1, y1=>$y1, x2=>$x2, y2=>$y2, normalize => 1 );

Set attributes of the coordinate object.

  input  : one or more of x1, x2, y1, y2
  output : the coordinate object

=head2 width

    my $width = $coord->width;

Returns the width represented by the coordinate object.

=head2 height

    my $height = $coord->height;

Returns the height represented by the coordinate object.

=head2 add

    $coord->add( $const );
    $coord->add( $coord_to_add );
    $coord->add( { x1=>$x1, y1=>$y1, x2=>$x2, y2=>$y2 } );

Add to the coordinate attributes of the object.

If the argument is a constant, it's added to all the components of the
coordinate.
If it's a Curses::Toolkit::Object::Coordinates, it's added side by side
If it's a hashref, it's added side by side

      input  : a CONSTANT
    OR
      input  : a Curses::Toolkit::Object::Coordinates
    OR
      input  : a HASHREF of 'x1', 'x2', 'y1', 'y2'

    output : the Curses::Toolkit::Object::Coordinates object

=head2 subtract

    $coord->subtract( $const );
    $coord->subtract( $coord_to_add );
    $coord->subtract( { x1=>$x1, y1=>$y1, x2=>$x2, y2=>$y2 } );

Substract from the coordinate (also overloads '-').

If the argument is a constant, it's subtracted from all the components of the
coordinate.
If it's a Curses::Toolkit::Object::Coordinates, it's subtracted side by side
If it's a hashref, it's subtracted side by side

      input  : a CONSTANT
    OR
      input  : a Curses::Toolkit::Object::Coordinates
    OR
      input  : a HASHREF of 'x1', 'x2', 'y1', 'y2'

    output : the Curses::Toolkit::Object::Coordinates object

=head2 restrict_to

    $coord->restrict_to( $coord_to_fit_into );

Force the coordinate to be inside the passed coordinate.

  input  : a Curses::Toolkit::Object::Coordinates object
  output : the object

=head2 grow_to

    $coord->grow_to( $coord_to_match );

Force the coordinate to be at least as big as the passed coordinate.

  input  : a Curses::Toolkit::Object::Coordinates object
  output : the object

=head2 translate

    $coord->translate( x=>$x, y=>$y );

Given a X value and a Y value, translates the coordinate accordingly

  input  : x : value to translate the coord horizontally
           y : value to translate the coord vertically
  output : the Curses::Toolkit::Object::Coordinates object

=head2 translate_up

    $coord->translate_up( $offset );

Given a value, translates the coordinate up (value sign is ignored)

  input  : value to translate the coord up
  output : the Curses::Toolkit::Object::Coordinates object

=head2 translate_down

    $coord->translate_down( $offset );

Given a value, translates the coordinate down (value sign is ignored)

  input  : value to translate the coord down
  output : the Curses::Toolkit::Object::Coordinates object

=head2 translate_left

    $coord->translate_left( $offset );

Given a value, translates the coordinate left (value sign is ignored)

  input  : value to translate the coord left
  output : the Curses::Toolkit::Object::Coordinates object

=head2 translate_right

    $coord->translate_right( $offset );

Given a value, translates the coordinate right (value sign is ignored)

  input  : value to translate the coord right
  output : the Curses::Toolkit::Object::Coordinates object

=head2 contains

    my $bool = $coord->contains( $coord_to_check );

Return true if the coordinates contains the given coordinates

  input  : a Curses::Toolkit::Object::Coordinates object : the coordinates
  output : true or false

=head2 is_inside

    my $bool = $coord->is_inside( $coord_to_check );

Return true if the coordinates is inside the given coordinates

  input  : a Curses::Toolkit::Object::Coordinates object : the coordinates
  output : true or false

=head2 is_in_widget

    my $bool = $coord->is_in_widget( $widget );

Return true if the coordinates is inside the given widget

  input  : Curses::Toolkit::Widget : the widget
  output : true or false

=head1 AUTHOR

Damien "dams" Krotkine

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2011 by Damien "dams" Krotkine.

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

=cut


__END__

