package ICC::Support::bern;

use strict;
use Carp;

our $VERSION = 0.41;

# revised 2018-09-04
#
# Copyright © 2004-2020 by William B. Birkett

# inherit from Shared
use parent qw(ICC::Shared);

# enable static variables
use feature 'state';

# create new bern object
# hash keys are: 'input', 'output', 'fit', 'fix_hl', 'fix_sh'
# default values for 'input' and 'output' are []
# parameters: ([ref_to_attribute_hash])
# returns: (ref_to_object)
sub new {

	# get object class
	my $class = shift();

	# create empty bern object
	my $self = [
		{},	# object header
		[],	# parameter array
		[],	# min/max t-values
		[]	# min/max i/o-values
	];

	# if one parameter, a hash reference
	if (@_ == 1 && ref($_[0]) eq 'HASH') {
		
		# set object contents from hash
		_new_from_hash($self, $_[0]);
		
	} elsif (@_) {
		
		# error
		croak('invalid parameter(s)');
		
	}

	# return blessed object
	return(bless($self, $class));

}

# create inverse object
# swaps the 'input' and 'output' arrays
# returns: (ref_to_object)
sub inv {

	# clone new object
	my $self = Storable::dclone(shift());

	# swap 'input' and 'output' arrays
	($self->[1][0], $self->[1][1]) = ($self->[1][1], $self->[1][0]);
	($self->[2][0], $self->[2][1]) = ($self->[2][1], $self->[2][0]);
	($self->[3][0], $self->[3][1]) = ($self->[3][1], $self->[3][0]);

	# return inverted object
	return($self);

}

# write bern object to ICC profile
# note: writes an equivalent 'curv' object
# parameters: (ref_to_parent_object, file_handle, ref_to_tag_table_entry)
sub write_fh {

	# verify 4 parameters
	(@_ == 4) or croak('wrong number of parameters');

	# write bern data to profile
	goto &_writeICCbern;

}

# get tag size (for writing to profile)
# note: writes an equivalent curv object
# returns: (tag_size)
sub size {

	# get parameters
	my ($self) = @_;

	# get count value (defaults to 4370)
	my $n = $self->[0]{'curv_points'} // 4370;

	# return size
	return(12 + $n * 2);

}

# compute curve function
# parameters: (input_value)
# returns: (output_value)
sub transform {

	# get parameters
	my ($self, $in) = @_;

	# verify input
	(Scalar::Util::looks_like_number($in)) or croak('invalid transform input');

	# compute intermediate value(s)
	my @t = _rev($self->[1][0], $self->[2][0], $self->[3][0], $in);

	# warn if multiple values
	(@t == 1) or carp('multiple transform values');

	# return first output value
	return(_fwd($self->[1][1], $t[0]));

}

# compute inverse curve function
# parameters: (input_value)
# returns: (output_value)
sub inverse {

	# get parameters
	my ($self, $in) = @_;

	# verify input
	(Scalar::Util::looks_like_number($in)) or croak('invalid transform input');

	# compute intermediate value(s)
	my @t = _rev($self->[1][1], $self->[2][1], $self->[3][1], $in);

	# warn if multiple values
	(@t == 1) or carp('multiple transform values');

	# return first output value
	return(_fwd($self->[1][0], $t[0]));

}

# compute curve derivative
# parameters: (input_value)
# returns: (derivative_value)
sub derivative {

	# get parameters
	my ($self, $in) = @_;

	# return forward derivative
	return(_derivative($self, 0, $in));

}

# compute parametric partial derivatives
# parameters: (input_value)
# returns: (partial_derivative_array)
sub parametric {

	# get parameters
	my ($self, $in) = @_;

	# return forward parametric partial derivatives
	return(_parametric($self, 0, $in));

}

# get/set header hash reference
# parameters: ([ref_to_new_hash])
# returns: (ref_to_hash)
sub header {

	# get object reference
	my $self = shift();

	# if there are parameters
	if (@_) {
		
		# if one parameter, a hash reference
		if (@_ == 1 && ref($_[0]) eq 'HASH') {
			
			# set header to copy of hash
			$self->[0] = Storable::dclone(shift());
			
		} else {
			
			# error
			croak('parameter must be a hash reference');
			
		}
		
	}

	# return reference
	return($self->[0]);

}

# get/set input array reference
# an array of two values is a range
# otherwise, it contains input values
# parameters: ([ref_to_array])
# returns: (ref_to_array)
sub input {

	# get object reference
	my $self = shift();

	# if one parameter supplied
	if (@_ == 1) {
		
		# verify 'input' vector (between 1 and 21 numeric values)
		(ICC::Shared::is_num_vector($_[0]) && 1 <= @{$_[0]} && 21 >= @{$_[0]}) or croak('invalid input values');
		
		# store output values
		$self->[1][0] = [@{$_[0]}];
		
		# check for min/max values
		_minmax($self, 0);
		
	} elsif (@_) {
		
		# error
		carp('too many parameters');
		
	}

	# return array reference
	return($self->[1][0]);

}

# get/set output array reference
# parameters: ([ref_to_array])
# returns: (ref_to_array)
sub output {

	# get object reference
	my $self = shift();

	# if one parameter supplied
	if (@_ == 1) {
		
		# verify 'output' vector (between 1 and 21 numeric values)
		(ICC::Shared::is_num_vector($_[0]) && 1 <= @{$_[0]} && 21 >= @{$_[0]}) or croak('invalid input values');
		
		# store output values
		$self->[1][1] = [@{$_[0]}];
		
		# check for min/max values
		_minmax($self, 1);
		
	} elsif (@_) {
		
		# error
		carp('too many parameters');
		
	}

	# return array reference
	return($self->[1][1]);

}

# normalize transform
# adjusts Bernstein coefficients linearly
# adjusts 'input' and 'output' by default
# hash keys: 'input', 'output'
# parameter: ([hash_ref])
# returns: (ref_to_object)
sub normalize {

	# get parameters
	my ($self, $hash) = @_;

	# local variables
	my ($m, $b, $val, $src, $x0, $x1, $y0, $y1, $f0, $f1, @minmax);

	# set hash key array
	my @key = qw(input output);

	# for ('input', 'output')
	for my $i (0, 1) {
		
		# undefine slope
		$m = undef;
		
		# if no parameter (default)
		if (! defined($hash)) {
			
			# if array has 2 or more coefficients
			if (@{$self->[1][$i]} > 1 && ($x0 = $self->[1][$i][0]) != ($x1 = $self->[1][$i][-1])) {
				
				# compute adjustment values
				$m = abs(1/($x0 - $x1));
				$b = -$m * ($x0 < $x1 ? $x0 : $x1);
				
				# set endpoints
				($y0, $y1) = $x0 < $x1 ? (0, 1) : (1, 0);
				
			}
			
		} else {
			
			# if hash contains key
			if (defined($val = $hash->{$key[$i]})) {
				
				# if array has 2 or more coefficients
				if (@{$self->[1][$i]} > 1 && $self->[1][$i][0] != $self->[1][$i][-1]) {
					
					# if hash value is an array reference
					if (ref($val) eq 'ARRAY') {
						
						# if two parameters
						if (@{$val} == 2) {
							
							# get parameters
							($y0, $y1) = @{$val};
							
							# get endpoints
							$x0 = $self->[1][$i][0];
							$x1 = $self->[1][$i][-1];
							
						# if three parameters
						} elsif (@{$val} == 3) {
							
							# get parameters
							($src, $y0, $y1) = @{$val};
							
							# if source is 'endpoints'
							if (! ref($src) && $src eq 'endpoints') {
								
								# get endpoints
								$x0 = $self->[1][$i][0];
								$x1 = $self->[1][$i][-1];
								
							# if source is 'minmax'
							} elsif (! ref($src) && $src eq 'minmax') {
								
								# get all possible min/max values
								@minmax = ($self->[1][$i][0], $self->[1][$i][-1], @{$self->[3][$i]});
								
								# get min/max values
								$x0 = List::Util::min(@minmax);
								$x1 = List::Util::max(@minmax);
								
							}
					
						# if four parameters
						} elsif (@{$val} == 4) {
							
							# get parameters
							($x0, $x1, $y0, $y1) = @{$val};
							
						}
						
						# if x/y values are numeric
						if ((4 == grep {Scalar::Util::looks_like_number($_)} ($x0, $x1, $y0, $y1)) && ($x0 != $x1)) {
							
							# compute slope
							$m = ($y0 - $y1)/($x0 - $x1);
							
							# compute offset
							$b = $y0 - $m * $x0;
							
						} else {
							
							# warning
							carp("invalid '$key[$i]' parameter(s)");
							
						}
					
					}
				
				} else {
					
					# warning
					carp("can't normalize '$key[$i]' coefficients");
					
				}
				
			}
		
		}
		
		# if slope is defined
		if (defined($m)) {
			
			# set endpoint flags
			$f0 = $self->[1][$i][0] == $x0;
			$f1 = $self->[1][$i][-1] == $x1;
			
			# adjust Bernstein coefficients (y = mx + b)
			@{$self->[1][$i]} = map {$m * $_ + $b} @{$self->[1][$i]};
			
			# set endpoints (to ensure exact values)
			$self->[1][$i][0] = $y0 if ($f0);
			$self->[1][$i][-1] = $y1 if ($f1);
			
		}
		
	}

	# re-calculate min/max values
	_minmax($self);

	# return
	return($self);

}

# check if monotonic
# returns min/max values
# returns t-values by default
# returns 'input' values if format is 'input'
# returns 'output' values if format is 'output'
# curve is monotonic if no min/max values
# parameters: ([format])
# returns: (array_of_values)
sub monotonic {

	# get object reference
	my ($self, $fmt) = @_;

	# local variables
	my (@s);

	# compute min/max values
	_minmax($self);

	# if format undefined
	if (! defined($fmt)) {
		
		# return sorted t-values ('input' and 'output')
		return(sort {$a <=> $b} (@{$self->[2][0]}, @{$self->[2][1]}));
		
	# if format 'input'
	} elsif ($fmt eq 'input') {
		
		# return input values
		return(map {_fwd($self->[1][0], $_)} sort {$a <=> $b} (@{$self->[2][0]}, @{$self->[2][1]}));
		
	# if format 'output'
	} elsif ($fmt eq 'output') {
		
		# return output values
		return(map {_fwd($self->[1][1], $_)} sort {$a <=> $b} (@{$self->[2][0]}, @{$self->[2][1]}));
		
	} else {
		
		# error
		croak('unsupported format for min/max values');
		
	}
	
}

# update internal object elements
# this method used primarily when optimizing
# call after changing 'input' or 'output' values
sub update {

	# recompute min/max values
	goto &_minmax;

}

# make table for profile objects
# assumes curve domain/range is (0 - 1)
# parameters: (number_of_table_entries, [direction])
# returns: (ref_to_table_array)
sub table {

	# get parameters
	my ($self, $n, $dir) = @_;

	# local variables
	my ($up, $table);

	# validate number of table entries
	($n == int($n) && $n >= 2) or carp('invalid number of table entries');

	# purify direction flag
	$dir = ($dir) ? 1 : 0;

	# array upper index
	$up = $n - 1;

	# for each table entry
	for my $i (0 .. $up) {
		
		# compute table value
		$table->[$i] = _transform($self, $dir, $i/$up);
		
	}

	# return table reference
	return($table);

}

# make equivalent 'curv' object
# assumes curve domain/range is (0 - 1)
# parameters: (number_of_table_entries, [direction])
# returns: (ref_to_curv_object)
sub curv {

	# return 'curv' object reference
	return(ICC::Profile::curv->new(table(@_)));

}

# print object contents to string
# format is an array structure
# parameter: ([format])
# returns: (string)
sub sdump {

	# get parameters
	my ($self, $p) = @_;

	# local variables
	my ($s, $fmt, $fmt2);

	# resolve parameter to an array reference
	$p = defined($p) ? ref($p) eq 'ARRAY' ? $p : [$p] : [];

	# get format string
	$fmt = defined($p->[0]) && ! ref($p->[0]) ? $p->[0] : 'undef';

	# set string to object ID
	$s = sprintf("'%s' object, (0x%x)\n", ref($self), $self);

	# if input_parameter_array defined
	if (defined($self->[1][0])) {
		
		# set format
		$fmt2 = join(', ', ('%.3f') x @{$self->[1][0]});
		
		# append parameter string
		$s .= sprintf("  input parameters  [$fmt2]\n", @{$self->[1][0]});
		
	}

	# if output_parameter_array defined
	if (defined($self->[1][1])) {
		
		# set format
		$fmt2 = join(', ', ('%.3f') x @{$self->[1][1]});
		
		# append parameter string
		$s .= sprintf("  output parameters [$fmt2]\n", @{$self->[1][1]});
		
	}

	# return
	return($s);

}

# find min/max values within the interval (0 - 1)
# used by '_rev' to determine solution regions
# called by the 'update' method
# i/o_index: 0 - input, 1 - output, undef - both
# parameters: (ref_to_object, [i/o_index])
sub _minmax {

	# get parameters
	my ($self, $io) = @_;

	# local variables
	my (@s, $t, $d);
	my ($par, $fwd, $fwd2, $drv);

	# for input and/or output curves
	for my $i (defined($io) ? ($io ? 1 : 0) : (0, 1)) {
		
		# initialize s-values array
		@s = ();
		
		# get ref to Bernstein parameters
		$par = $self->[1][$i];
		
		# if number of parameters > 2
		if (@{$par} > 2) {
			
			# compute forward differences
			$fwd = [map {$par->[$_] - $par->[$_ - 1]} (1 .. $#{$par})];
			
			# if forward differences have different signs (+/-)
			if (grep {($fwd->[0] < 0) ^ ($fwd->[$_] < 0)} (1 .. $#{$fwd})) {
				
				# compute second forward differences
				$fwd2 = [map {$fwd->[$_] - $fwd->[$_ - 1]} (1 .. $#{$fwd})];
				
				# compute derivative values over range (0 - 1)
				$drv = [map {$#{$par} * _fwd($fwd, $_/(4 * $#{$par}))} (0 .. 4 * $#{$par})];
				
				# for each pair of derivatives
				for my $j (1 .. $#{$drv}) {
					
					# if derivatives have different signs (+/-)
					if (($drv->[$j - 1] < 0) ^ ($drv->[$j] < 0)) {
						
						# estimate root from the derivative values
						$t = ($j * $drv->[$j - 1] - ($j - 1) * $drv->[$j])/(($drv->[$j - 1] - $drv->[$j]) * 4 * $#{$par});
						
						# loop until derivative approaches 0
						while (abs($d = $#{$par} * _fwd($fwd, $t)) > 1E-9) {
							
							# adjust root using Newton's method
							$t -= $d/($#{$fwd} * $#{$par} * _fwd($fwd2, $t));
							
						}
						
						# push root on t-value array
						push(@s, $t) if ($t > 0 && $t < 1);
						
					}
					
				}
				
			}
			
		}
		
		# warn if root values were found
		print "curve $i is not monotonic\n" if (@s && ! defined($self->[2][$i]));
		
		# save root t-values
		$self->[2][$i] = [@s];
		
		# save root i/o-values
		$self->[3][$i] = [map {_fwd($par, $_)} @s];
		
	}
	
}

# combined transform
# nominal domain (0 - 1)
# parameters: (ref_to_object, direction, input_value)
# returns: (output_value)
sub _transform {

	# get parameters
	my ($self, $dir, $in) = @_;

	# local variables
	my ($p0, $p1, $p2, $p3, @t);

	# verify input
	(Scalar::Util::looks_like_number($in)) or croak('invalid transform input');

	# get parameter arrays
	$p0 = $self->[1][$dir];
	$p1 = $self->[2][$dir];
	$p2 = $self->[3][$dir];
	$p3 = $self->[1][1 - $dir];

	# compute intermediate value(s)
	@t = _rev($p0, $p1, $p2, $in);

	# warn if multiple values
	(@t == 1) or carp('multiple transform values');

	# return first output value
	return(_fwd($p3, $t[0]));

}

# combined derivative
# nominal domain (0 - 1)
# parameters: (ref_to_object, direction, input_value)
# returns: (derivative_value)
sub _derivative {

	# get parameters
	my ($self, $dir, $in) = @_;

	# local variables
	my ($p0, $p1, $p2, $p3, @t);
	my ($di, $do);

	# verify input
	(Scalar::Util::looks_like_number($in)) or croak('invalid transform input');

	# get parameter arrays
	$p0 = $self->[1][$dir];
	$p1 = $self->[2][$dir];
	$p2 = $self->[3][$dir];
	$p3 = $self->[1][1 - $dir];

	# compute intermediate value(s)
	@t = _rev($p0, $p1, $p2, $in);

	# warn if multiple values
	(@t == 1) || print "multiple transform values\n";

	# compute input derivative (using first value)
	($di = _drv($p0, $t[0])) or croak('infinite derivative value');

	# compute output derivative (using first value)
	$do = _drv($p3, $t[0]);

	# return combined derivative
	return($do/$di);

}

# compute parametric partial derivatives
# direction: 0 - normal, 1 - inverse
# parameters: (ref_to_object, direction, input_value)
# returns: (partial_derivative_array)
sub _parametric {

	# get parameters
	my ($self, $dir, $in) = @_;

	# local variables
	my ($p0, $p1, $p2, $p3);
	my (@t, $t, $di, $do, $bfi, $bfo, @pj);

	# verify input
	(Scalar::Util::looks_like_number($in)) or croak('invalid transform input');

	# get parameter arrays
	$p0 = $self->[1][$dir];
	$p1 = $self->[2][$dir];
	$p2 = $self->[3][$dir];
	$p3 = $self->[1][1 - $dir];

	# compute intermediate value(s)
	@t = _rev($p0, $p1, $p2, $in);

	# warn if multiple values
	(@t == 1) || print "multiple transform values\n";

	# use first value
	$t = $t[0];

	# compute input derivative
	$di = _drv($p0, $t);

	# compute input Bernstein polynomials
	$bfi = @{$p0} ? _bernstein($#{$p0}, $t) : undef();

	# compute output derivative
	$do = _drv($p3, $t);

	# compute output Bernstein polynomials
	$bfo = @{$p3} ? _bernstein($#{$p3}, $t) : undef();

	# initialize array
	@pj = ();

	# if there are input parameters
	if (defined($bfi)) {
		
		# for each parameter
		for my $i (0 .. $#{$bfi}) {
			
			# verify non-zero input derivative
			($di) or croak('infinite Jacobian element');
			
			# compute partial derivative
			$pj[$i] = -$bfi->[$i] * $do/$di;
			
		}
		
	}

	# add output parameters (if any)
	push(@pj, @{$bfo}) if (defined($bfo));

	# return array of partial derivatives
	return(@pj);

}

# direct forward transform
# nominal domain (0 - 1)
# parameters: (parameter_array_reference, input_value)
# returns: (output_value)
sub _fwd {

	# get parameters
	my ($par, $in) = @_;

	# if no parameters
	if (! @{$par}) {
		
		# return input
		return($in);
		
	# one parameter
	} elsif (@{$par} == 1) {
		
		# return constant
		return($par->[0]);
		
	# two parameters
	} elsif (@{$par} == 2) {
		
		# return linear interpolated value
		return((1 - $in) * $par->[0] + $in * $par->[1]);
		
	# three or more parameters
	} else {
		
		# check if ICC::Support::Lapack module is loaded
		state $lapack = defined($INC{'ICC/Support/Lapack.pm'});
		
		# if input < 0
		if ($in < 0) {
			
			# return extrapolated value
			return($par->[0] + $in * _drv($par, 0));
			
		# if input > 1
		} elsif ($in > 1) {
			
			# return extrapolated value
			return($par->[-1] + ($in - 1) * _drv($par, 1));
			
		} else {
			
			# if Lapack module loaded
			if ($lapack) {
				
				# return Bernstein interpolated value
				return(ICC::Support::Lapack::bernstein($in, $par));
				
			} else {
				
				# return Bernstein interpolated value
				return(ICC::Shared::dotProduct(_bernstein($#{$par}, $in), $par));
				
			}
			
		}
		
	}
	
}

# direct reverse transform
# nominal range (0 - 1)
# parameters: (parameter_array_reference, x-min/max_array_reference, y-min/max_array_reference, input_value)
# returns: (output_value -or- array_of_output_values)
sub _rev {

	# get parameters
	my ($par, $xminmax, $yminmax, $in) = @_;

	# local variables
	my (@xs, @ys, $slope, $ext, @sol);
	my ($p, $loop, $xlo, $xhi, $xval, $yval);

	# if no parameters
	if (! @{$par}) {
		
		# return input
		return($in);
		
	# one parameter
	} elsif (@{$par} == 1) {
		
		# warn if input not equal to constant
		($in == $par->[0]) or carp('curve input not equal to constant parameter');
		
		# return input
		return($in);
		
	# two parameters
	} elsif (@{$par} == 2) {
		
		# return linear interpolated value
		return(($in - $par->[0])/($par->[1] - $par->[0]));
		
	# three or more parameters
	} else {
		
		# setup segment arrays
		@xs = (0, @{$xminmax}, 1);
		@ys = ($par->[0], @{$yminmax}, $par->[-1]);
		
		# if slope(0) not 0
		if ($slope = _drv($par, 0)) {
			
			# compute extrapolation from 0
			$ext = ($in - $par->[0])/$slope;
			
			# save if less than 0
			push(@sol, $ext) if ($ext < 0);
			
		}
		
		# test first y-value
		push(@sol, $xs[0]) if ($ys[0] == $in);
		
		# for each array segment
		for my $i (1 .. $#xs) {
			
			# if input value falls within segment
			if (($in > $ys[$i - 1] && $in < $ys[$i]) || ($in < $ys[$i - 1] && $in > $ys[$i])) {
				
				# get segment polarity
				$p = $ys[$i] > $ys[$i - 1];
				
				# initialize limits
				$xlo = $xs[$i - 1];
				$xhi = $xs[$i];
				
				# initialize loop counter
				$loop = 0;
				
				# bisection method loop
				while ($xhi - $xlo > 1E-3 && $loop++ < 15) {
					
					# compute midpoint
					$xval = ($xlo + $xhi)/2;
					
					# compute y-value
					$yval = _fwd($par, $xval);
					
					# if y-value > input -or- y-value < input
					if ($p ? ($yval > $in) : ($yval < $in)) {
						
						# set high limit
						$xhi = $xval;
						
					} else {
						
						# set low limit
						$xlo = $xval;
						
					}
					
				}
				
				# initialize loop counter
				$loop = 0;
				
				# Newton's method loop
				while (abs($in - $yval) > 1E-12 && $loop++ < 5 && ($slope = _drv($par, $xval))) {
					
					# adjust x-value
					$xval += ($in - $yval)/$slope;
					
					# compute y-value
					$yval = _fwd($par, $xval);
					
				}
				
				# print warning if failed to converge
				printf("'_rev' function of 'bern' object failed to converge with input of %.2f\n", $in) if (abs($in - $yval) > 1E-12);
				
				# save result
				push(@sol, $xval);
				
			}
			
			# add segment y-value, if a solution
			push(@sol, $xs[$i]) if ($ys[$i] == $in);
			
		}
		
		# if slope(1) not 0
		if ($slope = _drv($par, 1)) {
			
			# compute extrapolation from 1
			$ext = ($in - $par->[-1])/$slope + 1;
			
			# save if greater than 1
			push(@sol, $ext) if ($ext > 1);
			
		}
		
	}

	# return result (array or first solution)
	return(wantarray ? @sol : $sol[0]);

}

# forward derivative
# nominal domain is (0 - 1)
# parameters: (parameter_array_reference, input_value)
# returns: (output_value)
sub _drv {

	# get parameters
	my ($par, $in) = @_;

	# if no parameters
	if (! @{$par}) {
		
		# return identity derivative
		return(1);
		
	# one parameter
	} elsif (@{$par} == 1) {
		
		# return constant derivative
		return(0);
		
	# two parameters
	} elsif (@{$par} == 2) {
		
		# return linear derivative
		return($par->[1] - $par->[0]);
		
	# three or more parameters
	} else {
		
		# check if ICC::Support::Lapack module is loaded
		state $lapack = defined($INC{'ICC/Support/Lapack.pm'});
		
		# limit input value (0 - 1) to force linear extrapolation
		$in = $in < 0 ? 0 : ($in > 1 ? 1 : $in);
		
		# compute parameter differences
		my $diff = [map {$par->[$_ + 1] - $par->[$_]} (0 .. $#{$par} - 1)];
		
		# if Lapack module loaded
		if ($lapack) {
			
			# return Bernstein interpolated derivative
			return($#{$par} * ICC::Support::Lapack::bernstein($in, $diff));
			
		} else {
			
			# return Bernstein interpolated derivative
			return($#{$par} * ICC::Shared::dotProduct(_bernstein($#{$diff}, $in), $diff));
			
		}
		
	}
	
}

# compute Bernstein basis values
# parameters: (bernstein_degree, t)
# returns: (ref_to_bernstein_array)
sub _bernstein {

	# get parameters
	my ($degree, $t0) = @_;

	# local variables
	my ($bern, $t1, $m0, $m1, $n);

	# binomial coefficients
	state $pascal = [
		[1],
		[1, 1],
		[1, 2, 1],
		[1, 3, 3, 1],
		[1, 4, 6, 4, 1],
		[1, 5, 10, 10, 5, 1],
		[1, 6, 15, 20, 15, 6, 1],
		[1, 7, 21, 35, 35, 21, 7, 1],
		[1, 8, 28, 56, 70, 56, 28, 8, 1],
		[1, 9, 36, 84, 126, 126, 84, 36, 9, 1],
		[1, 10, 45, 120, 210, 252, 210, 120, 45, 10, 1],
		[1, 11, 55, 165, 330, 462, 462, 330, 165, 55, 11, 1],
		[1, 12, 66, 220, 495, 792, 924, 792, 495, 220, 66, 12, 1],
		[1, 13, 78, 286, 715, 1287, 1716, 1716, 1287, 715, 286, 78, 13, 1],
		[1, 14, 91, 364, 1001, 2002, 3003, 3432, 3003, 2002, 1001, 364, 91, 14, 1],
		[1, 15, 105, 455, 1365, 3003, 5005, 6435, 6435, 5005, 3003, 1365, 455, 105, 15, 1],
		[1, 16, 120, 560, 1820, 4368, 8008, 11440, 12870, 11440, 8008, 4368, 1820, 560, 120, 16, 1],
		[1, 17, 136, 680, 2380, 6188, 12376, 19448, 24310, 24310, 19448, 12376, 6188, 2380, 680, 136, 17, 1],
		[1, 18, 153, 816, 3060, 8568, 18564, 31824, 43758, 48620, 43758, 31824, 18564, 8568, 3060, 816, 153, 18, 1],
		[1, 19, 171, 969, 3876, 11628, 27132, 50388, 75582, 92378, 92378, 75582, 50388, 27132, 11628, 3876, 969, 171, 19, 1],
		[1, 20, 190, 1140, 4845, 15504, 38760, 77520, 125970, 167960, 184756, 167960, 125970, 77520, 38760, 15504, 4845, 1140, 190, 20, 1]
	];

	# copy binomial coefficients for this degree
	$bern = [@{$pascal->[$degree]}];

	# compute input complement
	$t1 = 1 - $t0;

	# initialize multipliers
	$m0 = $m1 = 1;

	# get polynomial degree
	$n = $#{$bern};

	# for each polynomial
	for (1 .. $n) {
		
		# multiply
		$bern->[$_] *= ($m0 *= $t0);
		$bern->[$n - $_] *= ($m1 *= $t1);
		
	}

	# return array reference
	return($bern);

}

# fit bern object to data
# uses LAPACK dgels function to perform a least-squares fit
# the parameters to be fitted have the value 'undef' and must all be within the
# output side of the parameter array, e.g. [[0, 100], [undef, undef, undef, undef]]
# fit data is an array containing input and output vectors
# parameters: (ref_to_object, fit_data_reference, ref_to_parameter_hash)
# returns: (dgels_info_value)
sub _fit {

	# get parameters
	my ($self, $fit, $hash) = @_;

	# local variables
	my ($m, $n, $d, $t, $p, $span, @sol);
	my (@so, $outz, $bern, $nrhs, $info);
	my ($outz2, $bern2, $fix_hl, $fix_sh, @c2);

	# check if ICC::Support::Lapack module is loaded
	state $lapack = defined($INC{'ICC/Support/Lapack.pm'});

	# verify Lapack module loaded (need to add non-Lapack routine someday)
	($lapack) or croak("'fit' option requires ICC::Support::Lapack module");

	# get degree
	$d = $#{$self->[1][1]};

	# for each output parameter
	for my $i (0 .. $d) {
		
		# if parameter is undefined
		if (! defined($self->[1][1][$i])) {
			
			# push index
			push(@so, $i);
			
			# set value to 0
			$self->[1][1][$i] = 0;
			
		}
		
	}

	# if no 'undef' parameters
	if (@so == 0) {
		
		# warning
		print "no 'undef' parameters to fit\n";
		
		# return
		return(0)
		
	}

	# check for min/max values
	_minmax($self);

	# for each sample
	for my $i (0 .. $#{$fit->[0]}) {
		
		# compute output difference
		$outz->[$i] = [$fit->[1][$i] - transform($self, $fit->[0][$i])];
		
		# get intermediate value
		$t = _rev($self->[1][0], $self->[2][0], $self->[3][0], $fit->[0][$i]);
		
		# compute Bernstein values for undefined parameters
		$bern->[$i] = [@{_bernstein($d, $t)}[@so]];
		
	}

	# make copies
	$outz2 = Storable::dclone($outz);
	$bern2 = Storable::dclone($bern);

	# get array sizes
	$m = @{$bern};
	$n = @{$bern->[0]};
	$nrhs = @{$outz->[0]};

	# fit the data, return if failed
	($info = ICC::Support::Lapack::dgels('N', $m, $n, $nrhs, $bern, $m, $outz, $m)) && return($info);

	# get curve polarity (0 - increasing, 1 - decreasing)
	$p = ($span = ($so[-1] == $d ? $outz->[$n - 1][0] : $self->[1][1][-1]) - ($so[0] == 0 ? $outz->[0][0] : $self->[1][1][0])) < 0;

	# get flags (indicating HL or SH contrast needs to be fixed)
	$fix_hl = $hash->{'fix_hl'} && $so[0] == 0 && $so[1] == 1 && (($outz->[0][0] > $outz->[1][0]) ^ $p);
	$fix_sh = $hash->{'fix_sh'} && $so[-1] == $d && $so[-2] == $d - 1 && (($outz->[$n - 1][0] < $outz->[$n - 2][0]) ^ $p);

	# if degree >= 4 and HL or SH needs fixing
	if ($d >= 4 && ($fix_hl || $fix_sh)) {
		
		# for each sample
		for my $i (0 .. $#{$bern2}) {
			
			# if highlight fix
			if ($fix_hl) {
				
				# get first 2 Bernstein values
				@c2 = splice(@{$bern2->[$i]}, 0, 2);
				
				# combine to single value
				unshift(@{$bern2->[$i]}, ($c2[0] + $c2[1]));
				
			}
			
			# if shadow fix
			if ($fix_sh) {
				
				# get last 2 Bernstein values
				@c2 = splice(@{$bern2->[$i]}, -2);
				
				# combine to single value
				push(@{$bern2->[$i]}, ($c2[0] + $c2[1]));
				
			}
			
		}
		
		# get array sizes
		$m = @{$bern2};
		$n = @{$bern2->[0]};
		$nrhs = @{$outz2->[0]};
		
		# fit the data, return if failed
		($info = ICC::Support::Lapack::dgels('N', $m, $n, $nrhs, $bern2, $m, $outz2, $m)) && return($info);
		
		# extract solution
		@sol = map {$outz2->[$_][0]} (0 .. $n - 1);
		
		# replace combined coefficient(s)
		splice(@sol, 1, 0, $sol[0] + $span * 0.001) if ($fix_hl);
		splice(@sol, -1, 0, $sol[-1] - $span * 0.001) if ($fix_sh);
		
	} else {
		
		# extract solution
		@sol = map {$outz->[$_][0]} (0 .. $n - 1);
		
	}

	# copy solution to output array
	@{$self->[1][1]}[@so] = @sol;

	# return
	return(0);

}

# set object contents from parameter hash
# parameters: (ref_to_object, ref_to_parameter_hash)
sub _new_from_hash {

	# get parameters
	my ($self, $hash) = @_;

	# local variables
	my ($in, $out, $fit);

	# if 'input' is defined
	if (defined($in = $hash->{'input'})) {
		
		# if 'input' is a vector (21 values or less, some may be 'undef')
		if (ICC::Shared::is_vector($in) && 21 >= @{$in}) {
			
			# set object 'input' array
			$self->[1][0] = [@{$in}];
			
		# if 'input' is an integer between 1 and 20
		} elsif (Scalar::Util::looks_like_number($in) && $in == int($in) && $in > 0 && $in < 21) {
			
			# make identity curve
			$self->[1][0] = [map {$_/$in} (0 .. $in)];
			
		} else {
			
			# error
			croak("invalid 'input' values");
			
		}
		
	} else {
		
		# disable input
		$self->[1][0] = [];
		
	}

	# if 'output' is defined
	if (defined($out = $hash->{'output'})) {
		
		# if 'output' is a vector (21 values or less, some may be 'undef')
		if (ICC::Shared::is_vector($out) && 21 >= @{$out}) {
			
			# set object 'output' array
			$self->[1][1] = [@{$out}];
			
		# if 'output' is an integer between 1 and 20
		} elsif (Scalar::Util::looks_like_number($out) && $out == int($out) && $out > 0 && $out < 21) {
			
			# make identity curve
			$self->[1][1] = [map {$_/$out} (0 .. $out)];
			
		} else {
			
			# error
			croak("invalid 'output' values");
			
		}
		
	} else {
		
		# disable output
		$self->[1][1] = [];
		
	}

	# if 'fit' is defined and not empty
	if (defined($fit = $hash->{'fit'}) && @{$fit}) {
		
		# verify 'fit' vectors
		(ICC::Shared::is_num_vector($fit->[0]) && 1 <= @{$fit->[0]}) or croak("invalid 'fit' input values");
		(ICC::Shared::is_num_vector($fit->[1]) && 1 <= @{$fit->[1]}) or croak("invalid 'fit' output values");
		(@{$fit->[0]} == @{$fit->[1]}) or croak("'fit' input and output vectors are different size");
		
		# fit object to data
		(_fit($self, $fit, $hash)) && croak("'fit' operation failed");
		
	} else {
		
		# verify object parameters are all defined
		(@{$self->[1][0]} == grep {defined($_)} @{$self->[1][0]}) or croak("some 'input' values are 'undef'");
		(@{$self->[1][1]} == grep {defined($_)} @{$self->[1][1]}) or croak("some 'output' values are 'undef'");
		
	}

	# add segment min/max y-values
	_minmax($self);

}

# write bern object to ICC profile
# note: writes an equivalent curv object
# parameters: (ref_to_object, ref_to_parent_object, file_handle, ref_to_tag_table_entry)
sub _writeICCbern {

	# get parameters
	my ($self, $parent, $fh, $tag) = @_;

	# local variables
	my ($n, $up, @table);

	# get count value (defaults to 4096, the maximum allowed in an 'mft2' tag)
	$n = $self->[0]{'curv_points'} // 4096;

	# validate number of table entries
	($n == int($n) && $n >= 2) or carp('invalid number of table entries');

	# array upper index
	$up = $n - 1;

	# for each table entry
	for my $i (0 .. $up) {
		
		# transform value
		$table[$i] = transform($self, $i/$up);
		
	}

	# seek start of tag
	seek($fh, $tag->[1], 0);

	# write tag type signature and count
	print $fh pack('a4 x4 N', 'curv', $n);

	# write array
	print $fh pack('n*', map {$_ < 0 ? 0 : ($_ > 1 ? 65535 : $_ * 65535 + 0.5)} @table);

}

1;