package Device::Modbus::Unit::Route;

use Carp;
use strict;
use warnings;
use v5.10;

sub new {
    my ($class, %args) = @_;
    
    my %valid_zone = (
        discrete_coils    => 'rw',
        discrete_inputs   => 'ro',
        input_registers   => 'ro',
        holding_registers => 'rw',
    );

    foreach my $field (qw/address quantity zone read_write routine/) {
        croak "Missing required arguments: $field"
            unless exists $args{$field};
    }
    croak "Invalid Modbus model type: zone '$args{zone}' does not exist"
        unless exists $valid_zone{$args{zone}};
    croak "Modbus zone '$args{zone}' is read-only"
        if $args{read_write} eq 'write' && $valid_zone{$args{zone}} eq 'ro';
    croak "Parameter read_write must be either 'read' or 'write'"
        unless $args{read_write} =~ /^read|write$/;
    croak "The routine for an address must be a code reference"
        unless ref $args{routine} && ref $args{routine} eq 'CODE';

    $args{route_tests} = _build_tests($args{address});
    $args{qty_tests}   = _build_tests($args{quantity});

    my $self = bless \%args, $class;
}

sub routine {
    my $self = shift;
    return $self->{routine};
}

# Receives a route string and converts it into an array reference of
# anonymous subroutines. Each subroutine will test if a given value
# matches a part of the route.    
sub _build_tests {
    my $route = shift;

    # Star matches always
    return [ sub { 1 } ] if $route =~ /\*/;

    $route    =~ s/\s+//g;
    my @atoms = split /,/, $route;
    my @tests;
    foreach my $atom (@atoms) {
        # Range test
        if ($atom =~ /^(\d+)\-(\d+)$/) {
            my ($min, $max) = ($1, $2);
            push @tests, sub { my $val = shift; return $val >= $min && $val <= $max; };
        }
        # Equality test
        else {
            push @tests, sub { return shift == $atom; };
        }
    }

    return \@tests;
}

# Tests an address
sub test_route {
    my ($self, $value) = @_;
    foreach my $test (@{$self->{route_tests}}) {
        return 1 if $test->($value);
    }
    return 0;
}

# Tests a quantity
sub test_quantity {
    my ($self, $value) = @_;
    foreach my $test (@{$self->{qty_tests}}) {
        return 1 if $test->($value);
    }
    return 0;
}

1;
