package LIMS::Controller::Local::Labels;

# provides methods for printing slide labels - requires entry in config/.local/worklists.yml

use base 'LIMS::Base';
use Data::Dumper;
$Data::Dumper::Terse = 1;

use IO::Socket;

use Moose;
with (
	'LIMS::Controller::Roles::Misc',
	'LIMS::Controller::Roles::ULISA',
	'LIMS::Controller::Roles::DataMap',
);

has labels => (
	is  => 'ro',
	isa => 'ArrayRef[HashRef]',
	default => sub { [] },
	lazy => 1,
	traits => ['Array'],
	handles  => {
        add_to_labels => 'push',
        count_labels  => 'count',
    },
);

has formats => (
    is      => 'ro',
    isa     => 'HashRef',
    builder => '_build_formats',
    lazy    => 1,
    traits  => ['Hash'],
    handles => {
        get_format    => 'get',
        format_exists => 'exists'
      },
);

sub _build_formats {
    return {
        #default format
        label => { fields => [qw/labno name label/], no_of_labels => 1 },

        cell_selection_worklist => {
            fields       => [qw/labno lastname sampletype labtest/],
            no_of_labels => 5
        }
    };
}

__PACKAGE__->meta->make_immutable(inline_constructor => 0);


# ------------------------------------------------------------------------------
sub default : StartRunmode {
	my $self = shift; $self->_debug_path($self->get_current_runmode);

	# get label_type from query param, or return to start (in case user cleared menu):
	my $label_type = $self->query->param('label_type')
	|| return $self->redirect( $self->query->url . '/worklist' );

	# check method exists or return error:
	unless ( UNIVERSAL::can($self, $label_type) ) {
		return $self->error( qq!no method  "$label_type" found at !
		. $self->get_current_runmode );
	}

	# forward to required method to generate data for template:
	my $data = $self->forward($label_type);
    $self->tt_params( labels => $data );

	return $self->render_view('worklist/local/labels/'.$label_type.'.tt', {});
}

# ------------------------------------------------------------------------------
sub histology_staining : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);

	# get lab_test details (H&E and H&E/Giemsa):

    my @lab_test_ids = map {
        $self->get_lab_test_for_unique_test_name($_)->id;
    } qw( haematoxylin_eosin h_and_e_giemsa ); # warn Dumper \@lab_test;

    # have to die as default() caller doesn't handle err rtn:
	@lab_test_ids == 2 || die 'expected test_names either not found, or not unique';

	{ # get requests where test status is not complete:
        my %args = ( # don't supply status_option_id, forces model to load id <> 2
			lab_test_id => \@lab_test_ids,
			fetch_only  => [ 'request' ], # don't need all cols from model
		);

		my $data = $self->model('WorkList')->get_outstanding_investigations(\%args);

		return $data;
	}
}

# ------------------------------------------------------------------------------
sub immunohistology_panels : Runmode {
	my $self = shift; $self->_debug_path($self->get_current_runmode);
    return $self->forward('_ihc_tests'); # don't restrict on status option
}

# ------------------------------------------------------------------------------
sub dako_link : Runmode {
	my $self = shift; $self->_debug_path($self->get_current_runmode);
    return $self->forward('_ihc_tests', { status_option => 'new' });
}

# ------------------------------------------------------------------------------
sub do_ulisa : Runmode {
	my $self = shift; $self->_debug_path($self->get_current_runmode);

	my @request_ids = $self->query->param('request_id'); # warn Dumper \@request_ids;

	# if ! @request_ids, don't return via rm or may get loop if arrived via timeout:
	unless (@request_ids) {
		return $self->redirect(
			$self->query->url . '/local_labels?label_type=dako_link'
		);
	}

	my $yaml = $self->get_yaml_file('print_labels')
		|| return $self->error('no config file found'); # warn Dumper $yaml;
	my $config = $yaml->{dako_link};

	# get immunohistology panel lab-tests:
	my $panels = $self->_get_immunohistology_panel_lab_tests(); # warn Dumper $panels;

	# some tests don't require labels:
	my $skip_tests = $config->{skip_tests}; # warn Dumper $skip_tests;

	# get map of request_id => test_name(s):
	my $request_lab_test_map = $self->_get_request_test_name_map(\@request_ids);

	# just need request, investigation_name(s) & patient data on request_ids:
	for my $id (@request_ids) { # don't sort - need original submission order
		my $o = $self->model('Request')->get_patient_and_request_data($id);
            # $self->debug( [$o->request_number, $o->year] );
		my %d = (
			request => $o->as_tree(max_depth => 0), # only want base object data
			patient => $o->patient_case->patient->as_tree, # don't need dt object
		); # warn Dumper \%d;

		# get list of tests/panels for this request:
		my $investigations = $request_lab_test_map->{$id}; # warn Dumper $investigations;
		# for each test/panel, calculate how many labels of each type required:

		TEST:
		for my $ref (@$investigations) { # warn Dumper $ref; # lab_test href
            my $test_name = $ref->{test_name};
			next TEST if grep $test_name eq $_, @$skip_tests; # skip unwanted tests

            if ( $ref->{test_type} eq 'test' ) {
                push @{ $d{lab_test} }, $ref;
            } # if test_type = panel, get list of individual lab-tests (AoH):
            elsif ( my $panel_lab_test = $panels->{$test_name} ) {
                push @{ $d{lab_test} }, $_ for @$panel_lab_test;
            } # not test, and can't find panel:
            else { die "no lab-tests found for $test_name panel" }
		} # warn Dumper \%d;
        # add 1 negative label (ficticious lab_test_id = 000): # discontinued 01/2018
        # push @{ $d{lab_test} }, { test_id => '000', test_name => 'NEG' };

        my $rtn = $self->send_xml(\%d); # C::R::ULISA
        # return error if method rtn value:
        return $self->error($rtn) if $rtn;
	}

	# set flash message:
	$self->flash( info => $self->messages('worklist')->{dako_link_ok} );

	{ # make hash of request_id's for template:
		my %done = map { $_ => 1 } @request_ids;
		$self->tt_params( done => \%done );
    }
    return $self->forward('default');
}

# ------------------------------------------------------------------------------
sub dna_quantification : Runmode {
    return shift->_get_quantification_data('dna_quantification');
}

# ------------------------------------------------------------------------------
sub rna_quantification : Runmode {
    return shift->_get_quantification_data('rna_quantification');
}

# ------------------------------------------------------------------------------
sub _get_quantification_data {
	my ($self, $test_name) = @_; $self->_debug_path($self->get_current_runmode);

    my $lab_test_id = $self->get_lab_test_for_unique_test_name($test_name)->id;

	# get requests where test status is not complete:
    my %args = ( # don't supply status_option_id, forces model to load id <> 2
		lab_test_id => $lab_test_id,
		fetch_only  => [ 'request' ], # don't need all cols from model
	);
	my $data = $self->model('WorkList')->get_outstanding_investigations(\%args);
	return $data;
}

# ------------------------------------------------------------------------------
sub print_lab_section_labels : Runmode {
	my $self = shift; $self->_debug_path($self->get_current_runmode);

	my @request_ids = $self->query->param('request_id'); # warn Dumper \@request_ids;

    my $specimen_map = $self->specimen_map(\@request_ids); # warn Dumper $specimen_map;

	for my $id (@request_ids) {
		my $o = $self->model('Request')->get_patient_and_request_data($id);
            # $self->debug( [$o->request_number, $o->year] );
		my %d = (
			request => $o->as_tree(max_depth => 0), # only want base object data
			patient => $o->patient_case->patient->as_tree, # don't need dt object
		);

		my $specimens = $specimen_map->{$id}->{sample_code}; # arrayref
		for my $sample_code (@$specimens) {
            my %h = ( data => \%d, label => $sample_code );
			my $label = $self->_format_label_data(\%h);
			$self->add_to_labels($label);
		}
	}
    # do print_labels function:
    my $rtn = $self->_do_print_labels();
	# return error if method rtn value:
    return $self->error($rtn) if $rtn;

	# set flash message:
	$self->flash( info => $self->messages('worklist')->{print_labels_ok} );

    # store request_ids in session:
    $self->session->param( lab_section_labels => \@request_ids );

	my $addr = $self->query->param('query_string'); # warn $addr;
	return $self->redirect( $addr );
}

# ------------------------------------------------------------------------------
sub print_histology_panel_labels : Runmode {
	my $self = shift; $self->_debug_path($self->get_current_runmode);

	my @request_ids = $self->query->param('request_id'); # warn Dumper \@request_ids;

	# if ! @request_ids, don't return via rm or may get loop if arrived via timeout:
	unless (@request_ids) {
		return $self->redirect(
			$self->query->url . '/local_labels?label_type=immunohistology_panels'
		);
	}

	my $yaml = $self->get_yaml_file('print_labels')
		|| return $self->error('no config file found'); # warn Dumper $yaml;
	my $config = $yaml->{immunohistology_panels};

	# get immunohistology panel lab-tests:
	my $panels = $self->_get_immunohistology_panel_lab_tests(); # warn Dumper $panels;

	# some tests don't require labels:
	my $skip_tests = $config->{skip_tests};

	# get map of request_id => test_name(s):
	my $request_lab_test_map = $self->_get_request_test_name_map(\@request_ids);

	# just need request, investigation_name(s) & patient data on request_ids:
	for my $id (@request_ids) { # don't sort - need original submission order
		my $o = $self->model('Request')->get_patient_and_request_data($id);
            # $self->debug( [$o->request_number, $o->year] );
		my %d = (
			request => $o->as_tree(max_depth => 0), # only want base object data
			patient => $o->patient_case->patient->as_tree, # don't need dt object
		);

		# get list of tests/panels for this request:
		my $investigations = $request_lab_test_map->{$id}; # warn Dumper $investigations;

		my $blank_label = do {
            my %h = ( data => \%d, label => undef ); # don't need a title
            $self->_format_label_data(\%h);
        };

		# for each test/panel, calculate how many labels of each type required:
		TEST:
		for my $ref (@$investigations) {
            my $field_label = $ref->{field_label};
            my $test_name   = $ref->{test_name};
			next TEST if grep $test_name eq $_, @$skip_tests; # skip unwanted tests

            if ( $ref->{test_type} eq 'test' ) {
                my %h = ( data => \%d, label => $field_label ); # warn $field_label;
				my $label = $self->_format_label_data(\%h);
				$self->add_to_labels($label);
                if ( my $n = $config->{test_blank_labels} ) { # add blanks:
                    $self->add_to_labels($blank_label) for (1 .. $n);
                }                
            }
            # if test_name is in panel_lab_test (ie it's a panel), add 1 label
			# for each label_list entry:
            elsif ( my $panel_lab_test = $panels->{$test_name} ) {
                for my $t (@$panel_lab_test) { # warn Dumper $t; # lab-test href
                    my %h = ( data => \%d, label => $t->{field_label} );
					my $data = $self->_format_label_data(\%h);
					$self->add_to_labels($data);
				}
                { # add 1 negative:
                    my %h = ( data => \%d, label => 'NEG' );
                    my $neg = $self->_format_label_data(\%h);
                    $self->add_to_labels($neg);
                }
                if ( my $n = $config->{panel_blank_labels} ) { # add blank labels:
                    $self->add_to_labels($blank_label) for (1 .. $n);
                }
			}
            else { die "no lab-tests found for $test_name panel" }
		}
	}

    # do print_labels function:
    my $rtn = $self->_do_print_labels();
	# return error if method rtn value:
    return $self->error($rtn) if $rtn;

	# set flash message:
	$self->flash( info => $self->messages('worklist')->{print_labels_ok} );

	{ # make hash of request_id's for template:
		my %done = map { $_ => 1 } @request_ids;
		$self->tt_params( done => \%done );
	}

    return $self->forward('default');
}

# ------------------------------------------------------------------------------
sub print_histology_staining_labels : Runmode {
	my $self = shift; $self->_debug_path($self->get_current_runmode);

	my @request_ids = $self->query->param('request_id'); # warn Dumper \@request_ids;

	# if ! @request_ids, don't return via rm or may get loop if arrived via timeout:
	unless (@request_ids) {
		return $self->redirect(
			$self->query->url . '/local_labels?label_type=histology_staining'
		);
	}

	# get specimen_map for request_ids:
	my $specimen_map = $self->specimen_map(\@request_ids);

	my $yaml = $self->get_yaml_file('print_labels'); # warn Dumper $yaml;

	# get specimen_type defs from histology_staining section of yaml config:
	my $specimen_regex = $yaml->{histology_staining}->{specimen_types};
		# warn Dumper $specimen_regex;
	my $name_format = $yaml->{histology_staining}->{name_format}; # override default format

	# just need request, specimen & patient data on request_ids:
	for my $id (sort @request_ids) {
		my $o = $self->model('Request')->get_patient_and_request_data($id);

		my %d = (
			request => $o->as_tree(max_depth => 0), # only want base object data
			patient => $o->patient_case->patient->as_tree, # don't need dt object
		);

		my $specimen = $specimen_map->{$id}->{sample_code};

        # common fields for _format_label_data (label name added inside loop):
        my %label_data = ( data => \%d, name_format => $name_format );

		# for each request_specimen, calculate how many labels of each type required:
		while ( my ($sample_code_regex, $label_list) = each %$specimen_regex ) {
			SPECIMEN:
			for my $sample_code (@$specimen) { # warn $sample_code;
				next SPECIMEN unless $sample_code =~ /$sample_code_regex/;
				# send request/patient data, label name & name_format to _format_label_data():
				for my $lbl (@$label_list) { # warn Dumper $lbl;
                    $label_data{label} = $lbl; # update label name
                    my $data = $self->_format_label_data(\%label_data);
					$self->add_to_labels($data);
				}
			}
		}
	}

    # do print_labels function:
    my $rtn = $self->_do_print_labels();
	# return error if method rtn value:
    return $self->error($rtn) if $rtn;

	# set flash message:
	$self->flash( info => $self->messages('worklist')->{print_labels_ok} );

	{ # make hash of request_id's for template:
		my %done = map { $_ => 1 } @request_ids;
		$self->tt_params( done => \%done );
	}

    return $self->forward('default');
}

# ------------------------------------------------------------------------------
sub print_xna_extraction_labels : Runmode {
    my $self = shift;

    my $vars = $self->query->Vars(); # warn Dumper $vars;

    # retrieve data from session (param = xna_extraction_request_specimen):
    my $data = $self->session->param('xna_extraction_request_specimen');
        # warn Dumper $data;

    my %request_specimens; # create HoA of requests & specimens
	my @all_request_ids; # to hold req_id * in same order as $data *

    for (@$data) {
        my ($request_id, $specimen) = split '~';
		push @all_request_ids, $request_id;
        push @{ $request_specimens{$request_id} }, $specimen;
    } # warn Dumper \%request_specimens;

    # extract (unique) request_id's:
    my $request_ids = LIMS::Local::Utils::get_unique_elements(\@all_request_ids);

	for my $id (@$request_ids) { # warn Dumper $id;
		my $o = $self->model('Request')->get_patient_and_request_data($id);
            # $self->debug( [$o->request_number, $o->year] );
		my %d = (
			request => $o->as_tree(max_depth => 0), # only want base object data
			patient => $o->patient_case->patient->as_tree, # don't need dt object
		);

		my $specimens = $request_specimens{$id}; # array(ref) of sample codes
		for my $sample_code (@$specimens) {
            # add xna extraction type to specimen code for label description:
            my $description = sprintf '%s [%s]',
                $sample_code, uc $vars->{extraction_type};
            my %h = ( data => \%d, label => $description );
			my $label = $self->_format_label_data(\%h);
			$self->add_to_labels($label);
		}
	}
    # do print_labels function:
    my $rtn = $self->_do_print_labels();
	# return error if method rtn value:
    return $self->error($rtn) if $rtn;

	# set flash message:
	$self->flash( info => $self->messages('worklist')->{print_labels_ok} );

    my $arg_str = sprintf 'extraction_type=%s;function_name=%s;lab_section=%s;'
        . 'print_labels=1', lc $vars->{extraction_type}, $vars->{function_name},
        $vars->{lab_section}; # warn $arg_str;

	my $addr = join '?', '/local_worklist/xna_extraction_worksheet', $arg_str; # warn $addr;
    return $self->redirect( $self->query->url . $addr );
}

# ------------------------------------------------------------------------------
sub print_plasma_storage_labels : Runmode {
	my $self = shift; $self->_debug_path($self->get_current_runmode);

	my @data = $self->query->param('label_id'); # warn Dumper \@data;
    my $return_addr = '/local_worklist/plasma_storage_labels'; # in case of error
    my $messages    =  $self->messages('worklist');
    my @request_ids;
    for (@data) {
        my ($req_id, $specimen, $count) = split '~'; # ? don't need specimen
        unless ($req_id) { # one or more rows submitted with no #labels option
            $self->flash( error => $messages->{no_plasma_label_data} );
            return $self->redirect( $self->query->url . $return_addr );
        }
        next unless $count; # in case it's 0
        push @request_ids, $req_id;
		my $o = $self->model('Request')->get_patient_and_request_data($req_id);
            # $self->debug( [$o->request_number, $o->year] );
		my %d = (
			request => $o->as_tree(max_depth => 0), # only want base object data
			patient => $o->patient_case->patient->as_tree, # don't need dt object
		);
        { # these labels require trial number in place of patient name:
            # get trial id for request:
            my $trial_id =
                $self->model('ClinicalTrial')->get_request_trial($req_id); # warn $trial_id;
            if ( $trial_id ) { # get trial number for patient:
                my $patient_id = $o->patient_case->patient_id;
                my $trial_number = $self->model('ClinicalTrial')
                    ->get_trial_number($patient_id, $trial_id);
                if ( $trial_number ) { # replace patient last_name:
                    $d{patient}->{last_name} = $trial_number;
                }
            }
        }
    	my $label = do {
            my %h = ( data => \%d, label => 'Plasma' );
            $self->_format_label_data(\%h);
        };
		$self->add_to_labels($label) for 1 .. $count;
	}
    unless ( $self->count_labels ) {
        $self->flash( error => $messages->{no_labels_selected} );
        return $self->redirect( $self->query->url . $return_addr );
    }
    # do print_labels function:
    my $rtn = $self->_do_print_labels();
	# return error if method rtn value:
    return $self->error($rtn) if $rtn;

	# set flash message:
	$self->flash( info => $messages->{print_labels_ok} );

    my $arg_str = 'function_name=trial_plasma_storage';
	my $addr = join '?', '/local_worklist', $arg_str; # warn $addr;
    return $self->redirect( $self->query->url . $addr );
}

# ------------------------------------------------------------------------------
sub print_xna_quantification_labels : Runmode {
	my $self = shift; $self->_debug_path($self->get_current_runmode);

	my @request_ids = $self->query->param('request_id'); # warn Dumper \@request_ids;

	# if ! @request_ids, don't return via rm or may get loop if arrived via timeout:
	unless (@request_ids) {
		return $self->redirect(
			$self->query->url . '/local_labels?label_type=' .
                $self->query->param('label_type')
		);
	}

	# get specimen_map for request_ids:
	my $specimen_map = $self->specimen_map(\@request_ids); # warn Dumper $specimen_map;

	for my $id (@request_ids) {
		my $o = $self->model('Request')->get_patient_and_request_data($id);
            # $self->debug( [$o->request_number, $o->year] );
		my %d = (
			request => $o->as_tree(max_depth => 0), # only want base object data
			patient => $o->patient_case->patient->as_tree, # don't need dt object
		);

		my $specimens = $specimen_map->{$id}->{sample_code}; # array(ref) of sample codes
		for my $sample_code (@$specimens) {
            my %h = ( data => \%d, label => $sample_code );
			my $label = $self->_format_label_data(\%h);
			$self->add_to_labels($label);
		}
	}

    # do print_labels function:
    my $rtn = $self->_do_print_labels();
	# return error if method rtn value:
    return $self->error($rtn) if $rtn;

	# set flash message:
	$self->flash( info => $self->messages('worklist')->{print_labels_ok} );

	{ # make hash of request_id's for template:
		my %done = map { $_ => 1 } @request_ids;
		$self->tt_params( done => \%done );
	}

    return $self->forward('default');
}

# ------------------------------------------------------------------------------
sub print_cell_selection_labels {
	my $self = shift; $self->_debug_path($self->get_current_runmode);
    # 1st arg is caller (C::Local::Worklist) so we can set flash() on it for tt:
    my $calling_object = shift; # LIMS::Controller::Local::Worklist
	my $label_data     = shift;

    my %specimen_map;
    foreach my $pair ( @{ $label_data->{request_specimen} } ) { # warn Dumper $pair;
        my ($request_id, $specimen) = split '~', $pair; # eg 331105~PB
        push @{ $specimen_map{$request_id} } , $specimen;
    }

    foreach my $entry ( @{ $label_data->{requests} } ) { # RequestLabTestStatus object
        my $request  = $entry->request;
        my $patient  = $request->patient_case->patient;
        my $lab_test = $entry->lab_test;
        foreach my $specimen ( @{ $specimen_map{$request->id} } ) { # warn Dumper $specimen;
            my %d = (
                request  => $request->as_tree(max_depth => 0), # only want base object data
                patient  => $patient->as_tree, # don't need dt object
                lab_test => $lab_test->as_tree(max_depth => 0), # don't need dt object
            );
			my $label = $self->_format_cell_selection_label_data(\%d, $specimen);
			$self->add_to_labels($label);
        }
    }

    my %msg; # flash message for tt:
    if ( $self->count_labels ) { # we have some requests selected for printing
        # _do_print_labels returns undef on success, or error:
        if ( my $rtn = $self->_do_print_labels('cell_selection_worklist') ) {
            $msg{error} = $rtn;
        }
        else {
            $msg{info} = $self->messages('worklist')->{print_labels_ok};
        }
    }
    else { # no requests selected:
        $msg{warning} = $self->messages('worklist')->{no_selection};
    }
    # set flash message in caller (Controller::Local::Worklist) object:
    $calling_object->flash(%msg);
    return 0; # caller doesn't expect a return value
}

# ------------------------------------------------------------------------------
# shared by dako_link() & immunohistology_panels() to retrieve IHC tests:
sub _ihc_tests : Runmode {
    my $self = shift; $self->_debug_path($self->get_current_runmode);
    my $args = shift; # optional href, if need to restrict by status opt

	# get list of lab_tests for immunohistochemistry section:
	my $lab_tests = do {
        my %args = ( section_name => 'Immunohistochemistry' );
        $self->model('LabTest')->get_section_lab_tests(\%args);
    }; # warn Dumper $lab_tests;

	# some tests don't require labels:
	my $yaml = $self->get_yaml_file('print_labels'); # warn Dumper $yaml;
	my $skip_tests = $yaml->{immunohistology_panels}->{skip_tests};

    my @lab_test_ids = ();
    for my $t (@$lab_tests) {
        next if grep $t->test_name eq $_, @$skip_tests; # only skips tests, not panels
        push @lab_test_ids, $t->id;
    } # warn Dumper \@lab_test_ids;

	{ # get requests where test status is not complete:
		my %args = (
			lab_test_id      => \@lab_test_ids,
			fetch_only       => [ 'request' ], # don't need all cols from model
		);
        # if we need to restrict to particular status option:
        if ( my $status_option = $args->{status_option} ) {
            my $option_id = do {
                my $map = $self->lab_test_status_options_map('description');
                $map->{$status_option}->{id};
            };
            $args{status_option_id} = $option_id;
        }

		my $data = $self->model('WorkList')->get_outstanding_investigations(\%args);

        # get_outstanding_investigations() returns one row per test, need unique requests:
        my (@requests, %seen);
        for my $r (@$data) {
            my $id = $r->request->id;
            next if $seen{$id}++;
            push @requests, $r->request;
        } # warn Dumper \@requests;
		return \@requests;
	}
}

# ------------------------------------------------------------------------------
sub _do_print_labels {
    my $self = shift; $self->_debug_path();
    # default label is 3 fields (labno, name, label), or provide alternative:
    my $label_type = shift || 'label';
    die 'invalid label format' unless $self->format_exists($label_type);
    my $label_format = $self->get_format($label_type); # warn Dumper $label_format;

	my $labels = $self->labels; # warn Dumper $labels;

	my $peer_addr =
        $ENV{LOCALHOST_SOCKET} # print to a localhost socket for testing
            ? 'localhost'
            : $self->cfg('settings')->{label_printer_addr};

	$peer_addr || return 'no setting for label printer address found in config file';

    my $socket = new IO::Socket::INET(
        PeerAddr => $peer_addr,
        PeerPort => 9100,
        Proto 	 => 'tcp',
		Timeout  => 10,
    ) || return 'could not create socket: ' . $!;

    # need to shift 1st item from @data to format 1st label:
    my $first_label = shift @$labels; # warn Dumper $first_label;

    # check label has all fields for this format:
    if ( grep { ! exists $first_label->{$_} } @{ $label_format->{fields} } ) {
        die "label doesn't have all fields necessary for this format";
    }

    my %first_label_data =
      map { $_ => $first_label->{$_} } @{ $label_format->{fields} };

    $first_label_data{no_of_labels} =  $label_format->{no_of_labels};

	# process label.tt template:
	my $label_body = do {
        my $tt = "worklist/local/labels/${label_type}.tt";
		$self->tt_process($tt, \%first_label_data);
	};

	# send 1st label to printer:
	print $socket ${$label_body}; # deref

    # (R)eplace field params for rest of labels:
    for my $next_label (@$labels) { # warn Dumper $next_label;
		for my $field( @{ $label_format->{fields} } ) { # eg R LABEL;Giemsa R LABNO;H1/10 etc
			print $socket sprintf "R %s;%s\n", uc $field, $next_label->{$field};
		}
		print $socket "A $label_format->{no_of_labels}\n"; # print 1 label each
    }

    close $socket;

	return 0; # as caller expects only errors returned
}

# ------------------------------------------------------------------------------
sub _format_label_data {
    my ($self, $ref) = @_; $self->_debug_path();

    my $name_format = $ref->{name_format};    # if initial required on label (optional)
    my $label       = $ref->{label} || '';    # warn $label; # can be blank for controls
    my $data        = $ref->{data};           # warn Dumper $data; # hashref

	# need lab_no, patient name & label name:
    my $name = ucfirst $data->{patient}->{last_name};    # truncate if too long
    if (defined $name_format and $name_format eq 'lastname_firstinitial') {
        ( my $initial = $data->{patient}->{first_name} ) =~ s/^(\w).*/ \U$1/;
        $name .= $initial;
    }
	my $labno = sprintf 'H%s/%s',
		$data->{request}{request_number}, $data->{request}{year} - 2000;

	my %data = (
		name  => $name,
		labno => $labno,
		label => $label,
	);

	return \%data;
}

# ------------------------------------------------------------------------------
sub _format_cell_selection_label_data {
	my $self  = shift; $self->_debug_path();
	my $data  = shift; # warn Dumper $data; # hashref
	my $label = shift || ''; # warn $label; # can be blank for controls

	# need lab_no, patient name & label name:
	my $lastname  = ucfirst $data->{patient}->{last_name}; # truncate if too long
	my $labno = sprintf 'H%s/%s',
		$data->{request}{request_number}, $data->{request}{year} - 2000;
    my $test_name = $data->{lab_test}{field_label};

    my %label_data = (
        lastname => $lastname,
        labno    => $labno,
        labtest    => $test_name,
        sampletype => $label
    );

	return \%label_data;
}
# ------------------------------------------------------------------------------
sub _get_request_test_name_map {
	my ($self, $request_ids) = @_; $self->_debug_path();

	my %args = (
		section_name => 'Immunohistochemistry',
		request_id   => $request_ids,
        description  => { ne => 'complete' },
	);

	my $o = $self->model('LabTest')
		->get_request_lab_tests_status_for_section(\%args);

	my %map;
	for my $data (@$o) {
		my $request_id    = $data->request_id;
        my $lab_test_data = $data->lab_test->as_tree(max_depth => 0);
        push @{ $map{$request_id} }, $lab_test_data;
	} # warn Dumper \%map;
	return \%map;
}

# ------------------------------------------------------------------------------
sub _get_immunohistology_panel_lab_tests {
    my $self = shift; $self->_debug_path();

    my $o = $self->model('LabTest')->get_panel_lab_tests('immunohistochemistry');

    # need HoA:
    my %panels = ();
    for (@$o) {
		next unless $_->lab_test->is_active eq 'yes';
        my $panel_name = $_->panel_test->test_name;
        push @{ $panels{$panel_name} }, $_->lab_test->as_tree;
    } # warn Dumper \%panels;
    return \%panels;
}

1;
