#------------------------------------------------------------------------------
#$Author: andrius $
#$Date: 2020-12-18 07:14:35 -0500 (Fri, 18 Dec 2020) $
#$Revision: 7139 $
#$URL: svn://saulius-grazulis.lt/restful/tags/v0.15.2/lib/HTMLGenerator.pm $
#------------------------------------------------------------------------------
#*
#  Generate HTML forms and pages for database record and table representation.
#
# 'data2html' - main function for table and control buttons generation on a
# page.
#
# Generates head_controls buttons:
#    for record alteration (new, edit).
#
# Pagination buttons (prev, next) are generated in db.pl.
#
# 'data2html' is called by other functions:
#    data2html_page
#    data2html_edit_form
#    data2html_new_form
#    data2html_list_table.
#**

package HTMLGenerator;
use warnings;
use strict;

use CGI qw(:standard start_a end_a start_div end_div start_span end_span -nosticky -utf8);
use Clone qw(clone);
use Data::UUID;
use File::Basename qw(dirname basename);
use HTML::Entities qw( encode_entities decode_entities );
use Text::Markdown qw(markdown);
use Encode qw(decode);
use URI::Encode qw( uri_encode uri_decode );
use Data::Validate::URI qw(is_uri);
use Database qw( has_values );
use Database::Order;
use RestfulDB::Defaults;
use RestfulDB::Exception;
use RestfulDB::SQL qw(
    is_blob
    is_character_string
    is_numerical
    is_integer
    is_text
);
use RestfulDB::Version qw( $VERSION );
use POSIX qw(strftime);
use List::MoreUtils qw(uniq);
use List::Util qw( any );
use Text::sprintfn;

require Exporter;
our @ISA = qw( Exporter );
our @EXPORT_OK = qw(
    data2html
    data2html_page
    data2html_edit_form
    data2html_new_form
    data2html_list_table
    error_page
    forward_backward_control_form
    make_table_uris
    make_subresource_uri
    nameless_submit
    start_form_QS
    start_html5
    transfer_query_string
    start_root
    end_root
    header_tables_pl
    header_db_pl
    header_record_pl
    start_doc_wrapper
    end_doc_wrapper
    start_main_content
    end_main_content
    footer_tables_pl
    footer_db_pl
    footer_record_pl
    start_card_fluid
    end_card
    nameless_submit_disabled
    three_column_row_html
    two_column_row_html
    button_classes_string
    warnings2html
    datalists
);

## @function make_table_uris (Database db, $db_table, $db_column, $request_uri)
# Convert a column name into a RESTful URI of a referenced table if
# the column is a foreign key.
#
# @param db \ref Database object
# @param db_table table name
# @param request_uri request URI
# @retval db_column column name
sub make_table_uris
{
    my ( $db, $db_table, $db_column, $request_uri ) = @_;

    my $column_properties = $db->get_column_properties( $db_table );
    my $altname = $column_properties->{altname}{$db_column};
    my $units = $column_properties->{measurement_units}{$db_column};
    my $units_suffix = defined $units ? " [$units]" : '';

    if( $column_properties->{coltype}{$db_column} &&
        ( $column_properties->{coltype}{$db_column} eq 'fk' ||
          $column_properties->{coltype}{$db_column} eq 'dbrev' ) ) {
        my $foreign_keys = $db->get_foreign_keys( $db_table );
        my( $fk ) = grep { $_->name eq $db_column } @$foreign_keys;
        my $fk_table;
        if( $fk ) {
            $fk_table = $fk->parent_table;
        } else {
            $fk_table = $db_column;
            $fk_table =~ s/_id$//;
        }
        my $fk_referenced_table_uri =
            dirname( $request_uri ) . "/" . $fk_table;
        $fk_referenced_table_uri =
            transfer_query_string( $fk_referenced_table_uri,
                                   $request_uri,
                                   { exclude_re =>
                                         'id_column|offset|rows|table|order|' .
                                         'select_.*|search_value|filter' } );
        my $column_text = $db_column;
        $column_text =~ s/_id$//;
        if( defined $altname ) {
            $column_text = $altname;
        }
        $column_text .= $units_suffix;
        return a({ -class => 'redirect-link',
                   -href => $fk_referenced_table_uri }, $column_text );
    } else {
        my $column_text = defined $altname ? $altname : $db_column;
        return $column_text . $units_suffix;
    }
}

## @function get_foreign_key_cell_text ($colname, $value, %$params)
# Using the new data structure \ref Database::get_record_descriptions.
#
# @param colname column name
# @param value id
# @param params hash of parameters
# @return text describing foreign key values
sub get_foreign_key_cell_text($$$)
{
    my ( $colname, $value, $params ) = @_;

    my ( $record, $foreign_keys, $ambiguous_fks ) =
        ( $params->{record},
          $params->{foreign_keys},
          $params->{ambiguous_fks});

    my $related_data = defined $record ? $record->{columns} : undef;
    my $cell_value;

    my( $fk ) = grep { $_->name eq $colname } @$foreign_keys;
    if( $fk && defined $related_data ) {
        my $foreign_table = $fk->parent_table;

        my( $foreign_id_column ) =
            grep { $related_data->{$_}{coltype} &&
                   $related_data->{$_}{coltype} eq 'id' }
                 sort keys %$related_data;
        $foreign_id_column = $RestfulDB::Defaults::default_id_column
            unless defined $foreign_id_column;

        my $foreign_id = $related_data->{$foreign_id_column}{value};

        if( defined $record->{metadata}{fk_format} ) {
            my %column_data = map { $_ =>
                                    (defined $related_data->{$_}{value}
                                           ? $related_data->{$_}{value}
                                           : '') }
                                  keys %$related_data;
            $cell_value = sprintfn( $record->{metadata}{fk_format},
                                    \%column_data );
        } else {
            $cell_value = join ', ',

            map { exists $related_data->{$_}{value} &&
                  defined $related_data->{$_}{value}
                    ? $related_data->{$_}{value} : () }

            grep { (!$related_data->{$_}{coltype} ||
                     $related_data->{$_}{coltype} !~
                        /^(id|fk|uuid|dbrev|cssclass|mimetype|format)$/ ) &&
                    !is_text($related_data->{$_}{sqltype}) &&
                    !is_blob($related_data->{$_}{sqltype}) }

            sort keys %$related_data;
        }

        if( $cell_value eq '' ) {
            $cell_value = $foreign_id;
        }
            
        if( defined $ambiguous_fks ){
            my $fk_name = $fk->name();
            my $ambiguous = $ambiguous_fks->{$fk_name}->{$cell_value};
            $cell_value .= $ambiguous ? qq( ($foreign_id_column = $foreign_id) ) : ''; 
        }
    }

    return defined $cell_value ? $cell_value : $value;
}

## @function transfer_query_string ($new_uri, $old_uri, %$options)
# Transfers query string from the "old" URI that was used for original
# request to the "new" URI that must serve a GET request with the same
# parameters. The "new" URI is assumed to be without any query string.
sub transfer_query_string
{
    my ($new_uri, $old_uri, $options) = @_;

    $options = {} unless $options;
    my $extra_parameters = $options->{append};
    my $exclude_re = $options->{exclude_re};

    die "The new URI should not have any query string, but '$new_uri' does"
        if $new_uri =~ /\?/;

    my $fragment = '';
    $fragment = $1 if $new_uri =~ s/(#[^#]+)$//;

    $old_uri =~ s/#[^#]+$//;
    my( $old_uri_base, $query_string ) = split /\?/, $old_uri;
    my @key_value_pairs;
    if( $query_string ) {
        @key_value_pairs = split /&/, $query_string;
    }

    if( $exclude_re ) {
        @key_value_pairs = grep { !/^($exclude_re)=/ } @key_value_pairs;
    }

    if( $extra_parameters ) {
        if( ref $extra_parameters eq 'HASH' ) {
            my $uri = URI::Encode->new( { encode_reserved => 1 } );
            for my $key (sort keys %$extra_parameters ) {
                my @values = ref $extra_parameters->{$key}
                                ? @{$extra_parameters->{$key}}
                                : ( $extra_parameters->{$key} );
                push @key_value_pairs,
                     map { sprintf '%s=%s',
                                   $uri->encode( $key ),
                                   defined $_ ? $uri->encode( $_ ) : '' }
                         @values;
            }
        } else {
            push @key_value_pairs, $extra_parameters;
        }
    }

    if( @key_value_pairs ) {
        $new_uri .= '?' . join( '&', @key_value_pairs );
    }

    $new_uri .= $fragment;

    return $new_uri;
}

## @function make_subresource_uri ($uri, $subresource, $extra_parameters)
# Build an URI which specifies desired sub-resource; transfer the
# query string from the old URI to the new one if necessary.

sub make_subresource_uri
{
    my ( $uri, $subresource, $extra_parameters ) = @_;

    # Remove query string:
    my ($resource_uri) = ($uri =~ /^(.*?)(?:\?|$)/);

    return
        transfer_query_string( $resource_uri . "/" . $subresource,
                               $uri, $extra_parameters );
}

sub _make_order_spec
{
    my ($dbtable, $dbcolumn, $order, $updown) = @_;

    # Making a full copy of Database::Order data structure to avoid
    # overwriting the original object:
    my $new_order = clone( $order );

    if( defined $updown ) {
        $new_order->order_ascending(  $dbtable, $dbcolumn ) if $updown eq 'a';
        $new_order->order_descending( $dbtable, $dbcolumn ) if $updown eq 'd';
    } else {
        $new_order->unorder( $dbtable, $dbcolumn );
    }

    my $order_param = $new_order->query_string;

    return $order_param ?
        'order=' . uri_encode( $order_param, { encode_reserved => 1 } ) : '';
}

## @function make_sort_buttons ($dbcolumn, $uri, $button_images, $order)
# Creates \<a href=...> references with image bodies and target URIs that
# switch on and off database sorting options by manipulating the 'order=...'
# CGI parameter.

sub make_sort_buttons
{
    my ( $dbtable, $dbcolumn, $uri, $button_images, $order ) = @_;

    $order = Database::Order->new_from_string() unless $order;

    my( $uri_base ) = split /\?/, $uri;
    if( $uri =~ /(#[^#]+)$/ && $uri_base !~ /#/ ) {
        $uri_base .= $1;
    }

    my $sort_buttons_html =
        span( { -class => 'inside-label-linker' }, br ) .
        start_div({ -class => "sort_buttons_class" }) . "\n";

    # Generate the "up" button:
    my $up_ref;
    my $up_class = 'up_button_class';
    if( $order->column_is_selected( $dbtable, $dbcolumn, 'a' )) {
        $up_ref = transfer_query_string( $uri_base, $uri,
                                         { append => _make_order_spec( $dbtable,
                                                                       $dbcolumn,
                                                                       $order,
                                                                       undef ),
                                           exclude_re => 'order' } );
    } else {
        $up_ref = transfer_query_string( $uri_base, $uri,
                                         { append => _make_order_spec( $dbtable,
                                                                       $dbcolumn,
                                                                       $order,
                                                                       'a' ),
                                           exclude_re => 'order' } );
        $up_class .= ' button_selected';
    }

    $sort_buttons_html .=
        a( { -href => $up_ref },
           img({ -src => $button_images->{up},
                 -alt => "up button",
                 -class => $up_class
               })
        );

    # The "down" button:
    my $down_ref;
    my $down_class = 'down_button_class';
    if( $order->column_is_selected( $dbtable, $dbcolumn, 'd' )) {
        $down_ref = transfer_query_string( $uri_base, $uri,
                                           { append => _make_order_spec( $dbtable,
                                                                         $dbcolumn,
                                                                         $order,
                                                                         undef ),
                                             exclude_re => 'order' } );
    } else {
        $down_ref = transfer_query_string( $uri_base, $uri,
                                           { append => _make_order_spec( $dbtable,
                                                                         $dbcolumn,
                                                                         $order,
                                                                         'd' ),
                                             exclude_re => 'order' } );
        $down_class .= ' button_selected';
    }

    $sort_buttons_html .=
        a( { -href => $down_ref },
           img({ -src => $button_images->{down},
                 -alt => "down button",
                 -class => $down_class
               })
        );

    $sort_buttons_html .= end_div() . "\n";

    return $sort_buttons_html;
}

## @function data_value2html (Database db, $db_table, %$row_data, $key, %$options)
# Converts column data to HTML.
#
# @param db \ref Database object
# @param db_table table name
# @param row_data one item from the data structure generated by
#                 \ref Database::get_record_descriptions()
# @param key column name
# @param options hash of options
# @return HTML value for specified column.
sub data_value2html($$$$$)
{
    my ( $db, $db_table, $row_data, $key, $options ) = @_;
    my ( $record_id, $database_base, $data_level, $request_uri,
         $ambiguous_fks ) = (
        $options->{record_id},
        $options->{database_base_uri},
        $options->{data_level},
        $options->{request_uri},
        $options->{ambiguous_fks});

    return '' if !exists $row_data->{columns}{$key};

    my $column_properties = $db->get_column_properties( $db_table );
    my $foreign_keys = $db->get_foreign_keys( $db_table );

    my $uri;
    my $href_target = '_blank';
    my $download;
    my $value =
        exists $row_data->{columns}{$key}{value} ?
               $row_data->{columns}{$key}{value} : undef;
    my $cell_value = $value;
    my $urlvalue =
        exists $row_data->{columns}{$key}{urlvalue} ?
               $row_data->{columns}{$key}{urlvalue} : undef;
    my $coltype =
        exists $row_data->{columns}{$key}{coltype} ?
               $row_data->{columns}{$key}{coltype} : undef;
    my $sqltype = $row_data->{columns}{$key}{sqltype};

    if( $coltype && $coltype =~ /^(fk|dbrev)$/ &&
        ($row_data->{columns}{$key}{fk} ||
         $row_data->{columns}{$key}{fk_target}) ) {
        my $fk = $row_data->{columns}{$key}{fk};
        my $fk_id = $value;
        my $fk_target = $row_data->{columns}{$key}{fk_target};
        my $fk_table =
            $fk ? $fk->parent_table : $fk_target->{metadata}{table_name};

        # FIXME: should read all related foreign records at
        # once and cache them, to reduced DB traffic.

        if( $coltype eq 'fk' && has_values( $fk_target ) ) {
            $cell_value =
                get_foreign_key_cell_text(
                    $key,
                    $fk_id,
                    { record => $fk_target,
                      foreign_keys => $foreign_keys,
                      ambiguous_fks => $ambiguous_fks } );
        } elsif( defined $fk_id ) {
            $cell_value = $fk_id;
        } else {
            $cell_value = '';
        }
        $cell_value = encode_entities( $cell_value );

        if( defined $fk_id ) {
            $uri = transfer_query_string(
                $database_base . '/' . $fk_table . '/' . $fk_id,
                $request_uri,
                { exclude_re => 'id_column|order|offset|rows|' .
                                'table|select_.*|search_value|filter' } );
            $href_target = undef;
        }
    } elsif( defined $coltype &&
             ( ( $coltype eq 'image'  && defined $urlvalue ) ||
               ( $coltype eq 'imguri' && defined $value ) ) ) {
        my $image_uri;
        if( $coltype eq 'image' ) {
            $image_uri = $urlvalue;
        } else {
            $image_uri = $value;
        }
        my $args = { '-src' => $image_uri,
                     '-alt' => "Image of $db_table/$record_id/$key" };
        if( exists $column_properties->{cssclass}{$key} ) {
            my $css_class_column = $column_properties->{cssclass}{$key};
            if( exists $row_data->{columns}{$css_class_column} &&
                defined $row_data->{columns}{$css_class_column}{value} ) {
                $args->{'-class'} =
                    $row_data->{columns}{$css_class_column}{value};
            }
        }
        # Here fill up the table cell with lightbox picture:
        # a(table_sized_image);
        # div(enlarged_page_sized_picture + a('close button'
        #     <- for pure css implementation)).
        #
        # If eant to implement set close enlarged picture on 'Esc' key
        # press -> use JS.
        my $target_image_id = "$db_table-$key$record_id";
        my $image_html = img( $args );
        my $image_link_html = a( {-class => "lightbox",
                                  -href => "#$target_image_id"},
                                 $image_html );
        my $close_link_html = a( {-class => "lightbox-close", href => "#"}, '' );
        my $target_html = div( {-class => "lightbox-target",
                                -id => $target_image_id},
                                $image_html,
                                $close_link_html);
        $cell_value = $image_link_html . $target_html;
    } elsif( $coltype && $coltype eq 'url' ) {
        $uri = $value;
    } elsif( defined $value && defined $row_data->{columns}{$key}{resolver} ) {
        $uri = sprintf( $row_data->{columns}{$key}{resolver}, $value );
        $uri = $database_base . '/' . $uri if !is_uri( uri_encode( $uri ) );
        $cell_value = encode_entities $value;
    } elsif( defined $value && defined $coltype && $coltype =~ /^(id|extkey)$/ ) {
        my $record_uri = $database_base . '/' . $db_table . '/' . $value;
        $cell_value =
            a( { -href =>
                     transfer_query_string(
                         $record_uri,
                         $request_uri,
                         $data_level == 1 ? {} :
                         { exclude_re => 'id_column|order|offset|rows|' .
                                         'table|select_.*|search_value|filter' } )
               }, $value );
    } elsif( defined $value &&
             exists $column_properties->{printf_format}{$key} ) {
        $cell_value = sprintf $column_properties->{printf_format}{$key},
                              encode_entities $cell_value;
    } elsif( defined $coltype && $coltype eq 'markdown' ) {
        $cell_value = markdown( encode_entities( $value, '<>&' ) )
            if defined $value;
    } elsif( is_numerical( $sqltype ) && defined $value && $value ne '' ) {
        $cell_value = encode_entities sprintf '%11.6g', $value;
        $cell_value =~ s/\s//g;
    } elsif( is_blob( $sqltype ) && defined $urlvalue ) {
        # BLOBs will be represented by download URIs by
        # default:
        $cell_value = $record_id . '/' . $key;
        $uri = $database_base . '/' . $db_table . '/' . $cell_value;
        $href_target = undef;
        $download = $key;
        if( exists $column_properties->{filename}{$key} ) {
            my $filename_column = $column_properties->{filename}{$key};
            if( $row_data->{columns}{$filename_column} &&
                defined $row_data->{columns}{$filename_column}{value} ) {
                $cell_value = $row_data->{columns}{$filename_column}{value};
                $download = $cell_value;
            }
        }
    } elsif ( !$db->{db}{content}{user}
        && defined ($column_properties->{display}{$key})
        && $column_properties->{display}{$key} eq 'foruser' ) {
        $cell_value = span( { -class => "confidential-msg" }, $value );
    } else {
        $cell_value = encode_entities( $value );
    }

    if( defined $uri ) {
        return a( { -href => $uri,
                    (defined $href_target ? ( -target => $href_target ) : ()),
                    (defined $download ? ( -download => $download ) : ()),
                  },
                  $cell_value );
    } else {
        return $cell_value;
    }
}

## @function data_value2html_input (Database db, $db_table, %$row_data, $key, %$options)
# Create HTML snippet array for one data item, to be displayed in a
# HTML input form by the caller.
#
# @param db \ref Database object
# @param db_table table name
# @param row_data one item from the data structure generated by
#                 \ref Database::get_record_descriptions()
# @param key column name
# @param options hash of options
# @return HTML snippet with the data value to be displayed in a table.
sub data_value2html_input($$$$$)
{
    my ( $db, $db_table, $row_data, $key, $options ) = @_;
    my ( $record_id, $database_base, $data_level,
         $add_none_to_enumerators, $horizontal, $request_uri,
         $suggest_values, $full_table_name, $empty_set_values, 
         $ambiguous_fks ) = (
        $options->{record_id},
        $options->{database_base_uri},
        $options->{data_level},
        $options->{add_none_to_enumerators},
        $options->{horizontal},
        $options->{request_uri},
        $options->{suggest_values},
        $options->{full_table_name},
        $options->{empty_set_values},
        $options->{ambiguous_fks} );

    my $column_properties = $db->get_column_properties( $db_table );
    my $foreign_keys = $db->get_foreign_keys( $db_table );

    my $uri;
    my $href_target = '_blank';
    my $value =
        exists $row_data->{columns}{$key}{value} ?
               $row_data->{columns}{$key}{value} : undef;
    my $cell_value = $value;
    my $urlvalue =
        exists $row_data->{columns}{$key}{urlvalue} ?
               $row_data->{columns}{$key}{urlvalue} : undef;
    my $coltype =
        exists $row_data->{columns}{$key}{coltype} ?
               $row_data->{columns}{$key}{coltype} : undef;
    my $sqltype = $row_data->{columns}{$key}{sqltype};
    my $enumeration_values =
        exists $row_data->{metadata}{enumeration_values}{$key} ?
               $row_data->{metadata}{enumeration_values}{$key} : undef;

    my @input_required = ($data_level == 1 &&
                          $row_data->{columns}{$key}{mandatory})
                            ? ( '-required', 'required' )
                            : ();

    if( (any { $_->{name} eq $key } @$foreign_keys) &&
        $coltype =~ /^(fk|dbrev)$/ ) {
        my $fk_id = $value;
        my $fk_target = $row_data->{columns}{$key}{fk_target};
        my $fk_table = $fk_target->{metadata}{table_name};
        my $fk_properties = $db->get_table_properties( $fk_table );
        my $new_record_uri =
            transfer_query_string( "$database_base/$fk_table",
                                   $ENV{REQUEST_URI},
                                   { append => 'action=template',
                                     exclude_re =>
                                         'id_column|offset|rows|table' } );

        if( defined $coltype && $coltype eq 'fk' ) {
            my $id_column = $db->get_id_column( $fk_table );
            my $fk_values =
                $db->get_record_descriptions( $fk_table,
                                              {
                                                  no_foreign => 1,
                                                  no_related => 1,
                                                  no_empty => 1,
                                              } );

            my @enum_values;
            my %enum_labels;
            for my $fk_value ( @{ $fk_values } ) {
                next if !$id_column;
                next if !exists  $fk_value->{columns}{$id_column}{value};
                next if !defined $fk_value->{columns}{$id_column}{value};
                my $id = $fk_value->{columns}{$id_column}{value};
                push @enum_values, $id;
                $enum_labels{$id} =
                    get_foreign_key_cell_text(
                        $key,
                        $id,
                        { record => $fk_value,
                          foreign_keys => $foreign_keys,
                          ambiguous_fks => $ambiguous_fks } );

                if( $enum_labels{$id} eq '' ) {
                    $enum_labels{$id} = $id;
                }
            }

            @enum_values = sort { $enum_labels{$a} cmp
                                  $enum_labels{$b} } @enum_values;

            push @enum_values, '';
            $enum_labels{''} = 'None' unless exists $enum_labels{''};

            # NULL should match option 'None':
            $value = '' if !defined $value;

            return popup_menu( -name => "column:$full_table_name.$key",
                               -class => 'dropdown',
                               -default => $value,
                               -values => \@enum_values,
                               -labels => \%enum_labels ) .
                   ($fk_properties->{is_read_only}
                        ? ''
                        : a( { -class => 'redirect-link',
                               -href => $new_record_uri,
                               -target => '_blank' }, 'New' ));
        } else {
            $cell_value = $fk_id;
        }

        if( $fk_id ) {
            $uri = transfer_query_string(
                $database_base . '/' . $fk_table . '/' . $fk_id, $request_uri,
                { exclude_re =>
                      'id_column|order|offset|rows|' .
                      'table|select_.*|search_value|filter' } );
            $href_target = undef;
        }
    } elsif( defined $coltype &&
             ( $coltype eq 'image' || $coltype eq 'imguri' ) ) {
        my $image_uri;
        if(      $coltype eq 'image'  && defined $urlvalue ) {
            $image_uri = $urlvalue;
        } elsif( $coltype eq 'imguri' && defined $value ) {
            $image_uri = $value;
        }
        if( $image_uri ) {
            my $args = { '-class' => 'image-preview',
                         '-src' => $image_uri,
                         '-alt' => "Image of $db_table/$record_id/$key" };
            if( exists $column_properties->{cssclass}{$key} ) {
                my $css_class_column = $column_properties->{cssclass}{$key};
                if( exists $row_data->{columns}{$css_class_column} &&
                    defined $row_data->{columns}{$css_class_column}{value} ) {
                    $args->{'-class'} .=
                        ' ' . $row_data->{columns}{$css_class_column}{value};
                }
            }
            # img.preview-image
            #
            # Here fill up the table cell with lightbox picture:
            # a(table_sized_image);
            # div(enlarged_page_sized_picture + a('close button'
            #     <- for pure css implementation)).
            #
            # If want to implement set close enlarged picture on 'Esc' key
            # press -> use JS.
            #
            my $target_image_id = "$db_table-$key$record_id";
            my $image_html = img( $args );
            my $image_link_html = a( {-class => "lightbox",
                                      -href => "#$target_image_id"},
                                     $image_html );
            my $close_link_html = a( {-class => "lightbox-close",
                                      -href => "#"},
                                     '' );
            my $target_html = div( {-class => "lightbox-target",
                                    -id => $target_image_id},
                                    $image_html,
                                    $close_link_html);
            $cell_value = $image_link_html . $target_html;
            # Cannot require the contents to be uploaded on each change:
            @input_required = () if $coltype eq 'image';
        } else {
            $cell_value = '';
        }
        if( $coltype eq 'image' ) {
            return $cell_value . filefield( -name => "column:$full_table_name.$key",
                                            -value => $image_uri,
                                            @input_required );
        } else {
            return $cell_value . input( { -id => "column:$full_table_name.$key",
                                          -type  => 'text',
                                          -name  => "column:$full_table_name.$key",
                                          -value => $image_uri,
                                          @input_required } );
        }
    } elsif( defined $coltype && $coltype eq 'url' ) {
        $uri = $value;
    } elsif( defined $coltype && $coltype eq 'id' ) {
        $uri = defined $value ? $database_base . '/' . $db_table . '/' . $value : '';
    } elsif( exists $row_data->{columns}{$key}{set_values} ) {
        my @checked;
        if( defined $cell_value ) {
            @checked = split ',', $cell_value;
        }
        my $hidden = '';
        if( $empty_set_values ) {
            $hidden = hidden( { -name => "column:$full_table_name.$key",
                                -value => '' } );
        }
        return $hidden .
               checkbox_group( -name => "column:$full_table_name.$key",
                               -default => \@checked,
                               -values => $row_data->{columns}{$key}{set_values} );
    } elsif( $enumeration_values ) {
        $cell_value = '' if ! defined $cell_value;
        my @enum_values = @{ $enumeration_values };
        if( $add_none_to_enumerators && ! any { ! defined $_ } @enum_values ) {
            push @enum_values, undef;
        }
        my %enum_labels = map { defined $_ ? ( $_ => $_ ) : ( '' => 'None' ) }
                              @enum_values;
        @enum_values = uniq( map { defined $_ ? $_ : '' } @enum_values );
        return popup_menu( -name => "column:$full_table_name.$key",
                           -class => 'dropdown',
                           -default => $cell_value,
                           -values => \@enum_values,
                           -labels => \%enum_labels );
    } elsif( is_blob( $sqltype ) ) {
        # BLOBs will be represented by download URIs by default.
        my $blob_uri;
        if( defined $urlvalue ) {
            $blob_uri = $urlvalue;
            my $filename = "$record_id/$key";
            my $download;
            if( exists $column_properties->{filename}{$key} ) {
                my $filename_column = $column_properties->{filename}{$key};
                if( $row_data->{columns}{$filename_column} &&
                    defined $row_data->{columns}{$filename_column}{value} ) {
                    $filename = $row_data->{columns}{$filename_column}{value};
                    $download = $filename;
                }
            }
            $cell_value = a( { -href => $blob_uri,
                             ($download ? (-download => $download) : ()) },
                             $filename );
            # Cannot require the contents to be uploaded on each change:
            @input_required = ();
        }
        return (defined $cell_value ? $cell_value : '') .
               filefield( -name => "column:$full_table_name.$key",
                          -value => $blob_uri,
                          @input_required );
    } elsif( is_text( $sqltype ) ||
             ( defined $coltype && $coltype eq 'markdown' ) ) {
        # Return 'textarea' field inside the table cell: with the specific name
        # and default value if available.
        return textarea( -name => "column:$full_table_name.$key",
                         -default => $value,
                         @input_required );
    } else {
        $cell_value = $value;
    }

    # TODO: there are too much return statements that are scattered across the
    # function. Begging to be split into small functions (A.G.).
    if( defined $uri ) {
        return a( { -href => $uri,
                    (defined $href_target
                        ? ( -target => $href_target ) : ()) },
                  defined $cell_value ? $cell_value : '' );
    } else {
        my %validation_attributes;
        if( is_integer( $sqltype ) ) {
            $validation_attributes{'data-validation-type'} = 'int';
            if( defined $row_data->{columns}{$key}{length} ) {
                $validation_attributes{'data-max-value'} =
                    $row_data->{columns}{$key}{length};
            }
        } elsif( is_numerical( $sqltype ) ) {
            $validation_attributes{'data-validation-type'} = 'float';
        } elsif( is_character_string( $sqltype ) &&
                 defined $row_data->{columns}{$key}{length} ) {
            $validation_attributes{'data-validation-type'} = 'string';
            $validation_attributes{'data-max-length'} =
                $row_data->{columns}{$key}{length};
        }

        if( exists $column_properties->{validation_regex}{$key} ) {
            $validation_attributes{'data-validation-regex'} =
                $column_properties->{validation_regex}{$key};
            if( !$validation_attributes{'data-validation-type'} ) {
                $validation_attributes{'data-validation-type'} = 'string';
            }
        }

        if( %validation_attributes ) {
            $validation_attributes{-onchange} =
                "validate_input( 'column:$full_table_name.$key' )";
        }

        my $input_suggestion = '';
        if( $suggest_values &&
            defined $coltype && $coltype =~ /^c(time|date)|extkey$/ ) {
            $input_suggestion =
                suggest_input( $db_table, $key,
                               {
                                   coltype => $coltype,
                                   value => $cell_value,
                                   full_table_name => $full_table_name,
                                   ($coltype eq 'extkey'
                                        ? ( suggested_values =>
                                            $db->get_value_suggestions( $db_table, $key ) )
                                        : ()),
                                   (%validation_attributes
                                        ? (validation => $validation_attributes{-onchange})
                                        : ()),
                               } );
        }

        # Return text input field in record edit form : add inline width to
        # the text input field, in character symbols.
        #  my $val_length = length( $cell_value );
        #  -style => "width:$val_length"."ch",
        # This construct doen't display well while scaling down, can't use
        # absolute width units while statically assigning size to the input
        # field. Two solutions:
        # (1) - use JS and absolute units.
        # (2) - use relative units only.
        #
        # <label> MUST follow <input> immediately as it will be used for
        # validation messages
        return input( { -id => "input:$full_table_name.$key",
                        -type  => 'text',
                        -name  => "column:$full_table_name.$key",
                        -value => defined $cell_value ? $cell_value : '',
                        @input_required,
                        %validation_attributes } ) .
               ( %validation_attributes ?
                 label( { -class => 'validation-message' }, '' ) : '' ) .
               $input_suggestion;
    }

    return $cell_value;
}

## @function suggest_input ($db_table, $column, %$options)
# Creates HTML/Javascript snippet that suggests input values for given
# input field.
# @param db_table table name
# @param column column name
# @param options hash of options
# @return HTML snippet
sub suggest_input
{
    my ( $db_table, $column, $options ) = @_;

    my ( $value, $coltype, $suggested_values, $full_table_name,
         $validation ) = (
        $options->{value},
        $options->{coltype},
        $options->{suggested_values},
        $options->{full_table_name},
        $options->{validation},
    );

    my $cell_content = "";
    my $input_id = "input:$full_table_name.$column";
    my $select_id = "select:$full_table_name.$column";
    my $default_id = "default:$full_table_name.$column";
    my $suggestion_text = "";

    if( !$suggested_values || ! @{$suggested_values} ) {
        if( defined $coltype && $coltype eq 'cdate' ) {
            # Current date:
            $suggested_values = [ strftime( '%F', gmtime() ) ];
            $suggestion_text =
                'Current server date (' . strftime( '%Z', gmtime()) . '): ';
        } elsif( defined $coltype && $coltype eq 'ctime' ) {
            # Current time:
            $suggested_values = [ strftime( '%T', gmtime() ) ];
            $suggestion_text =
                'Current server time (' . strftime( '%Z', gmtime()) . '): ';

        } elsif( defined $coltype && $coltype eq 'cdatetime' ) {
            # Current datetime:
            $suggested_values = [ strftime( "%F %T", gmtime() ) ];
            $suggestion_text =
                'Current server date and time (' .
                strftime( '%Z', gmtime()) . '): ';
        }
    }

    $validation = $validation ? " $validation;" : '';
    if( $suggested_values && @{ $suggested_values } > 0 ) {
        if( @{ $suggested_values } == 1 ) {
            $cell_content .=
                $suggestion_text . $suggested_values->[0] .
                hidden( { -id => $select_id,
                          -value => $suggested_values->[0] } ) . "\n" .
                button( { -id => $default_id,
                          -class => button_classes_string(),
                          -style => 'display:none',
                          -name => "insert_default",
                          -label => "Insert",
                          -onclick =>
                              "insert_suggested_value(" .
                                  "'$full_table_name.$column', '');" .
                              $validation,
                        } );
        } else {
            $cell_content .=
                $suggestion_text .
                popup_menu( -id => $select_id,
                            -class => 'dropdown',
                            -values => $suggested_values,
                            -onchange =>
                                "insert_suggested_value(" .
                                    "'$full_table_name.$column', 'text');" .
                                $validation ) .
                button( { -id => $default_id,
                          -class => button_classes_string(),
                          -style => 'display:none',
                          -name => "insert_default",
                          -label => "Insert",
                          -onclick =>
                              "insert_suggested_value(" .
                                  "'$full_table_name.$column', 'text');" .
                              $validation
                        } );
        }
        $cell_content .=
            '<script type="text/javascript">' .
            "document.getElementById('$default_id').style.display='inline'" .
            '</script>';
    }

    return $cell_content;
}

## @function additional_table_list (Database db, @$data, $db_table)
# Creates HTML/Javascript snippet that hides/shows related tables that are empty.
# @param db \ref Database object
# @param data data structure generated by the
#             \ref Database::get_record_descriptions() method.
# @param table_name table name.
# @param table_from table name that the list is related to.
# @return HTML snippet

sub additional_table_list
{
    my ( $db, $data, $table_name, $table_from ) = @_;

    my %labels;
    for my $related_table_name ( sort keys %{ $data->[0]{related_tables} } ) {
        my $table_properties = $db->get_table_properties($related_table_name);
        next if $table_properties->{is_read_only};

        my $related_data = $data->[0]{related_tables}{$related_table_name};
        for my $related_entry ( @{ $related_data } ) {
            my $foreign_key = $related_entry->{metadata}{foreign_key};
            my $column_order = $related_entry->{metadata}{column_order};

            if( defined $foreign_key ) {
                # Determines which foreign keys should be displayed.
                my @all_foreign_keys =
                    grep  { $_ ne $foreign_key &&
                            defined $related_entry->{columns}{$_}{coltype} &&
                            $related_entry->{columns}{$_}{coltype} eq 'fk' &&
                            $related_entry->{columns}{$_}{fk_target}{metadata}{table_name} eq $table_from }
                          @$column_order;
                $labels{"$table_name.$related_table_name.$foreign_key"} =
                    $related_table_name . ' (' . join( ', ', @all_foreign_keys ) . ')';
            } else {
                $labels{"$table_name.$related_table_name"} =
                    $related_table_name;
            }
        }
    }

    my $html = '';
    if( %labels ) {
        $html .=
            fieldset( legend( $table_from ),
                      input( { -type => 'button',
                               -class => button_classes_string(),
                               -value => 'Add table',
                               -onclick => "unhide_insert_form('$table_name');" } ) .
                      popup_menu( -class => 'related_table_selector',
                                  -id => "add-table:$table_name",
                                  -default => 0,
                                  -values => [ sort keys %labels ],
                                  -labels => \%labels ) );
    }

    return $html;
}

## @function data2html (Database db, @$data, $db_table, function_ref value2html, %$options)
# Creates HTML table (or tables, recursively) from data structure.
#
# @param db \ref Database object
# @param data data structure generated by the
#             \ref Database::get_record_descriptions() method.
# @param db_table table name
# @param value2html A function reference. This function will be used
#                   to convert each data value into an appropriate
#                   HTML snippet, either to create a data display page
#                   or to create a data input form.
# @param options hash of options
# @return HTML with table(s)
sub data2html
{
    my( $db, $data, $db_table, $value2html, $options ) = @_;
    my( $columns, $id_column, $hide_column_types, $hide_columns_by_id,
        $hide_views, $table_properties, $order, $foreign_key, $multi_fk,
        $url_level, $data_level, $vertical, $expandable_id, $no_add_button,
        $no_additional_table_list, $request_uri,
        $close_first_expandables, $full_table_name, $empty_set_values,
        $editable, $ambiguous_fks ) =
        ( $options->{columns},
          $options->{id_column},
          $options->{hide_column_types},
          $options->{hide_columns_by_id},
          $options->{hide_views},
          $options->{table_properties},
          $options->{order},
          $options->{foreign_key},
          $options->{multi_fk},
          $options->{level},
          $options->{data_level},
          $options->{vertical},
          $options->{expandable_id},
          $options->{no_add_button},
          $options->{no_additional_table_list},
          $options->{request_uri},
          $options->{close_first_expandables},
          $options->{full_table_name},
          $options->{empty_set_values},
          $options->{editable},
          $options->{ambiguous_fks}
        );

    my $reverse_fk = $db->get_reverse_foreign_keys( $db_table );

    my @columns;
    if( defined $columns && @$columns ) {
        @columns = @$columns;
    } else {
        # If columns are not user-selected, the list of all existing
        # columns of the table are taken from the database.
        # No columns should be hidden from the user if explicitly asked for.
        # In table displays, however, UUID columns are hidden for brevity.
        if( $vertical ) {
            @columns = $db->get_column_names( $db_table,
                                              { hide_column_types =>
                                                [ 'cssclass', 'mimetype', 'format' ] } );
        } else {
            @columns = $db->get_column_names( $db_table );
        }

        my %columns_to_hide;

        # Hiding the column that is a foreign key to already displayed
        # table. Hiding only non-composite keys as composite ones may be
        # composed of non-composite foreign keys themselves.
        if( $foreign_key && !$foreign_key->is_composite ) {
            $columns_to_hide{$foreign_key->child_column} = 1;
        }

        # Hiding composite key pseudocolumn if it is used:
        if( $foreign_key && $foreign_key->is_composite ) {
            $columns_to_hide{$foreign_key->name} = 1;
        }

        @columns = grep { !$columns_to_hide{$_} } @columns;
    }

    $id_column = $db->get_id_column( $db_table ) if !defined $id_column;

    # Managing table display classes.
    $table_properties = {} unless defined $table_properties;

    $url_level = 1 if !defined $url_level;
    $data_level = 1 if !defined $data_level;

    my $expandable_id_now = $expandable_id;
    $expandable_id_now = 'expandable' if !defined $expandable_id_now;
    $expandable_id_now .= '_';
    my $related_tables = 0; # number of expandables

    my $database_base = $request_uri;
    for (1..$url_level) {
        $database_base = dirname( $database_base );
    }

    # Collecting all possible columns:
    my @selected_columns = (defined $id_column ? $id_column : ());
    if( @$data ) {
        push @selected_columns,
             grep { !defined $id_column || $_ ne $id_column }
                  @columns > 0 ? @columns : @{ $data->[0]{metadata}{column_order} };
    }

    # Hides certain types of columns according to $hide_column_types.
    if( $hide_column_types && @$data ) {
        my @filtered_columns;
        my $counter = 0;
        for my $selected_column ( @selected_columns ) {
            if( ! any { defined $data->[0]{columns}{$selected_column}{coltype} &&
                                $data->[0]{columns}{$selected_column}{coltype} eq $_ }
                @{ $hide_column_types } ) {
                push @filtered_columns, $selected_column;
            }
        }
        @selected_columns = @filtered_columns;
    }

    # Preparing the header:
    my $sort_button_uri = $request_uri;
    if( defined $expandable_id ) {
        $sort_button_uri =~ s/#.*$//;
        $sort_button_uri .= '#related_' . $expandable_id;
    }
    my @header = information_table_header( $db,
                                           $db_table,
                                           \@selected_columns,
                                           $url_level,
                                           $order,
                                           !$vertical && !$editable,
                                           $sort_button_uri );

    # Collect classes to decide the column label class: if '.mandatory',
    # then activate ::after label asterix in CSS rules on small screens.
    my %mandatory_columns = map { $_ => 1 }
                            @{$db->get_mandatory_columns( $db_table )};
    my @column_name_classes = map { 'mobile-cell' .
                                    (exists $mandatory_columns{$_}
                                        ? ' mandatory' : '') }
                                  @selected_columns;

    # '$html' gathers table data.
    # '$new_record_button' gathers head_controls elements:
    # specifically in this function 'New' record button.
    my $html = '';
    my $new_record_button = '';
    my @rows;
    my $uuid_gen = Data::UUID->new;

    for( my $i = 0; $i < @$data; $i++ ) {
        next if !has_values( $data->[$i] ) && !$editable;

        my $data_item = $data->[$i];
        my $record_id = $id_column ? $data_item->{columns}{$id_column}{value} : undef;
        my $action = $data_item->{metadata}{action};

        next if $hide_columns_by_id && defined $record_id &&
                any { $record_id eq $_ } @$hide_columns_by_id;

        my $full_table_path = $full_table_name ? "$full_table_name." : '';
        $full_table_path .= $data_item->{metadata}{table_name};
        $full_table_path .= $multi_fk ? '.' . $foreign_key->name : '';
        $full_table_path .= ':' . $i;

        # FIXME: instead of checking the level, data2html() should
        # rely on proper filter transferred to recursive invocations
        # (S.G.).
        my @cells = map { &{$value2html}( $db, $db_table, $data_item, $_,
                                          {
                                            record_id => $record_id,
                                            database_base_uri => $database_base,
                                            data_level => $data_level,
                                            add_none_to_enumerators =>
                                                $editable,
                                            request_uri => $request_uri,
                                            suggest_values => $editable,
                                            full_table_name => $full_table_path,
                                            empty_set_values => $empty_set_values,
                                            ambiguous_fks => $ambiguous_fks
                                         } ) } @selected_columns;

        if( $editable ) {
            push @cells, div( { -class => "hidden" },
                              radio_group( -name => "action:$full_table_path",
                                           -values => [ 'update', 'insert' ],
                                           -default => $action ) );

            foreach (sort grep { $data_item->{columns}{$_}{coltype} &&
                                 $data_item->{columns}{$_}{coltype} eq 'uuid' }
                               keys %{$data_item->{columns}}) {
                my $uuid = $data_item->{columns}{$_}{value};
                $uuid = lc $uuid_gen->create_str() if !defined $uuid && $data_level == 1;
                $cells[-1] .= div( { -class => 'hidden' },
                                   hidden( -name => "column:$full_table_path.$_",
                                           -value => $uuid,
                                           'data-type' => 'uuid' ) );
            }
        }

        push @rows, \@cells;

        next unless $vertical;

        # Full table names are not visualised for now.
        # $html .= show_full_table_name( $full_table_path );

        my $entry_html = '';

        @rows = @{ _transpose_table( \@rows ) };

        if( @header ) {
            my $column_properties = $db->get_column_properties( $db_table );
            my @header_now;
            my @rows_now;
            my @selected_columns_now;
            my @column_name_classes_now;
            for my $i (keys @header) {
                next if $column_properties->{display}{$selected_columns[$i]} &&
                        $column_properties->{display}{$selected_columns[$i]} eq 'notnull' &&
                        !grep { defined $_ && $_ ne '' } @{$rows[$i]};
                push @header_now, $header[$i];
                push @rows_now, [ $header[$i], @{$rows[$i]} ];
                push @selected_columns_now, $selected_columns[$i];
                push @column_name_classes_now, $column_name_classes[$i];
            }
            push @rows_now, $rows[-1] if $editable;
            @header = @header_now;
            @rows = @rows_now;
            @selected_columns = @selected_columns_now;
            @column_name_classes = @column_name_classes_now;
        }

        #############------------------------------------------------------
        # Table generation.

        # Set table classes.
        # Properly proccess the no table class property case.
        # This table is essentialy 'horizontal'. So no need to horizontally
        # expand table size: there is always only two columns in a table -
        # 'Column', 'Value'.
        my %table_properties = %$table_properties;
        if ( defined $table_properties{'-class'} ) {
            $table_properties{'-class'} .= ' column-value';
        } else {
            $table_properties{'-class'} = 'column-value';
        }

        # Set data-label for each row in table body:
        #   td({ -'data-label' => "column-name" }).
        # Data labels are mandatory for the accurate mobile table display.
        $entry_html .= table_html( \@rows,
                                   \@header,
                                   \@column_name_classes,
                                   $vertical,
                                   \%table_properties,
                                   [ 'Column', 'Value' ],
                                   $editable );

        # At this point we have all of the table data assembled in the
        # '$entry_html'.
        if( ! $no_additional_table_list ) {
            $entry_html .= additional_table_list( $db, [ $data_item ],
                                                  $full_table_path,
                                                  $db_table ) . "\n";
        }

        #############------------------------------------------------------
        @rows = (); # must be emptied

        my $inner_html = '';

        for my $table (sort keys %{$data_item->{related_tables}}) {
            my $id_column = $db->get_id_column( $table );
            my $related_table_items = $data_item->{related_tables}{$table};
            my $related_table_action =
                $data_item->{related_tables}{$table}[0]{metadata}{action};
            my $related_table_filter =
                $data_item->{related_tables}{$table}[0]{metadata}{filter};
            my $related_table_column_order =
                $data_item->{related_tables}{$table}[0]{metadata}{column_order};
            my $related_table_properties = $db->get_table_properties( $table );

            my $related_editable = $editable &&
                                   !$related_table_properties->{is_read_only};

            next if !$related_editable &&
                    ( !@{$related_table_items} ||
                      !any { has_values( $_ ) } @{ $related_table_items } );
            next if $hide_views && $db->is_view( $table );
            next if !$related_editable && !has_values( $related_table_items->[0] );

            my @fks = @{$reverse_fk->{$table}};
            for my $fk (@fks) {
                next if !$fk->is_visualised;

                my $visualisation = $fk->{visualisation};
                my $relation = $fk->{relation};

                # Special treatment of tables that have multiple reverse fk
                # keys. Removes rows which fk column has the same value as
                # parent table id.
                my $hide_fk_rows = [];
                my $current_related_table_items = $related_table_items;
                if( @{$reverse_fk->{$table}} > 1 ) {
                    $current_related_table_items =
                        [ grep { $fk->name eq $_->{metadata}{foreign_key} }
                              @{ $related_table_items } ];
                    $multi_fk = 1;
                } else {
                    $multi_fk = 0;
                }

                my $vertical = $visualisation && $visualisation eq 'card';

                # In case of 'expandable' table block, returns the solid
                # section of related tables as '$table_html'.
                my $table_html =
                    data2html( $db, $current_related_table_items, $table,
                               $related_editable ? $value2html : \&data_value2html,
                               {
                                   id_column => $id_column,
                                   hide_column_types => $hide_column_types,
                                   order  => $order,
                                   foreign_key => $fk,
                                   multi_fk => $multi_fk,
                                   level  => $url_level,
                                   data_level  => $data_level + 1,
                                   vertical => $vertical,
                                   table_properties => $table_properties,
                                   request_uri => $request_uri,
                                   hide_columns_by_id => $hide_fk_rows,
                                   hide_views => $hide_views,
                                   expandable_id =>
                                       $expandable_id_now . $related_tables,
                                   ( $relation eq '1' ? ( no_add_button => 1 ) : () ),
                                   no_additional_table_list => $no_additional_table_list,
                                   close_first_expandables => $close_first_expandables,
                                   full_table_name => $full_table_path,
                                   empty_set_values => $empty_set_values,
                                   editable => $related_editable,
                                   ambiguous_fks => $ambiguous_fks
                               } );
                next if $table_html eq '';

                my $table_uri = transfer_query_string(
                    $database_base . '/' . $table,
                    $request_uri,
                    { $related_table_filter
                       ? ( append => $related_table_filter->query_string_uri ) : (),
                         exclude_re => 'id_column|offset|rows|' .
                          'table|order|select_.*|search_value|filter' } );

                my $field_name = "$full_table_path.$table";
                my( $fk_column_name ) =
                    ( @{$reverse_fk->{$table}} > 1 ? $fk->child_column : '' );
                my $remaining_fk_column_names =
                    $fk_column_name ?
                    join ', ',
                    reverse
                    sort { @{ $related_table_column_order } }
                    grep { $_ ne $fk_column_name }
                    map  { $_->child_column }
                         @fks :
                    '';
                if( $fk_column_name ) {
                    $field_name .= '.' . $fk_column_name;
                }

                my $make_table_hidden = 0;
                if( ( $related_editable && $related_table_action eq 'insert' && !$multi_fk ) ||
                    ( $multi_fk && !has_values( $current_related_table_items->[0] ) ) ) {
                    $make_table_hidden = 1;
                }

                # Collapsable, related table sequence section.
                $inner_html .= start_expandable(
                    " Related: $table" .
                    ( $remaining_fk_column_names ?
                      ' (' . $remaining_fk_column_names . ') ' : ' ' ) .
                    a( { -class => 'icon-external-link', -href => $table_uri }, '' ),
                    "expandable:$field_name",
                    { ( $data_level <= 1 && $close_first_expandables ?
                        ( is_closed => 1 ) : () ),
                      ( $make_table_hidden ?
                        ( is_hidden => 1 ) : () )
                    } );
                $inner_html .= div( { -class =>
                                          "related expandable",
                                      -id => $field_name,
                                      ( $make_table_hidden ?
                                        ( -style => "display: none;" ) : () ),
                                    },
                                    $table_html,
                                    $relation eq 'N' && $related_editable ?
                                    ( fieldset(
                                          { -class => 'js-button' },
                                          legend( $table .
                                                  ( $remaining_fk_column_names ?
                                                    ' ('.$remaining_fk_column_names.')':'' ) ),
                                          input( { -id => "js-add:$field_name",
                                                   -class => button_classes_string('js-button'),
                                                   -type => 'button',
                                                   -style => 'display: none;',
                                                   -value => 'Add entry',
                                                   -onclick => "add('$field_name')" } ) .
                                          input( { -id => "js-remove:$field_name",
                                                   -class => button_classes_string('js-button'),
                                                   -type => 'button',
                                                   -style => 'display: none;',
                                                   -value => 'Remove entry',
                                                   -onclick => "remove('$field_name')" } ),
                                          '<script type="text/javascript">' .
                                          "show_by_id( 'js-add:$field_name' );" .
                                          "show_by_id( 'js-remove:$field_name' );" .
                                          '</script>'
                                      ) ) : '',
                                    '<script>' .
                                    "empty_out_hidden_inputs( '$field_name' );" .
                                    '</script>' );
                $related_tables ++;
            }
        }

        if( $inner_html ne '' ) {
            $entry_html .= $inner_html . "\n";
        }

        if( $entry_html ne '' ) {
            $html .= div( { -class => 'entry' }, $entry_html );
        }
    }

    if( !$vertical && @rows ) {
        local $" = "\n";

        # Managing table display classes:
        # 'twofoldtable' is determined on displayable column count eq. 2
        # ( NB: in case of editable form column count increases by one ).
        my $column_count = scalar( @selected_columns );
        my $table_class =
            ( $column_count == 2 || ($column_count == 3 && $editable) ) ?
                'twofoldtable' : '';

        # Properly proccess the no table class property case.
        my %table_properties = %$table_properties;
        if( defined $table_properties{'-class'} ) {
            $table_properties{'-class'} .= ' '.$table_class
                unless $table_class eq '';
        } else {
            $table_properties{'-class'} = $table_class
                unless $table_class eq '';
        }

        $html .= table_html( \@rows,
                             \@selected_columns,
                             \@column_name_classes,
                             $vertical,
                             \%table_properties,
                             \@header,
                             $editable );
    }

    my $hidden_field = '';
    my $first_record;

    for my $record ( grep { has_values( $_ ) } @$data ) {
        if( ! any { $record->{columns}{$id_column}{value} eq $_ }
                 @{ $hide_columns_by_id } ) {
            $first_record = $record;
            last;
        }
    }

    if( $first_record && $foreign_key && !$foreign_key->is_composite ) {
        my $column_from = $foreign_key->child_column;
        $hidden_field = input( { -type  => 'hidden',
                                 -name  => "column:$db_table:0.$column_from",
                                 -value => $first_record->{columns}{$column_from}{value} } ) . "\n";
    }

    # Collect form buttons.
    my $dbtable_properties = $db->get_table_properties( $db_table );
    if( !$no_add_button && !$editable && !$dbtable_properties->{is_read_only} ) {
        $new_record_button .=
            start_form( -class => 'head_controls',
                        -action => "$database_base/$db_table" ,
                        -method => 'post' ) . "\n" .
                        input( { -type  => 'hidden',
                                 -name  => 'action',
                                 -value => 'template' } ) . "\n" .
                        $hidden_field .
                        submit( -name => 'New',
                                -value => 'New',
                                -class => button_classes_string() ) . "\n" .
                        end_form . "\n";
    }

    # If requested by the caller return:
    # - $concat_html == head + New record button + table html
    # - table html('$html') and controls buttons
    #   ('$new_record_button') separately.
    my $concat_html;
    if( $html || !$first_record )
    {
        # 'table-constrainer' is a parent container for the scrollable
        # data table. Data table has to have outer constrainer to react
        # against.
        # Note: outer constrainer is not needed for the flex tables.
        $html = div( {
                         defined $vertical && $vertical ?
                         ( -class => 'related_tables dbdata-cardized',
                           -id => 'table-constrainer' ) :
                         ( -class => 'related_tables dbdata-tabular',
                           -id => 'table-constrainer' )
                     },
                     $html );
        $concat_html = $new_record_button . $html;
    }

    # In array context return: html with table data (first), head_controls
    # html (second).
    # In scalar context return: concatenated html (head_controls + table), as
    # in upto -r3693 'data2html' function.
    return wantarray() ?
        ( $html, $new_record_button ) : $concat_html;
}

## @function data2html_page (Database db, @$data, $db_table, %$options)
# Creates HTML table (or tables, recursively) from data structure.
#
# @param db \ref Database object
# @param data data structure generated by the
#             \ref Database::get_record_descriptions() method.
# @param db_table table name
# @param options hash of options
# @return HTML with table(s)
sub data2html_page
{
    my( $db, $data, $db_table, $options ) = @_;

    my ( $record_id, $request_uri, $order, $filter, $selected_column ) =
    	( $options->{record_id},
          $options->{request_uri},
          $options->{order},
          $options->{filter},
          $options->{id_column} );

    my $html;
    my $navigation_buttons_row_html = '';
    my $record_modification_buttons = '';

    if( has_values( $data->[0] ) ) {
        # my $nbr_rec_info = 
        #         $db->get_neighbour_ids( $db_table, { 
        #                                   record_id =>$record_id, 
        #                                   order => $order, 
        #                                   filter => $filter, 
        #                                   selected_column => $selected_column });


        my $next_id = $db->get_neighbour_id( { db_table => $db_table,
                                               selected_column => $selected_column,
                                               filter => $filter,
                                               order => $order,
                                               current_row => $data->[0] });

        my $prev_id = $db->get_neighbour_id( { db_table => $db_table,
                                               selected_column => $selected_column,
                                               filter => $filter,
                                               order => $order,
                                               current_row => $data->[0],
                                               previous => 1 });


        my $current_row = $db->get_neighbour_id( { db_table => $db_table,
                                               selected_column => $selected_column,
                                               filter => $filter,
                                               order => $order,
                                               current_row => $data->[0],
                                               previous => 1,
                                               count => 1 });
#        my $next_id = $db->get_neighbour_id( $db_table, $selected_column, $filter, $order, $data->[0]);
#        my $prev_id = $db->get_neighbour_id( $db_table, $selected_column, $filter, $order, $data->[0],1);
#        my $current_row = $db->get_neighbour_id( $db_table, $selected_column, $filter, $order, $data->[0],1,1);
        my $row_number = $db->get_count( $db_table, $filter );

        my $nbr_rec_info = { next_id => $next_id,
                             prev_id => $prev_id,
                             curr_rec => $current_row,
                             n_rec => $row_number };
        my $paging_text = '';
        my $records_selected_count = $nbr_rec_info->{n_rec};
        # Scale the '$paging_text' column without the filter selection text.
        my ( $scale_lg, $scale_md, $scale_sm ) = ( 5, 6, 6 );

        if ( $filter->filter ) {
            my $filter_exp = $filter->query_string;
            $paging_text = "Record $nbr_rec_info->{curr_rec} of " .
                           "$records_selected_count selected with <b>$filter_exp</b>";
            # Scale the '$paging_text' column if filter selection text present.
            ( $scale_lg, $scale_md, $scale_sm ) = ( 10, 8, 8 );
        } else {
            $paging_text = "Record $nbr_rec_info->{curr_rec} of $records_selected_count" . "\n";
        }

        $request_uri =~ /(\?.*)$/ if $request_uri;

        my $prev_button_form =
                forward_backward_control_form(
                    transfer_query_string( dirname( $request_uri ) . '/' .
                                           (defined $nbr_rec_info->{prev_id}
                                                 ?  $nbr_rec_info->{prev_id}
                                                 : ''),
                                           $request_uri ),
                    # Sets responsive margins and padding for a button.
                    ($records_selected_count > 1 ?
                         nameless_submit( 'Prev', "Previous record" ) :
                         nameless_submit_disabled( 'Prev', "Inactive previous record" ))
                ) . "\n";

        my $next_button_form =
                forward_backward_control_form(
                    transfer_query_string( dirname( $request_uri ) . '/' .
                                           (defined $nbr_rec_info->{next_id}
                                                 ?  $nbr_rec_info->{next_id}
                                                 : ''),
                                           $request_uri ),
                    # Sets responsive margins and padding for a button.
                    ($records_selected_count > 1 ?
                         nameless_submit( 'Next', "Next record" ) :
                         nameless_submit_disabled( 'Next', "Inactive next record" ))
                ) . "\n";

        # 'scale' parameter meaning : signifies the column dimension class for the
        #                             MIDDLE column.
        #                             So the auxiliary columns fill up all of the remaining
        #                             row space (classes need inside function dimension
        #                             recalculations).
        $navigation_buttons_row_html =
            three_column_row_html({ column_left => $prev_button_form,
                                    column_middle => $paging_text,
                                    column_right => $next_button_form,
                                    scale_lg => $scale_lg,
                                    scale_md => $scale_md,
                                    scale_sm => $scale_sm
                                 });

        my $table_properties = $db->get_table_properties( $db_table );
        if( !$table_properties->{is_read_only} ) {
            $record_modification_buttons =
                    start_form(
                                -action => dirname( $request_uri ),
                                -method => 'post',
                                -class => 'head_controls' ) .
                        input( { -type  => 'hidden',
                                 -name  => 'action',
                                 -value => 'template' } ) .
                        submit( -name => 'New',
                                -value => 'New',
                                -class => button_classes_string() ) . "\n" .
                        end_form . "\n";

            $record_modification_buttons .=
                    start_form( -action => $request_uri,
                                -method => 'post',
                                -class => 'head_controls' ) .
                        submit( -name => 'Edit',
                                -value => 'Edit',
                                -class => button_classes_string() ) . "\n" .
                        end_form . "\n";
        }
    }

    my %options = %{ $options };
    $options{no_add_button} = 1;
    $options{no_additional_table_list} = 1;

    $html = data2html( $db, $data, $db_table,
                        \&data_value2html,
                        \%options );

    # In array context return: html with table data (first), head controls
    # html (second).
    # In scalar context return: concatenated html (head_controls + table), as
    # in upto -r3693 'data2html' function.
    return ( $html,
             $navigation_buttons_row_html,
             $record_modification_buttons );
}

## @function data2html_edit_form (Database db, @$data, $db_table, %$options)
# Creates HTML entry form fields (or tables, recursively) from data structure.
#
# @param db \ref Database object
# @param data data structure generated by the
#             \ref Database::get_record_descriptions() method.
# @param db_table table name
# @param options hash of options
# @return HTML with table(s)
sub data2html_edit_form
{
    my( $db, $data, $db_table, $options ) = @_;

    my ( $form_head, $form_tail ) =
        ( $options->{form_head},
          $options->{form_tail} );

    $options->{empty_set_values} = 1;
    $options->{hide_column_types} = [ 'uuid', 'dbrev', 'filename', 'md5' ];
    $options->{hide_views} = 1;
    # FIXME: all enumerators will have 'None' for now so, empty tables would
    # not be added.
    $options->{editable} = 1;

    my $html =
        start_form( -class => 'edit',
                    -method => 'POST' ) .
        $form_head .
        div( { -id => "$db_table:0" },
             data2html( $db, $data, $db_table,
                        \&data_value2html_input, $options ) ) .
        $form_tail .
        end_form();

    return $html;
}

## @function data2html_new_form (Database db, @$data, $db_table, %$options)
# Creates HTML entry form fields (or tables, recursively) from data structure.
#
# @param db \ref Database object
# @param data data structure generated by the
#             \ref Database::get_record_descriptions() method.
# @param db_table table name
# @param options hash of options
# @retval HTML with table(s)
sub data2html_new_form
{
    my( $db, $data, $db_table, $options ) = @_;

    my ( $form_head, $form_tail, $post_action ) =
        ( $options->{form_head},
          $options->{form_tail},
          $options->{post_action} );

    $options->{hide_column_types} = [ 'id', 'uuid', 'dbrev', 'filename', 'md5' ];
    $options->{editable} = 1;

    my $html =
        start_form( -class => 'edit',
                    -method => 'POST',
                    -action => $post_action ) .
        $form_head .
        data2html($db, $data, $db_table, \&data_value2html_input, $options) .
        $form_tail .
        end_form();

    return $html;
}

## @function data2html_list_table (Database db, @$data, $db_table, %$options)
# Creates HTML table from data structure.
#
# @param db \ref Database object
# @param data data structure generated by the
#             \ref Database::get_record_descriptions() method.
# @param db_table table name
# @param options hash of options
# @return HTML with table(s)
sub data2html_list_table
{
    my( $db, $data, $db_table, $options ) = @_;
    return data2html( $db,
                      $data,
                      $db_table,
                      \&data_value2html,
                      $options );
}

## @function information_table_header (Database db, $db_table, $@columns, $level, $@order, $make_sort_buttons)
# Generates a cell array for HTML table header.
#
# @param db \ref Database object
# @param db_table table name
# @param columns an array of table columns
# @param level integer value of URI depth
# @param order data structure representing the ordering
# @param make_sort_buttons optional flag indicating whether sort buttons
#        should be generated
# @retval cells array of header cells
sub information_table_header
{
    my ($db, $db_table, $columns, $level, $order, $make_sort_buttons, $uri) = @_;

    $order = Database::Order->new_from_string() if !defined $order;
    $uri = $ENV{REQUEST_URI} if !defined $uri;

    my $website_base = $ENV{REQUEST_URI};
    for (0..$level) {
        $website_base = dirname( $website_base );
    }

    my %mandatory_columns = map { $_ => 1 }
                                @{$db->get_mandatory_columns( $db_table )};
    my $comments = $db->get_column_comments( $db_table );

    my ($up, $down) = map { $website_base . "/images/" . $_ }
                          ( "up.png", "down.png" );
    my $button_images = {
        up => $up,
        down => $down
    };
    my $parent_uri = $ENV{REQUEST_URI};
    for (2..$level) {
        $parent_uri = dirname( $parent_uri );
    }

    if( $level >= 2 ) {
        $parent_uri =
            transfer_query_string( $parent_uri, $ENV{REQUEST_URI},
                                   { exclude_re => 'id_column|offset|rows|' .
                                        'table|select_.*|search_value|' .
                                        'filter|depth' } );
    }

    my @cells;
    for (@$columns) {
        my $span_options = {};

        if( $comments->{$_} ) {
            $span_options->{-title} = $comments->{$_};
        }

        if( %mandatory_columns && exists $mandatory_columns{$_} ) {
            $span_options->{-class} = 'mandatory';
            if( $span_options->{-title} ) {
                $span_options->{-title} .= ' (mandatory field)';
            } else {
                $span_options->{-title} = 'mandatory field';
            }
        }

        my $cell = span( $span_options,
                         make_table_uris( $db, $db_table, $_,
                                          $parent_uri ) );

        if( $make_sort_buttons ) {
            $cell .= make_sort_buttons( $db_table,
                                        $_,
                                        $uri,
                                        $button_images,
                                        $order );
        }
        push @cells, $cell;
    }
    return @cells;
}

sub show_full_table_name
{
    my( $full_table_name ) = @_;

    my @table_links;
    my @tables = split /\./, $full_table_name;
    for my $i (0..$#tables) {
        my $table = $tables[$i];
        $table =~ s/:(\d+)$//;
        my $number = $1;
        push @table_links,
             a( { href => '#' . join( '.', ( @tables[0..$i-1], $table ) ) },
                $table );
    }
    return span( { class => 'full_table_name' },
                 join ' / ', @table_links );
}

## @function table_html
# Generates HTML for data tables
sub table_html
{
    my( $rows, $column_names, $column_name_classes, $vertical,
        $table_properties, $column_headers, $editable ) = @_;

    $table_properties = {} unless $table_properties;
    my @column_names = @$column_names;
    my @column_headers = @$column_headers;
    my $no_column_arrows = 0;

    if( exists $table_properties->{'-class'} ) {
        my $table_class = $table_properties->{'-class'};
        if( defined $table_class ) {
            $no_column_arrows = 1 if $table_class =~ /column-value/;
        }
    }
    if( $editable ) {
        if( exists $table_properties->{'-class'} ) {
            $table_properties->{'-class'} .= ' editable';
        } else {
            $table_properties->{'-class'} = 'editable';
        }
        if( !$vertical ) {
            push @column_names, '';
            push @column_headers, '';
        }
    }

    my @body_data =
        map {
                my $row = $rows->[$_];
                my $index = $_;
                [
                    map {
                            $index = $_ if !$vertical;
                            my $class_for_mobile_column_header =
                                $column_name_classes->[$index] ?
                                $column_name_classes->[$index] :
                                'hidden';
                            my $mobile_column_header;
                            if ( $no_column_arrows ) {
                                $mobile_column_header .=
                                    $column_names[$index] ?
                                    $column_names[$index] : '';
                            } else {
                                $mobile_column_header .=
                                    $column_headers[$index] ?
                                    $column_headers[$index] : '';
                            }
                            td(
                                label(
                                    { -class => $class_for_mobile_column_header },
                                    $mobile_column_header ) .
                                (defined $row->[$_] ?
                                    span( { -class => 'cell-breaker' }, br ) .
                                    $row->[$_] :
                                    (!$vertical ?
                                        span( { -class => 'cell-breaker' }, br ) :
                                        ''))
                            );
                        } 0..$#column_headers
                ]
            } 0..$#$rows;

    return table( $table_properties,
                  thead( Tr( th( \@column_headers ) ) ),
                  tbody( map { Tr( @$_ ) } @body_data ) );
}

## @function _transpose_table ($table)
# Swaps rows and columns of a table.
sub _transpose_table
{
    my( $table ) = @_;
    my $table_now = [];
    for my $i (keys @$table) {
        for my $j (keys @{$table->[$i]}) {
            $table_now->[$j][$i] = $table->[$i][$j];
        }
    }
    return $table_now;
}

## @function error_page ($message)
# Generates error page with specified message.
sub error_page
{
    my( $cgi, $message, $page_level ) = @_;

    my $home_path;
    if ( !defined $page_level ) {
        $page_level = 1;
    }
    if ( $page_level == -1 ) {
        $home_path = ".";
    } else {
        $home_path = join '/', ('..') x $page_level;
    }

    my ( $status, $er_name, $er_sol, $er_info_url );
    if ( $message->isa( RestfulDB::Exception:: ) ) {
        $status = $message->http_status;
        $er_name = $message->status_class . ": " . $message->status_name;
        $er_sol = $message->suggested_solution;
        $er_info_url = $message->status_info_url;
    } else {
        $status = 500;
        $er_name = "Server Error: Internal Server Error";
        $er_sol = "Something went terribly wrong. Please contact " .
                  "the site administrator to resolve this issue.";
        $er_info_url = "https://tools.ietf.org/html/rfc7231#section-6.6.1";
    }

    my $html = $cgi->header( -type => "text/html", -expires => 'now',
                        -status => $status, -charset => 'UTF-8');

    $html .= start_html5($cgi, {
              -title => $er_name,
              -head => Link({
                             -rel  => 'icon',
                             -type => 'image/x-icon',
                             -href => "$home_path/images/favicon.ico"
              }),
              -meta => {'viewport' => 'width=device-width, initial-scale=1'},
              -style => [
                  {-src => "$home_path/styles/default/mini-default.css"},
                  {-src => "$home_path/styles/default/style.css"}
              ]
        } );

    # Open Main containers.
    $html .= start_root( {class => "error-page"} );
    $html .= start_div( {-class => "card warning"} );

    # Error data.
    $html .= h1($er_name);
    $html .= h2("HTTP status $status");

    # Start error specs.
    $html .= start_div( { -class => "error-specification"} );

    # Collapsible error text.
    $html .= input( { -type => "checkbox",
                      -id => "expandable:error1",
                      -checked => "",
                      -class => "isexpanded" } ) .
             label( { -for => "expandable:error1",
                      -class => "expandable" },
                      start_span() .
                      end_span() .
                      " The following errors have been detected:" ) .
             start_div( { -for => "error1",
                          -class => "related expandable" } );

    # Create error code block.
    $html .= start_div( {-class => "row"} );
    $html .= start_div( {-class => "col-sm col-md col-lg error-code-container"} );

    chomp $message;
    my $encoded_message = encode_entities( $message );
    $encoded_message =~ s/\n/<br\/>/g;
    $html .= p( $encoded_message );
    $html .= end_div() . end_div() . end_div();

    $html .= end_div(); # Close error specs div.

    # Present alternatives.
    $html .= p( $er_sol );
    $html .= p( a( { -href => $er_info_url },
                   "More information on HTTP status $status") .
                br .
                a( { -href  =>  $home_path,
                     -title =>  $RestfulDB::Defaults::website_title },
                   "Back to " . $RestfulDB::Defaults::website_title . " Home") .
                "\n" );

    # This ends an HTML document by printing the </body></html> tags.
    $html .= end_card();
    $html .= end_root() . "\n";
    $html .= end_html . "\n";

    print $html;
}

## @function forward_backward_control_form ($uri, $inner_html)
# Creates backward/forward form with optional inner HTML.
#
# @param uri link that is followed when acting on the HTML form element
# @param inner_html HTML that will be included in the HTML form
# @return HTML form generated by \ref start_form_QS()
sub forward_backward_control_form
{
    my( $uri, $inner_html ) = @_;
    return start_form_QS( -class  => 'head_controls',
                          -action => $uri,
                          -method => 'GET' ) .
           (defined $inner_html ? "\n" . $inner_html : '') .
           "\n" . end_form;
}

## @function start_form_QS ()
# Wrapper for CGI::start_form, which transforms query string part of
# the action to values in hidden fields.
sub start_form_QS
{
    my %params = @_;
    my %hidden_fields;
    if( exists $params{-action} )
    {
        my( $base_uri, $query_string ) = split /\?/, $params{-action};
        $params{-action} = $base_uri;
        if( $query_string )
        {
            my @query = split /&/, $query_string;
            %hidden_fields =
                map { my( $k, $v ) = split /=/, $_, 2;
                      my $decoded_v = $v;
                      $decoded_v =~ s/\+/ /g;
                      $decoded_v = uri_decode( $decoded_v );
                      my $decoded_k = $k;
                      $decoded_k =~ s/\+/ /g;
                      $decoded_k = uri_decode( $decoded_k );
                      ( encode_entities(decode('UTF-8', $decoded_k )) =>
                            encode_entities(decode('UTF-8', $decoded_v)) )
                    } @query;
        }
    }
    my $html .= start_form( %params );
    if( %hidden_fields )
    {
        $html .= "\n" . join( "\n", map { '<input type="hidden" ' .
                                          "name=\"$_\" " .
                                          "value=\"$hidden_fields{$_}\"/>"
                                      } sort keys %hidden_fields );
    }
    return $html;
}

## @function nameless_submit ($value)
# Creates <input type="submit" /> HTML element without name="..."
# attribute in order to protect its value from being appended to the
# query string. Standard CGI::submit() function does not seem to be
# able to create nameless submit buttons.
#
# @param value value of value="..." attribute (mandatory),
#        title string (optional)
# @return HTML
sub nameless_submit
{
    my $value = $_[0];
    my $title = $_[1];
    if ( defined $title ) {
        return input( { -type => 'submit',
                        -title => $title,
                        -class =>  button_classes_string(),
                        -value => $value } );
    } else {
        return input( { -type => 'submit',
                        -class =>  button_classes_string(),
                        -value => $value } );
    }
}

## @function start_expandable ($text, $id)
# Creates a HTML checkbox for an expandable HTML with a given text and
# element ID. Must be followed by \<div class="expandable"> to work.
#
# @param text text to be shown as a header for an expandable.
# @param id HTML element id for the checkbox.
# @return HTML
sub start_expandable
{
    my ($text, $id, $options) = @_;
    my ($is_closed, $is_hidden) = ($options->{'is_closed'}, $options->{'is_hidden'});
    return input( { -type  => 'checkbox',
                    -class => 'isexpanded',
                    -id    => $id,
                    ( $is_closed ? (): ( -checked => 'checked' ) ) } ) . "\n" .
           label( { -for => $id,
                    -class => 'expandable',
                    ( $is_hidden ? ( -style => 'display: None;' ) : () ) },
                  '<span></span>' . $text );
}

## @function start_html5 ($cgi, $parameters)
# Creates standard HTML header (without DTDs). As Perl's CGI.pm is unable
# to create plain \<!DOCTYPE html> header, a hack is applied to replace
# CGI.pm generated HTML header.
#
# Need to call header function via $cgi object and then send $cgi to
# start_html5, otherwise output additionally produces:
# '<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />'.
# See:
# https://stackoverflow.com/questions/10197586/how-to-remove-cgi-default-meta-charset-encoding-in-perl
sub start_html5
{
    my( $cgi, $parameters ) = @_;
    my $html = $cgi->start_html( %$parameters );
    $html =~ s/(<!DOCTYPE )[^>]+/$1html/m;
    return $html;
}

## @function version ()
# Prints out current version of RestfulDB.
# @return HTML with current RestfulDB version number
#
sub version
{
    return p( 'RestfulDB version: v' . $RestfulDB::Version::Version );
}

########----------------------------------------------------------------
# mini.css functions.

sub start_root
{
    my ( $options ) = @_;
    my %props = ( id => "root" );
    if ( defined $options ) {
        if ( $options->{'style'} ) {
            $props{style} = $options->{'style'};
        }
        if ( $options->{'class'} ) {
            $props{class} .= " ".$options->{'class'};
        }
    }
    return start_div( \%props ) . "\n";
}

sub end_root
{
    return end_div();
}

# Wrap native CGI.pm link function, with addition of SVG icon and span link name.
# @params svg string (mandatory)
# @retval link with embedded svg pic
sub svg_link
{
    my ( $options ) = @_;
    my ( $class,
         $title,
         $href,
         $svg_string,
         $link_name,
         $target ) = ( $options->{'class'},
                       $options->{'title'},
                       $options->{'href'},
                       $options->{'svg_string'},
                       $options->{'link_name'},
                       $options->{'target'} );
    # Set tag properties.
    my %props;
    if ( defined $class ) {
        $props{'class'} = $class;
    }
    if ( defined $title ) {
        $props{'title'} = $title;
    }
    if ( defined $target ) {
        $props{'target'} = $target;
    }
    if ( defined $href ) {
        $props{'href'} = $href;
    } else {
        $props{'href'} = '#';
    }

    # Collect link tag.
    my $svg_link = start_a( \%props );
    $svg_link .= $svg_string;
    if ( defined $link_name ) {
        $svg_link .= span($link_name);
    }
    $svg_link .= end_a();

    return $svg_link;
}

# Gets inner elements (mandatory arg) for the header row.
# Returns heder row html.
sub collect_header_row_html {
    my ( $inner_el_string ) = @_;
    my $header_html = '<header class="row">'."\n";
    $header_html .= $inner_el_string."\n";
    $header_html .= '</header>'."\n";
    return $header_html;
}

# SVG icons adapted from Feather icons (https://feathericons.com),
# as downloaded from https://github.com/feathericons/feather, v4.24.1.
# Copyright (c) 2013-2017 Cole Bemis, MIT license.
# Icons have been modified by the RestfulDB contributors.

sub home_icon_html {
    my $svg_html =
    '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" '.
    'viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" '.
    'stroke-linecap="round" stroke-linejoin="round">'.
    '<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>'.
    '<polyline points="9 22 9 12 15 12 15 22"></polyline></svg>';
    return $svg_html;
}

sub db_icon_html {
    my $svg_html =
    '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" '.
    'viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" '.
    'stroke-linecap="round" stroke-linejoin="round">'.
    '<ellipse cx="12" cy="5" rx="9" ry="3">'.
    '</ellipse><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3" ></path>'.
    '<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path></svg>';
    return $svg_html;
}

sub table_icon_html {
    my $svg_html =
    '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" '.
    'viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" '.
    'stroke-linecap="round" stroke-linejoin="round">'.
    '<line x1="8" y1="6" x2="21" y2="6"></line>'.
    '<line x1="8" y1="12" x2="21" y2="12"></line><line x1="8" y1="18" x2="21" '.
    'y2="18"></line><line x1="3" y1="6" x2="3" y2="6"></line>'.
    '<line x1="3" y1="12" x2="3" y2="12"></line><line x1="3" y1="18" x2="3" '.
    'y2="18"></line></svg>';
    return $svg_html;
}

sub table2_icon_html {
    my $svg_html =
    '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" '.
    'viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" '.
    'stroke-linecap="round" stroke-linejoin="round">'.
    '<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>'.
    '<line x1="3" y1="9" x2="21" y2="9"></line>'.
    '<line x1="9" y1="21" x2="9" y2="9"></line></svg>';
    return $svg_html;
}

sub selection_icon_html {
    my $svg_html =
    '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" '.
    'viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" '.
    'stroke-linecap="round" stroke-linejoin="round">'.
    '<polyline points="9 11 12 14 22 4"></polyline>'.
    '<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11">'.
    '</path></svg>';
    return $svg_html;
}

sub record_icon_html {
    my $svg_html =
    '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" '.
    'viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" '.
    'stroke-linecap="round" stroke-linejoin="round">'.
    '<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z">'.
    '</path><polyline points="13 2 13 9 20 9"></polyline></svg>';
    return $svg_html;
}

sub record_edit_icon_html {
    my $svg_html =
    '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" '.
    'viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" '.
    'stroke-linecap="round" stroke-linejoin="round">'.
    '<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7">'.
    '</path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z">'.
    '</path></svg>';
    return $svg_html;
}

# Add icons to the header.
# And insert into grid format.
sub header_tables_pl
{
    my ( $db_name ) = @_;
    my $logo_html = svg_link( {class => "button col-sm col-md",
                               title => "Database: $db_name",
                               href => "$db_name",
                               link_name => $db_name,
                               svg_string => db_icon_html() } );
    my $link_html = svg_link( {class => "button col-sm col-md",
                               title => $RestfulDB::Defaults::website_title,
                               href => "./",
                               link_name => "Home",
                               svg_string => home_icon_html() } );
    return collect_header_row_html( $logo_html.$link_html );
}

# Add icons to the header.
# And insert into grid format.
sub header_db_pl
{
    my ( $db_name, $db_table ) = @_;
    my $logo_html = svg_link( { class => "button col-sm col-md",
                                title => "Table: $db_table, page 1",
                                href => "../$db_name/$db_table",
                                link_name => $db_table,
                                svg_string => table2_icon_html() } );
    my $db_link_html = svg_link( {class => "button col-sm col-md",
                                  title => "Database: $db_name",
                                  href => "../$db_name",
                                  link_name => $db_name,
                                  svg_string => db_icon_html() } );
    my $home_link_html = svg_link( {class => "button col-sm col-md",
                                    title => $RestfulDB::Defaults::website_title,
                                    href => "../",
                                    link_name => "Home",
                                    svg_string => home_icon_html() } );
    return collect_header_row_html( $logo_html.
                                    $db_link_html.
                                    $home_link_html );
}

# Add icons to the header.
# And insert into grid format.
sub header_record_pl
{
    my ( $db_name, $db_table, $selection, $record_id ) = @_;
    my $logo_html = svg_link( {class => "button col-sm col-md",
                               title => "Record id: $record_id",
                               href => "../$db_table/$record_id$selection",
                               link_name => "$record_id",
                               svg_string => record_icon_html() } );
    my $selection_link_html = svg_link( {class => "button col-sm col-md",
                                         title => "Current selection",
                                         href => "../$db_table$selection",
                                         link_name => "Selected",
                                         svg_string => selection_icon_html() } );
    my $table_link_html = svg_link( { class => "button col-sm col-md",
                                      title => "Table: $db_table",
                                      href => "../$db_table",
                                      link_name => $db_table,
                                      svg_string => table2_icon_html() } );
    my $db_link_html = svg_link( {class => "button col-sm col-md",
                                  title => "Database: $db_name",
                                  href => "../../$db_name",
                                  link_name => $db_name,
                                  svg_string => db_icon_html() } );
    my $home_link_html = svg_link( {class => "button col-sm col-md",
                                    title => $RestfulDB::Defaults::website_title,
                                    href => "../../",
                                    link_name => "Home",
                                    svg_string => home_icon_html() } );
    return collect_header_row_html( $logo_html.
                                    $selection_link_html.
                                    $table_link_html.
                                    $db_link_html.
                                    $home_link_html );
}

sub start_doc_wrapper
{
    my ( $options ) = @_;
    my %props = ( id => "doc-wrapper", class => "row" );
    if ( defined $options ) {
        if ( $options->{'style'} ) {
            $props{style} = $options->{'style'};
        }
        if ( $options->{'class'} ) {
            $props{class} .= " ".$options->{'class'};
        }
    }
    return start_div( \%props ) . "\n";
}

sub end_doc_wrapper
{
    return end_div() . "\n";
}

sub start_main_content
{
    # Set class as an appropriate grid element !
    return "<main id='doc-content' class='col-sm col-md col-lg'>\n";
    ###return start_div({ -id => "doc-content", -class => "col-sm col-md col-lg" }) . "\n";
}

sub end_main_content
{
    return "</main>\n";
}

sub licenses_in_footer
{
    my $inner_lic_div = div({ -class => "section" },
        p(
          $RestfulDB::Defaults::website_title . " is a part of the",
          a({ -href => "http://www.solsa-mining.eu/",
            -target => "_blank"},
            "SOLSA"),
          "project,"
          . " funded by the EU",
          a({ -href => "https://ec.europa.eu/programmes/horizon2020/en",
              -target => "_blank"},
              "Horizon 2020"),
          "research and innovation program. Grant agreement No. 689868."
          . br . "RestfulDB version: v$RestfulDB::Version::Version."
          . " Stylesheet by ",
          a({ -href => "https://minicss.org",
              -target => "_blank"},
              "mini.css"),
          " | ",
          a({ -href => "https://github.com/Chalarangelo",
              -target => "_blank"},
              "\@Chalarangelo"),
          " | ",
          a({ -href => "https://github.com/Chalarangelo/mini.css/blob/master/LICENSE",
              -target => "_blank"},
              "MIT License."),
          "Icons by",
           a({ -href => "https://feathericons.com",
               -target => "_blank"},
               "Feather.")
        )
    );
    return $inner_lic_div;
}

# Returns 'Back to top' link appearing in footer.
sub back_to_top_in_footer
{
    my $svg_string =
    '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" ' .
    'viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" ' .
    'stroke-linecap="round" stroke-linejoin="round" style="height:13px; ' .
    'color:#0227bd;"><polyline points="18 15 12 9 6 15"></polyline></svg>';
    return svg_link( {href => "#top",
                      link_name => "Back to top",
                      svg_string => $svg_string} );
}

# @param  inner 'breadcrumb' div
# @param  inner div with licenses
# @return html footer block
sub collect_footer_divs
{
    my ( $inner_lic_div, $inner_nav_div, $options ) = @_;
    my $nav_class = $options->{'nav_class'};
    my $nav_subcolumn_class = "col-sm-12 col-md-3 col-lg-3";
    # Set tag properties.
    if ( defined $nav_class ) {
        $nav_subcolumn_class .= " ".$nav_class;
    }
    my $second_level_divs =
        div({ -class => "col-sm-12 col-md col-lg" },
            $inner_lic_div)
        . "\n"
        . div({ -class => $nav_subcolumn_class },
              $inner_nav_div)
        . "\n";
    my $html =
        "<footer>\n"
        . div({ -class => "row" },
              "\n",
              $second_level_divs)
        . "\n</footer>\n";
    return $html;
}

sub footer_tables_pl
{
    my ( $db_name ) = @_;
    #
    my $inner_lic_div = licenses_in_footer();
    #
    my $svg_link = back_to_top_in_footer();
    # Specific to 'db.pl' generated page.
    my $inner_nav_div = div({ -class => "section" },
        p( $svg_link ),
        p( "You are here: ",
           a({ -href => "./",
               -title => $RestfulDB::Defaults::website_title . ": Home" },
             span("Home")),
           a({ -href => "./$db_name",
               -title => "Database: $db_name"},
             span("&gt; $db_name")),
        )
    );
    return collect_footer_divs( $inner_lic_div, $inner_nav_div );
}

sub footer_db_pl
{
    my ( $db_name, $db_table ) = @_;
    #
    my $inner_lic_div = licenses_in_footer();
    #
    my $svg_link = back_to_top_in_footer();
    # Specific to 'db.pl' generated page.
    # No need to display 'Back to top' link in a db.pl footer on the medium
    # through large screens. See styles for 'footer p#dbpl-back-to-top'.
    my $inner_nav_div = div({ -class => "section" },
        p( {-id => "dbpl-back-to-top"}, $svg_link ),
        p( "You are here: ",
           a({ -href => "../",
               -title => $RestfulDB::Defaults::website_title . ": Home"},
             span("Home")),
           a({ -href => "../$db_name",
               -title => "Database: $db_name"},
             span("&gt; $db_name")),
           a({ -href => "../$db_name/$db_table",
               -title => "Table: $db_table" },
             span("&gt; $db_table")),
        )
    );
    return collect_footer_divs( $inner_lic_div,
                                $inner_nav_div,
                                { nav_class => "dbpl-footer-nav"} );
}

sub breadcrumbs_in_record_page
{
    my ( $db_name, $db_table, $selection, $record_id ) = @_;
    my $inner_paragraphs = '';
    my $selection_links = '';
    #-------------------------------------------------------------------
    # Specific to 'modify.pl' generated page.
    $selection_links = a({ -href => "../$db_table$selection",
                           -title => "Current selection"},
                         span("&gt; Selected"))
                         . " ";
    $selection_links .= a({ -href => "../$db_table/$record_id$selection",
                            -title => "Record id: $record_id"},
                          span("&gt; $record_id"));
    #-------------------------------------------------------------------

    $inner_paragraphs .= p( "You are here: ",
                            a({ -href => "../../",
                                -title => $RestfulDB::Defaults::website_title . ": Home" },
                              span("Home")),
                            a({ -href => "../../$db_name",
                                -title => "Database: $db_name"},
                              span("&gt; $db_name")),
                            a({ -href => "../$db_table",
                                -title => "Table: $db_table" },
                              span("&gt; $db_table")),
                            $selection_links ) . "\n";

    return $inner_paragraphs;
}

# TODO: rename footer functions as 'record.pl' no longer in active use.
sub footer_record_pl
{
    my ( $db_name, $db_table, $selection, $record_id ) = @_;
    #
    my $inner_lic_div = licenses_in_footer();
    #
    my $inner_paragraphs = '';
    my $svg_link = back_to_top_in_footer();
    $inner_paragraphs .= p( $svg_link ) . "\n";
    $inner_paragraphs .=
        breadcrumbs_in_record_page( $db_name, $db_table, $selection,
                                    $record_id );
    my $inner_nav_div = div({ -class => "section" },
                            $inner_paragraphs);
    return collect_footer_divs( $inner_lic_div, $inner_nav_div );
}

# Optional parameter: additional class name for the .card.fluid.
sub start_card_fluid
{
    my ( $options ) = @_;
    my %props = ( class => "card fluid" );
    if ( defined $options ) {
        if ( $options->{'style'} ) {
            $props{style} = $options->{'style'};
        }
        if ( $options->{'class'} ) {
            $props{class} .= " ".$options->{'class'};
        }
        if ( $options->{'id'} ) {
            $props{id} = $options->{'id'};
        }
    }
    return start_div( \%props ) . "\n";
}

sub end_card
{
    return end_div() . "\n";
}

########--------------------------------------------------------
# Create the data row that houses the three grid system:
# two smaller columns on the sides for the buttons (next, prev)
# and larger column in the middle for all of the filter
# selection text.
#
# This kind of system lets buttons stay in their places and
# filter text doesn't overflow of its column in the middle
# (screen size and text length notwithstanding).
#
# Note: buttons are added with responsive padding ir margins
# classes (see 'nameless_submit') that ensure smoother display
# for the buttons on the smaller screens.
#
# 'scale' parameter meaning : signifies the column dimension class for the
#                             MIDDLE column.
#                             So the auxiliary columns fill up all of the remaining
#                             row space.
#
# Gets: prev button form, next button form and pagination text (sans outer css).
# Returns: three column grids system row.
sub three_column_row_html
{
    my ( $options ) = @_;
    my ( $column_left,
         $column_middle,
         $column_right,
         $scale_lg,
         $scale_md,
         $scale_sm,
         $row_id ) = ($options->{'column_left'},
                      $options->{'column_middle'},
                      $options->{'column_right'},
                      $options->{'scale_lg'},
                      $options->{'scale_md'},
                      $options->{'scale_sm'},
                      $options->{'row_id'});

    ### Start the buttons navigation row.
    ##my $navigation_buttons_row_html =
    ##    start_div({ -class => "row align-flex-content-to-middle"})
    ##    . "\n";
    my $row_class_str = "class = 'row align-flex-content-to-middle'";
    my $row_props_str =
        defined $row_id ? "$row_class_str id = \"$row_id\"" : $row_class_str;
    my $navigation_buttons_row_html = "<div $row_props_str>" . "\n";

    # Keep the auxiliary columns  at both sides with second size column wrappers by
    # default.
    #
    # If required more specification on the main and auxiliary column dimensions
    # set by function parameters - write in the conditionals for the subcolumn width
    # calculations, using the primary parameter.
    # E.g. : $calc = 12 - $param.
    #
    # Column 1.
    my $scale_aux_lg = defined $scale_lg ? ( int((12-$scale_lg)/2) ) : 2;
    my $scale_aux_md = defined $scale_md ? ( int((12-$scale_md)/2) ) : 2;
    my $scale_aux_sm = defined $scale_sm ? ( int((12-$scale_sm)/2) ) : 2;
    if( defined $column_left ) {
        $navigation_buttons_row_html .= start_div({
            -class => "col-sm-$scale_aux_sm col-md-$scale_aux_md col-lg-$scale_aux_lg"
        }) . "\n";
        $navigation_buttons_row_html .= $column_left;
        $navigation_buttons_row_html .= end_div();
    }

    # Column 2.
    my $scale_string_lg = defined $scale_lg ? "col-lg-$scale_lg" : "col-lg-8";
    my $scale_string_md = defined $scale_md ? "col-md-$scale_md" : "col-md-8";
    my $scale_string_sm = defined $scale_sm ? "col-sm-$scale_sm" : "col-sm-8";
    $navigation_buttons_row_html .=
        start_div({ -class => "$scale_string_sm $scale_string_md $scale_string_lg",
                    -id => "filter-selection-text" }) . "\n";
    $navigation_buttons_row_html .=
        p({ -class => "doc" }, $column_middle);
    $navigation_buttons_row_html .= end_div();

    # Column 3.
    if( defined $column_right ) {
        $navigation_buttons_row_html .= start_div({
            -class => "col-sm-$scale_aux_sm col-md-$scale_aux_md col-lg-$scale_aux_lg"
        }) . "\n";
        $navigation_buttons_row_html .= $column_right;
        $navigation_buttons_row_html .= end_div();
    }

    # Close the buttons navigation row.
    $navigation_buttons_row_html .= end_div();

    return $navigation_buttons_row_html;
}

########--------------------------------------------------------
# Create the data row that houses the two column grid system.
# See explanation for the 'three_column_row_html'.
#
# 'scale' parameter meaning : signifies the column dimension class for the
#                             MINOR column.
#                             So the major column fills up all of the remaining
#                             row space.
#
# Gets: left column block, right column block.
# Returns: two column grid system as a single row html.
sub two_column_row_html
{
    my ( $options ) = @_;
    my ( $column_major,
         $column_minor,
         $scale_lg,
         $scale_md,
         $scale_sm,
         $row_id,
         $row_class,
         $invert_columns,
         $new_absolute
         ) = ( $options->{'column_major'},
                      $options->{'column_minor'},
                      $options->{'scale_lg'},
                      $options->{'scale_md'},
                      $options->{'scale_sm'},
                      $options->{'row_id'},
                      $options->{'row_class'},
                      $options->{'invert_columns'},
                      $options->{'new_absolute'}
                      );

    # Set the outer row class and id.
    my $row_class_str =
        defined $row_class ?
            "class = \"row align-flex-content-to-middle $row_class\"" :
            "class = \"row align-flex-content-to-middle\"";

    my $row_props_str =
        defined $row_id ? "$row_class_str id = \"$row_id\"" : $row_class_str;

    # Column MAJOR : in db.pl page generation context left column ;+
    # pagination buttons + filter selection text three column row
    # (the output of the 'three_column_row_html').
    #
    # No need for column class dimension recalculations (see in
    # 'three_column_row_html')
    # as the minor column size classes are explicitly set:
    # larger space column perfectly scales back depending on screen dimensions.
    my $column_major_html;
    $column_major_html .=
        start_div({ -class => ( defined $new_absolute ?
                                "col-sm-offset-1 col-sm col-md col-lg" :
                                "col-sm col-md col-lg" )}
        ) . "\n";
        ##start_div({ -class => "col-sm-12 col-md col-lg"}) . "\n";
    if( defined $column_major ) {
        $column_major_html .= $column_major;
    }
    $column_major_html .= end_div();

    # Column MINOR : in db.pl page generation context right column :=
    # 'back_to_no_filter_button'.
    my $column_minor_html;
    if( defined $column_minor ) {
        my $scale_string_lg =
            defined $scale_lg ? "col-lg-$scale_lg" : "col-lg-2";
        my $scale_string_md =
            defined $scale_md ? "col-md-$scale_md" : "col-md-12";
        my $scale_string_sm =
            defined $scale_sm ? "col-sm-$scale_sm" : "col-sm-12";
        if ( defined $new_absolute ) {
            $column_minor_html .=
                start_div({
                    -class => "$scale_string_sm $scale_string_md $scale_string_lg",
                    -style => "position: absolute; left: .25rem;"
                }) . "\n";
        } else {
            $column_minor_html .=
                start_div({
                    -class => "$scale_string_sm $scale_string_md $scale_string_lg"
                }) . "\n";
        }
        $column_minor_html .= $column_minor;
        $column_minor_html .= end_div();
    }

    # Start the buttons navigation row.
    my $row_html = "<div $row_props_str>" . "\n";

    # Collect row body.
    if ( defined $column_minor_html ) {
        $row_html .=
            defined $invert_columns ?
            ( $column_minor_html.$column_major_html) :
            ( $column_major_html.$column_minor_html) ;
    } else {
        $row_html .= $column_major_html;
    }

    # Close the buttons navigation row.
    $row_html .= end_div();

    return $row_html;
}

## @function nameless_submit_disabled ($value)
# Creates nameless submit button that has disabled styles. See nameless_submit.
# Using button property 'disabled' rather than '.disabled' class: no need to
# set pointer eventds for the button property.
#
# @param value value of value="..." attribute (mandatory),
#        title string (optional)
sub nameless_submit_disabled
{
    my $value = $_[0];
    my $title = $_[1];
    if ( defined $title ) {
        return input( { -type => 'submit',
                        -title => $title,
                        -class => button_classes_string(),
                        -disabled => "",
                        -value => $value } );
    } else {
        return input( { -type => 'submit',
                        -class => button_classes_string(),
                        -disabled => "",
                        -value => $value } );
    }
}

# Get the string of the main button classes, being used on the site.
# As an option : add additional classes if need be.
# TODO: add an option to specialiaze id's.
sub button_classes_string
{
    my $add_it = $_[0];
    # The main classes that are being used.
    my $classes =
        'button bordered shadowed small responsive-padding responsive-margin';
    if ( defined $add_it ) { # Add specified to the fold.
        $classes .= " ".$add_it;
    }
    return $classes;
}

sub warnings2html
{
    my @warnings = @_;

    return '' if !@warnings;

    my %seen_warnings;
    @warnings = grep { $seen_warnings{$_}++; $seen_warnings{$_} == 1 }
                map  { s/\n$//; $_ }
                     @warnings;

    return start_card_fluid( { class => 'warning' } ) .
           start_div( { -class => 'collapse section warning-specification' } ) .
           input( { -type => 'checkbox',
                    -id => 'expandable:warning1',
                    -checked => '',
                    -class => 'isexpanded' } ).
           label( { -for => 'expandable:warning1',
                    -class => 'expandable' },
                    "Warnings" ).
           start_div( { -class => 'section related expandable',
                        -id => 'warning1' } ) .
           '<ul>' .
           join( "\n", map { li( encode_entities( $_ ) ) }
                       map { $seen_warnings{$_} == 1
                                ? $_
                                : "$_ ($seen_warnings{$_} times)" }
                           @warnings ).
           '</ul>' .
           end_div() .
           end_div() .
           end_card();
}

## @function convert_fk_to_select ($fk_values, $ambiguous_fks, $foreign_keys)
# Creats HTML <select> block with value of the foreign key id and label 
# as text describing foreign key value (related table values).
#
# @param fk_values array generated by Database::get_fks_cell_text()  
#                  N.B. this array is similar to Database::get_record_descriptions() 
#                       output. Database::get_record_descriptions() output may be 
#                       used in this function
# @param ambiguous_values hash generated by \ref get_ambiguous_fk_values 
# @param foreign_keys array of Database::ForeignKey objects representing foreign 
#                     keys
# @return HTML select block.
sub convert_fk_to_select
{
    my( $fk_values, $ambiguous_fks, $foreign_keys ) = @_;

    my $html = '';
    foreach my $fk ( sort keys %$fk_values ) {
        $html .= qq(<select id="$fk" style="display: none">\n);
        my %duplicate_fk_cell_text = ();
        my %option_block;

        foreach my $fk_row ( @{ $fk_values->{$fk} } ){
            my $cell_text = get_foreign_key_cell_text( $fk, $fk_row->{value},
                                                       {    record => $fk_row,
                                                            foreign_keys => $foreign_keys,
                                                            ambiguous_fks => $ambiguous_fks
                                                        });
            $option_block{$cell_text} = $fk_row->{value}; 
        } 

        foreach( sort keys %option_block ){
            $html .= qq(<option value = "$option_block{$_}">$_</option>\n);
        }

        $html .= qq(</select>\n);
    }
    return $html;
}

## @function get_ambiguous_fk_values( $fk_table_values, $fk, $foreign_keys )
# Creates an hash of ambiguous and non-ambiguous values. Key in hash is a text 
# describing foreing key, value may be true if this text are ambiguous in 
# context of other values and false if not.
#
# @param fk_table_values array generated by Database::get_fks_cell_text()
# @param fk Database::ForeignKey object
# @param foreign_keys array of Database::ForeignKey objects representing foreign
#                     keys
# @return hash of ambiguous foreign key values.
sub get_ambiguous_fk_values
{
    my ( $fk_table_values, $fk, $foreign_keys ) = @_;

    my %ambiguity_counter; 
    
    foreach my $fk_row ( @{ $fk_table_values }){
        my $cell_value = get_foreign_key_cell_text( $fk, $fk_row->{value},
                                                    {   record => $fk_row,
                                                        foreign_keys => $foreign_keys,
                                                    } );
                                                
        if( !exists $ambiguity_counter{ $cell_value } ){
            $ambiguity_counter{ $cell_value } = [ $fk_row->{value} ];
        }else{
             push @{ $ambiguity_counter{ $cell_value } }, $fk_row->{value };
        }
    }
        
    my $ambiguous_fks = {};
    foreach my $unique_cell_text (keys %ambiguity_counter){
        my $unique_fk  =  { map { $_ => 1 } @{ $ambiguity_counter{ $unique_cell_text } } };
        $ambiguous_fks->{$unique_cell_text} = scalar(keys %{ $unique_fk }) - 1;
    }
    return $ambiguous_fks;
}
1;
