UIForTable.java

package swingtree;

import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import sprouts.Event;
import sprouts.Observable;
import sprouts.Observer;
import swingtree.api.Buildable;
import swingtree.api.Configurator;
import swingtree.api.model.BasicTableModel;
import swingtree.api.model.TableListDataSource;
import swingtree.api.model.TableMapDataSource;

import javax.swing.*;
import javax.swing.table.*;
import java.awt.Component;
import java.util.*;
import java.util.function.Function;

/**
 *  A SwingTree declarative builder designed for configuring {@link JTable} instances allowing
 *  for a fluent API to build tables in a declarative way.
 */
public final class UIForTable<T extends JTable> extends UIForAnySwing<UIForTable<T>, T>
{
    private static final Logger log = org.slf4j.LoggerFactory.getLogger(UIForTable.class);

    private final BuilderState<T> _state;


    /**
     * Extensions of the {@link  UIForAnySwing} always wrap
     * a single component for which they are responsible.
     *
     * @param state The {@link BuilderState} modelling how the component is built.
     */
    UIForTable( BuilderState<T> state ) {
        Objects.requireNonNull(state);
        _state = state;
    }

    @Override
    protected BuilderState<T> _state() {
        return _state;
    }
    
    @Override
    protected UIForTable<T> _newBuilderWithState(BuilderState<T> newState ) {
        return new UIForTable<>(newState);
    }

    /**
     *  Use this to set the table header.
     *
     * @param header The table header to be set.
     * @return This builder node.
     */
    public final UIForTable<T> withHeader( UIForTableHeader<?> header ) {
        NullUtil.nullArgCheck(header, "header", UIForTableHeader.class);
        return withHeader(header.getComponent());
    }

    /**
     *  Use this to set the table header.
     *
     * @param header The table header to be set.
     * @return This builder node.
     */
    public final UIForTable<T> withHeader( JTableHeader header ) {
        NullUtil.nullArgCheck(header, "header", JTableHeader.class);
        return _with( thisComponent -> {
                    thisComponent.setTableHeader(header);
                })
                ._this();
    }

    private static <T extends JTable> CellBuilder<T, Object> _renderTable() {
        return (CellBuilder) CellBuilder.forTable(Object.class);
    }

    /**
     *  Use this to build a table cell renderer for a particular column.
     *  The second argument accepts a lambda function which exposes the builder API for a cell renderer.
     *  Here is an example of how to use this method:
     * <pre>{@code
     *     UI.table(myModel)
     *     .withCellsForColumn("column1", it -> it
     *         .when(String.class)
     *         .asText( cell -> "[" + cell.valueAsString().orElse("") + "]" ) )
     *     )
     *     .withCellsForColumn("column2", it -> it
     *         .when(Float.class)
     *         .asText( cell -> "(" + cell.valueAsString().orElse("") + "f)" ) )
     *         .when(Double.class)
     *         .asText( cell -> "(" + cell.valueAsString().orElse("") + "d)" ) )
     *     );
     * }</pre>
     * The above example would render the first column of the table as a string surrounded by square brackets,
     * and the second column as a float or double value surrounded by parentheses.
     * Note that the API allows you to specify how specific types of table entry values
     * should be rendered. This is done by calling the {@link CellBuilder#when(Class)} method
     * before calling the {@link RenderAs#asText(Function)} method.
     * <br>
     * <b>
     *     Due to this method being inherently based on the expectation of type ambiguity it is
     *     a rather verbose way of defining how your cells should look and behave. The simpler and
     *     preferred way of defining cell views is through the {@link #withCell(Configurator)},
     *     {@link #withCellForColumn(String, Configurator)} and {@link #withCellForColumn(int, Configurator)}
     *     methods.
     * </b>
     *
     * @param columnName The name of the column for which the cell renderer will be built.
     * @param renderBuilder A lambda function which exposes a fluent builder API for a cell renderer
     *                      and returns the builder API for a cell renderer.
     *                      Call the appropriate methods on the builder API to configure the cell renderer.
     * @return This builder node.
     */
    public final UIForTable<T> withCellsForColumn(
        String columnName,
        Configurator<CellBuilder<T, Object>> renderBuilder
    ) {
        NullUtil.nullArgCheck(renderBuilder, "renderBuilder", CellBuilder.class);
        CellBuilder<T, Object> builder = _renderTable();
        try {
            builder = renderBuilder.configure(builder);
        } catch (Exception e) {
            log.error("Error while building table renderer.", e);
            return this;
        }
        return _withCellRendererForColumn(columnName, builder.getForTable());
    }

    /**
     *  Use this to build a basic table cell renderer for a particular column.
     *  The second argument passed to this method is a lambda function
     *  which accepts a {@link CellConf} representing the cell to be rendered.
     *  You may then return an updated cell with a desired view component
     *  through methods like {@link CellConf#view(Component)} or {@link CellConf#updateView(Configurator)}.
     *  Here an example of how this method may be used:
     * <pre>{@code
     *     UI.table(UI.ListData.ROW_MAJOR_EDITABLE, ()->List.of(List.of(1, 2, 3), List.of(7, 8, 9)) )
     *     .withCellForColumn(0, cell -> cell
     *          .updateView( comp -> comp
     *              .orGet(JLabel::new) // initialize a new JLabel if not already present
     *              .updateIf(JLabel.class, l -> {
     *                  l.setText(cell.valueAsString().orElse(""));
     *                  l.setBackground(cell.isSelected() ? Color.YELLOW : Color.WHITE);
     *                  return l;
     *              })
     *              //...
     *          )
     *     )
     *     .withCellForColumn(1, cell -> cell
     *          .updateView( comp -> comp
     *              //...
     *          )
     *     );
     * }</pre>
     * Also see {@link #withCellForColumn(int, Configurator)} method to build a cell renderer for a column by index,
     * and {@link #withCell(Configurator)} method to build a cell renderer for all columns of the table.
     * <br>
     * This API also supports the configuration of cell editors as the supplied lambda will also be
     * called by an underlying {@link TableCellEditor} implementation when the cell is in editing mode.
     * The cell will indicate that it needs an editor component by having the {@link CellConf#isEditing()}
     * set to true. You can then decide to return a different view component for the cell editor
     * by checking this property. The next time the lambda is invoked with the {@link CellConf#isEditing()}
     * flag is set to true, then the cell will still contain the same editor component as previously specified.
     * In case of the flag being false, the cell will contain the view component
     * that was provided the last time the cell was not in editing mode.
     *
     *
     * @param columnName The name of the column for which the cell renderer will be built.
     * @param cellConfigurator A lambda function which configures the cell view.
     * @return This builder node.
     */
    public final UIForTable<T> withCellForColumn(
        String columnName,
        Configurator<CellConf<T, Object>> cellConfigurator
    ) {
        Objects.requireNonNull(cellConfigurator);
        Objects.requireNonNull(columnName);
        return withCellsForColumn(columnName, it -> it.when((Class)Object.class).as(cellConfigurator));
    }

    /**
     *  Use this to build a table cell renderer for a particular column.
     *  The second argument accepts a lambda function which exposes the builder API for a cell renderer.
     *  Here an example of how this method may be used:
     * <pre>{@code
     *     UI.table(myModel)
     *     .withCellForColumn(0, it -> it
     *         .when(String.class)
     *         .asText( cell -> "[" + cell.valueAsString().orElse("") + "]" ) )
     *     )
     *     .withCellForColumn(1, it -> it
     *         .when(Float.class)
     *         .asText( cell -> "(" + cell.valueAsString().orElse("") + "f)" ) )
     *         .when(Double.class)
     *         .asText( cell -> "(" + cell.valueAsString().orElse("") + "d)" ) )
     *     );
     * }</pre>
     * The above example would render the first column of the table as a string surrounded by square brackets,
     * and the second column as a float or double value surrounded by parentheses.
     * Note that the API allows you to specify how specific types of table entry values
     * should be rendered. This is done by calling the {@link CellBuilder#when(Class)} method
     * before calling the {@link RenderAs#asText(Function)} method. <br>
     * <br>
     * <b>
     *      Due to this method being inherently based on the expectation of type ambiguity it is
     *      a rather verbose way of defining how your cells should look and behave. The simpler and
     *      preferred way of defining cell views is through the {@link #withCell(Configurator)},
     *      {@link #withCellForColumn(String, Configurator)} and {@link #withCellForColumn(int, Configurator)}
     *      methods.
     * </b>
     *
     *
     * @param columnIndex The index of the column for which the cell renderer will be built.
     * @param renderBuilder A lambda function which exposes a fluent builder API for a cell renderer
     *                      and returns the builder API for a cell renderer.
     *                      Call the appropriate methods on the builder API to configure the cell renderer.
     * @return This builder node.
     */
    public final UIForTable<T> withCellsForColumn(
        int columnIndex,
        Configurator<CellBuilder<T, Object>> renderBuilder
    ) {
        NullUtil.nullArgCheck(renderBuilder, "renderBuilder", CellBuilder.class);
        CellBuilder<T, Object> builder = _renderTable();
        try {
            builder = renderBuilder.configure(builder);
        } catch (Exception e) {
            log.error("Error while building table renderer.", e);
            return this;
        }
        return _withCellRendererForColumn(columnIndex, builder.getForTable());
    }

    /**
     *  Use this to build a basic table cell view for a particular column.
     *  The second argument passed to this method is a lambda function
     *  which accepts a {@link CellConf} representing the cell to be rendered and possibly even edited.
     *  You may then return an updated cell with a desired view component
     *  through methods like {@link CellConf#view(Component)} or {@link CellConf#updateView(Configurator)}.
     *  Here an example of how this method may be used:
     * <pre>{@code
     *     UI.table(UI.ListData.ROW_MAJOR_EDITABLE, ()->List.of(List.of(1, 2, 3), List.of(7, 8, 9)) )
     *     .withCellForColumn(0, cell -> cell
     *          .updateView( comp -> comp
     *              .orGet(JLabel::new) // initialize a new JLabel if not already present
     *              .updateIf(JLabel.class, l -> {
     *                  l.setText(cell.valueAsString().orElse(""));
     *                  l.setBackground(cell.isSelected() ? Color.YELLOW : Color.WHITE);
     *                  return l;
     *              })
     *              //...
     *          )
     *     )
     *     .withCellForColumn(1, cell -> cell
     *          .updateView( comp -> comp
     *              //...
     *          )
     *     );
     * }</pre>
     * Also see {@link #withCellForColumn(String, Configurator)} method to build a cell renderer for a column by name,
     * and {@link #withCell(Configurator)} method to build a cell renderer for all columns of the table.
     * <br>
     * This API also supports the configuration of cell editors as the supplied lambda will also be
     * called by an underlying {@link TableCellEditor} implementation when the cell is in editing mode.
     * The cell will indicate that it needs an editor component by having the {@link CellConf#isEditing()}
     * set to true. You can then decide to return a different view component for the cell editor
     * by checking this property. The next time the lambda is invoked with the {@link CellConf#isEditing()}
     * flag is set to true, then the cell will still contain the same editor component as previously specified.
     * In case of the flag being false, the cell will contain the view component
     * that was provided the last time the cell was not in editing mode.
     *
     * @param columnIndex The index of the column for which the cell renderer will be built.
     * @param cellConfigurator A lambda function which configures the cell view.
     *                         The lambda is invoked in two main situations: when the cell is in editing mode
     *                         and when the cell is not in editing mode (only rendering).
     *                         You may decide what to store in the cell based on its state.
     * @return This instance of the builder, to allow for declarative method chaining.
     */
    public final UIForTable<T> withCellForColumn(
        int columnIndex,
        Configurator<CellConf<T, Object>> cellConfigurator
    ) {
        Objects.requireNonNull(cellConfigurator);
        return withCellsForColumn(columnIndex, it -> it.when((Class)Object.class).as(cellConfigurator));
    }

    /**
     * Use this to register a table cell renderer for a particular column.
     * A {@link TableCellRenderer} is a supplier of {@link java.awt.Component} instances which are used to render
     * the cells of a table.
     * <b>Note that in SwingTree, the preferred way of defining a cell renderer for a particular column is through the
     * {@link #withCellForColumn(String, Configurator)} method, which allows for a more fluent and declarative
     * way of defining cell renderers as well as editors.</b>
     *
     * @param columnName The name of the column for which the cell renderer will be registered.
     * @param renderer The cell renderer to be registered.
     * @return This builder node, to allow for builder-style method chaining.
     */
    public final UIForTable<T> withCellRendererForColumn( String columnName, TableCellRenderer renderer ) {
        NullUtil.nullArgCheck(columnName, "columnName", String.class);
        NullUtil.nullArgCheck(renderer, "renderer", TableCellRenderer.class);
        return _with( thisComponent -> {
                    thisComponent.getColumn(columnName).setCellRenderer(renderer);
                    if ( renderer instanceof TableCellEditor )
                        thisComponent.getColumn(columnName).setCellEditor((TableCellEditor)renderer);
                })
                ._this();
    }

    private final UIForTable<T> _withCellRendererForColumn( String columnName, CellBuilder<?,?>.SimpleTableCellRenderer renderer ) {
        NullUtil.nullArgCheck(columnName, "columnName", String.class);
        NullUtil.nullArgCheck(renderer, "renderer", TableCellRenderer.class);
        return _with( thisComponent -> {
                    thisComponent.getColumn(columnName).setCellRenderer(renderer);
                    thisComponent.getColumn(columnName).setCellEditor(renderer);
                })
                ._this();
    }

    /**
     * Use this to register a table cell renderer for a particular column. <br>
     * A {@link TableCellRenderer} is a supplier of {@link java.awt.Component} instances which are used to render
     * the cells of a table.
     * <b>Note that in SwingTree, the preferred way of defining a cell renderer for a particular column is through the
     * {@link #withCellForColumn(int, Configurator)} method, which allows for a more fluent and declarative
     * way of defining cell renderers. It also supports both cell rendering and editing.</b>
     *
     * @param columnIndex The index of the column for which the cell renderer will be registered.
     * @param renderer The cell renderer to be registered.
     * @return This builder instance, to allow for method chaining.
     */
    public final UIForTable<T> withCellRendererForColumn( int columnIndex, TableCellRenderer renderer ) {
        NullUtil.nullArgCheck(renderer, "renderer", TableCellRenderer.class);
        return _with( thisComponent -> {
                    thisComponent.getColumnModel().getColumn(columnIndex).setCellRenderer(renderer);
                })
                ._this();
    }

    private final UIForTable<T> _withCellRendererForColumn( int columnIndex, CellBuilder.SimpleTableCellRenderer renderer ) {
        NullUtil.nullArgCheck(renderer, "renderer", TableCellRenderer.class);
        return _with( thisComponent -> {
                    thisComponent.getColumnModel().getColumn(columnIndex).setCellRenderer(renderer);
                    thisComponent.getColumnModel().getColumn(columnIndex).setCellEditor(renderer);
                })
                ._this();
    }

    /**
     *  Use this to register a {@link TableCellRenderer} for all columns of this table.<br>
     *  A {@link TableCellRenderer} is a supplier of {@link java.awt.Component} instances which are used to render
     *  the cells of a table.<br><br>
     *  <b>Note that in SwingTree, the preferred way of defining a cell renderer is through the
     *  {@link #withCell(Configurator)} method, which allows for a more fluent and declarative
     *  way of defining cell renderers and also supports both cell rendering and editing.</b>
     *
     * @param renderer A provider of {@link java.awt.Component} instances which are used to render the cells of a table.
     * @return This builder instance, to allow for method chaining.
     */
    public final UIForTable<T> withCellRenderer( TableCellRenderer renderer ) {
        NullUtil.nullArgCheck(renderer, "renderer", TableCellRenderer.class);
        return _with( thisComponent -> {
                    thisComponent.setDefaultRenderer(Object.class, renderer);
                })
                ._this();
    }

    private final UIForTable<T> _withCellRenderer( CellBuilder.SimpleTableCellRenderer renderer ) {
        NullUtil.nullArgCheck(renderer, "renderer", TableCellRenderer.class);
        return _with( thisComponent -> {
                    thisComponent.setDefaultRenderer(Object.class, renderer);
                    thisComponent.setDefaultEditor(Object.class, renderer);
                })
                ._this();
    }

    /**
     *  Use this to define a table cell renderer for all columns of this table
     *  using the fluent builder API exposed to the provided lambda function.<br>
     *  Here is an example of how this method is used:
     *  <pre>{@code
     *    UI.table()
     *    .withCells( it -> it
     *        .when(SomeDataType.class)
     *        .asText( cell -> cell.value().get().toString() )
     *    )
     *    // ...
     *  }</pre>
     *  You may want to know that a similar API is also available for the {@link javax.swing.JList}
     *  and {@link javax.swing.JComboBox} components, see {@link UIForList#withCells(Configurator)},
     *  {@link UIForCombo#withCells(Configurator)} for more information.
     *  <p>
     *  <b>
     *      Also see {@link #withCell(Configurator)} method, which constitutes the preferred way
     *      to build a list cell renderer as it is simpler, more concise and less error-prone.
     *  </b>
     *
     * @param renderBuilder A lambda function which exposes the builder API for a cell renderer
     *                      and returns the builder API for a cell renderer.
     *                      Call the appropriate methods on the builder API to configure the cell renderer.
     * @return This builder node.
     */
    public final UIForTable<T> withCells(
        Configurator<CellBuilder<T, Object>> renderBuilder
    ) {
        NullUtil.nullArgCheck(renderBuilder, "renderBuilder", CellBuilder.class);
        CellBuilder<T, Object> builder = _renderTable();
        try {
            builder = renderBuilder.configure(builder);
        } catch (Exception e) {
            log.error("Error while building table renderer.", e);
            return this;
        }
        Objects.requireNonNull(builder);
        return _withCellRenderer(builder.getForTable());
    }

    /**
     *  Allows for the configuration of a cell view for the items of the {@link JTable} instance.
     *  The {@link Configurator} lambda function passed to this method receives a {@link CellConf}
     *  exposing a wide range of properties describing the state of the cell, like
     *  its current item, its index, its selection state, etc.
     *  You may update return an updated cell with a desired view component
     *  through methods like {@link CellConf#view(Component)} or {@link CellConf#updateView(Configurator)}.
     *  <p>
     *  Here code snippet demonstrating how this method may be used
     *  as part of a UI declaration:
     *  <pre>{@code
     *      UI.table(UI.MapData.EDITABLE,()->{
     *          Map<String, List<String>> data = new LinkedHashMap<>();
     *          data.put("A", List.of("A1", "A2", "A3"));
     *          data.put("B", List.of("B1", "B2", "B3"));
     *          data.put("C", List.of("C1", "C2", "C3"));
     *          return data;
     *      })
     *      .withCell( cell -> cell
     *          .updateView( comp -> comp
     *              .orGet(JLabel::new) // initialize a new JLabel if not already present
     *              .updateIf(JLabel.class, tf -> {
     *                  tf.setText(cell.valueAsString().orElse(""));
     *                  tf.setBackground(cell.isSelected() ? Color.YELLOW : Color.WHITE);
     *                  return tf;
     *              })
     *          )
     *      )
     *  }</pre>
     *  In this example, a new {@link JTable} is created from a map of column names to lists of strings.
     *  The {@link Configurator} lambda function passed to this method configures the cell view
     *  by setting the text of a {@link JLabel} to the value of the cell, and setting the background
     *  color of the label to yellow if the cell is selected, and white otherwise.
     *  <br>
     *  This API also supports the configuration of cell editors as the supplied lambda will also be
     *  called by an underlying {@link TableCellEditor} implementation when the cell is in editing mode.
     *  The cell will indicate that it needs an editor component by having the {@link CellConf#isEditing()}
     *  set to true. You can then decide to return a different view component for the cell editor
     *  by checking this property. The next time the lambda is invoked with the {@link CellConf#isEditing()}
     *  flag is set to true, then the cell will still contain the same editor component as previously specified.
     *  In case of the flag being false, the cell will contain the view component
     *  that was provided the last time the cell was not in editing mode.
     *
     *
     * @param cellConfigurator The {@link Configurator} lambda function that configures the cell view.
     * @return This instance of the builder node to allow for fluent method chaining.
     * @param <V> The type of the value that is being rendered in this combo box.
     */
    public final <V> UIForTable<T> withCell(
            Configurator<CellConf<T, V>> cellConfigurator
    ) {
        return withCells( it -> it.when((Class)Object.class).as(cellConfigurator) );
    }

    /**
     * Use this to register a table cell editor for a particular column.
     * <b>Note that in SwingTree, the preferred way of defining a cell editor for a particular column is through the
     * {@link #withCellForColumn(String, Configurator)} method, which allows for a more fluent and declarative
     * way of defining cell editors.</b>
     *
     * @param columnName The name of the column for which the cell editor will be registered.
     * @param editor The cell editor to be registered.
     * @return This builder instance, to allow for method chaining.
     */
    public final UIForTable<T> withCellEditorForColumn( String columnName, TableCellEditor editor ) {
        NullUtil.nullArgCheck(columnName, "columnName", String.class);
        NullUtil.nullArgCheck(editor, "editor", TableCellEditor.class);
        return _with( thisComponent -> {
                    thisComponent.getColumn(columnName).setCellEditor(editor);
                })
                ._this();
    }

    /**
     * Use this to register a table cell editor for a particular column.
     * <b>Note that in SwingTree, the preferred way of defining a cell editor for a particular column is through the
     * {@link #withCellForColumn(int, Configurator)} method, which allows for a more fluent and declarative
     * way of defining cell editors.</b>
     * @param columnIndex The index of the column for which the cell editor will be registered.
     * @param editor The cell editor to be registered.
     * @return This builder node, to allow for builder-style method chaining.
     */
    public final UIForTable<T> withCellEditorForColumn( int columnIndex, TableCellEditor editor ) {
        NullUtil.nullArgCheck(editor, "editor", TableCellEditor.class);
        return _with( thisComponent -> {
                    thisComponent.getColumnModel().getColumn(columnIndex).setCellEditor(editor);
                })
                ._this();
    }

    /**
     *  Use this to set a table model.
     *  The provided argument is a builder object whose build method will be called
     *  for you instead of having to call the build method on the builder object yourself.
     *  <b>
     *      The preferred way of setting a table model is through the {@link #withModel(Configurator)}
     *      which exposes a fluent builder API for binding the table model to a data source
     *      without any boilerplate code.
     *  </b>
     * @param dataModelBuilder The builder object which will be used to build and then set the table model.
     * @return This builder object.
     */
    public final UIForTable<T> withModel( Buildable<BasicTableModel> dataModelBuilder ) {
        Objects.requireNonNull(dataModelBuilder);
        try {
            return this.withModel(dataModelBuilder.build());
        } catch (Exception e) {
            log.error("Error while building a table model.", e);
            return this;
        }
    }

    /**
     *  Exposes a fluent builder API for a table model. <br>
     *  Here an example demonstrating how this API
     *  is typically used as part of a UI declaration:
     *  <pre>{@code
     *  UI.table().withModel( m -> m
     *      .colName( col -> new String[]{"X", "Y", "Z"}[col] )
     *      .colCount( () -> 3 )
     *      .rowCount( () -> data.size() )
     *      .getsEntryAt( (r, c) -> data[r][c] )
     *      .updateOn(update)
     *  )
     *  }</pre>
     *  The builder API is exposed to the lambda function passed to this method.
     *  The actually {@link TableModel} is built internally and then set on the table.
     *
     * @param dataModelBuilder A lambda function which receives a builder API for a table model
     * @return This builder instance, to allow for further method chaining.
     */
    public final UIForTable<T> withModel(
        Configurator<BasicTableModel.Builder<Object>> dataModelBuilder
    ) {
        Objects.requireNonNull(dataModelBuilder);
        BasicTableModel.Builder<Object> builder = new BasicTableModel.Builder<>(Object.class);
        try {
            builder = dataModelBuilder.configure(builder);
        } catch (Exception e) {
            log.error("Error while building table model.", e);
        }
        return this.withModel(builder.build());
    }

    /**
     *  Exposes a fluent builder API for a table model holding a specific type of entry. <br>
     *  Here an example demonstrating how this API
     *  is typically used as part of a UI declaration:
     *  <pre>{@code
     *  UI.table().withModel(Double.class, m -> m
     *      .colName( col -> new String[]{"X", "Y", "Z"}[col] )
     *      .colCount( () -> 3 )
     *      .rowCount( () -> data.size() )
     *      .getsEntryAt( (r, c) -> data[r][c] )
     *      .updateOn(update)
     *  )
     *  }</pre>
     *  In this example, the table model is built for a {@link Double} based data source.
     *  So here the data array is a two-dimensional array of {@link Double}s. <br>
     *  <br>
     *  Note that the builder API is exposed to the lambda function passed to this method.
     *  The actual {@link TableModel} is built internally and then installed on the table component.
     *  <p>
     *  You can also use the {@link UI#table(Configurator)} factory method to directly create a table
     *  with a custom table model. <br>
     *
     * @param <E> The type of the table entry {@link Object}s.
     * @param itemType The type of the table entry {@link Object}s.
     * @param dataModelBuilder A lambda function which receives a builder API for a table model
     * @return This builder instance, to allow for further method chaining.
     */
    public final <E> UIForTable<T> withModel(
        Class<E> itemType,
        Configurator<BasicTableModel.Builder<E>> dataModelBuilder
    ) {
        Objects.requireNonNull(itemType);
        Objects.requireNonNull(dataModelBuilder);
        BasicTableModel.Builder<E> builder = new BasicTableModel.Builder<>(itemType);
        try {
            builder = dataModelBuilder.configure(builder);
        } catch (Exception e) {
            log.error("Error while building table model.", e);
        }
        return this.withModel(builder.build());
    }
    /**
     * Use this to set a basic table model for this table.
     * @param model The model for the table model.
     * @return This builder object.
     */
    public final UIForTable<T> withModel( BasicTableModel model ) {
        NullUtil.nullArgCheck(model, "model", BasicTableModel.class);
        return _with( thisComponent -> {
                    thisComponent.setModel(model);
                })
                ._this();
    }

    /**
     *  Use this instead of {@link JTable#setModel(TableModel)} if your table data can be represented by
     *  either a row major {@link List} of {@link List}s of entry {@link Object}s (a list of rows)      <br>
     *  or a columns major {@link List} of {@link List}s of entry {@link Object}s (a list of columns).  <br>
     *  This method will automatically create a {@link AbstractTableModel} instance for you.
     *  <p>
     *      <b>Please note that when the data of the provided data source changes (i.e. when the data source
     *      is a {@link List} and the list is modified), the table model will not be updated automatically!
     *      Use {@link #updateTableOn(sprouts.Event)} to bind an update {@link sprouts.Event} to the table model.</b>
     *
     * @param mode An enum which configures the layout as well as modifiability of the table in a readable fashion.
     * @param dataSource The {@link TableListDataSource} returning a list matrix which will be used to populate the table.
     * @return This builder node.
     * @param <E> The type of the table entry {@link Object}s.
     */
    public final <E> UIForTable<T> withModel( UI.ListData mode, TableListDataSource<E> dataSource ) {
        boolean isRowMajor = mode.isRowMajor();
        boolean isEditable = mode.isEditable();
        if ( isRowMajor )
            return _with( thisComponent ->
                    thisComponent.setModel(new ListBasedTableModel<E>(isEditable, dataSource)
                    {
                        @Override public int getRowCount() { return getData().size(); }
                        @Override public int getColumnCount() {
                            List<List<E>> data = getData();
                            return ( data.isEmpty() ? 0 : data.get(0).size() );
                        }
                        @Override public @Nullable Object getValueAt(int rowIndex, int columnIndex) {
                            List<List<E>> data = getData();
                            if (isNotWithinBounds(rowIndex, columnIndex)) return null;
                            return data.get(rowIndex).get(columnIndex);
                        }
                        @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
                            List<List<E>> data = getData();
                            if ( !isEditable || isNotWithinBounds(rowIndex, columnIndex) ) return;
                            data.get(rowIndex).set(columnIndex, (E)aValue);
                        }
                    })
                )
                ._this();
        else // isColumnMajor
            return _with( thisComponent ->
                    thisComponent.setModel(new ListBasedTableModel<E>(isEditable, dataSource)
                    {
                        @Override public int getRowCount() {
                            List<List<E>> data = getData();
                            return (data.isEmpty() ? 0 : data.get(0).size());
                        }
                        @Override public int getColumnCount() { return getData().size(); }
                        @Override public @Nullable Object getValueAt( int rowIndex, int columnIndex ) {
                            List<List<E>> data = getData();
                            if ( isNotWithinBounds(rowIndex, columnIndex) ) return null;
                            return data.get(columnIndex).get(rowIndex);
                        }
                        @Override public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
                            List<List<E>> data = getData();
                            if ( !isEditable || isNotWithinBounds(rowIndex, columnIndex) ) return;
                            data.get(columnIndex).set(rowIndex, (E)aValue);
                        }
                    })
                )
                ._this();
    }

    /**
     *  Use this instead of {@link JTable#setModel(TableModel)} if your table data can be represented based
     *  on a map of column names to lists of table entries (basically a column major matrix).  <br>
     *  This method will automatically create a {@link AbstractTableModel} instance for you.
     *  <p>
     *      <b>Please note that when the data of the provided data source changes (i.e. when the data source
     *      is a {@link Map} which gets modified), the table model will not be updated automatically!
     *      Use {@link #updateTableOn(sprouts.Event)} to bind an update {@link sprouts.Event} to the table model.</b>
     *
     * @param mode An enum which configures the modifiability of the table in a readable fashion.
     * @param dataSource The {@link TableMapDataSource} returning a column major map based matrix which will be used to populate the table.
     * @return This builder node.
     * @param <E> The type of the table entry {@link Object}s.
     */
    public final <E> UIForTable<T> withModel( UI.MapData mode, TableMapDataSource<E> dataSource ) {
        return _with( thisComponent -> {
                    thisComponent.setModel(new MapBasedColumnMajorTableModel<>(mode.isEditable(), dataSource));
                })
                ._this();
    }

    /**
     *  Use this to bind an {@link sprouts.Event} to the {@link TableModel} of this table
     *  which will trigger the {@link AbstractTableModel#fireTableDataChanged()} method.
     *  This is useful if you want to update the table when the data source changes.
     *
     * @param event The event to be bound.
     * @return This builder node, for chaining.
     */
    public final UIForTable<T> updateTableOn( Event event ) {
        NullUtil.nullArgCheck(event, "event", Event.class);
        return _with( thisComponent -> {
                    Observable.cast(event).subscribe(Observer.ofWeak(thisComponent, innerComponent->
                        _runInUI(()->{
                            TableModel model = innerComponent.getModel();
                            if ( model instanceof AbstractTableModel ) {
                                // We want the table model update to be as thorough as possible, so we
                                // will fire a table structure changed event, followed by a table data
                                // changed event.
                                ((AbstractTableModel)model).fireTableStructureChanged();
                                ((AbstractTableModel)model).fireTableDataChanged();
                            }
                            else
                                throw new IllegalStateException("The table model is not an AbstractTableModel instance.");
                        })
                    ));
                })
                ._this();
    }


    private static abstract class ListBasedTableModel<E> extends AbstractTableModel
    {
        private final TableListDataSource<E> dataSource;
        private final boolean isEditable;

        ListBasedTableModel(boolean isEditable, TableListDataSource<E> dataSource) {
            this.isEditable = isEditable;
            this.dataSource = dataSource;
        }

        @Override public boolean isCellEditable( int rowIndex, int columnIndex ) { return this.isEditable; }

        protected List<List<E>> getData() {
            List<List<E>> data = dataSource.get();
            if ( data == null ) return new ArrayList<>(); // We really don't want null pointer in UIs.
            return data;
        }
        protected boolean isNotWithinBounds(int rowIndex, int colIndex) {
            if ( rowIndex < 0 || rowIndex >= getRowCount()     ) return true;
            if ( colIndex < 0 || colIndex >= getColumnCount()  ) return true;
            return false;
        }
    }


    private abstract static class MapBasedTableModel<E> extends AbstractTableModel
    {
        private final TableMapDataSource<E> dataSource;
        private final boolean isEditable;

        MapBasedTableModel(boolean isEditable, TableMapDataSource<E> dataSource) {
            this.isEditable = isEditable;
            this.dataSource = dataSource;
        }

        protected Map<String, List<E>> getData() {
            Map<String, List<E>> data = dataSource.get();
            if ( data == null ) return Collections.emptyMap(); // We really don't want null pointer in UIs.
            return data;
        }

        @Override
        public @Nullable String getColumnName(int column) {
            List<String> columnNames = new ArrayList<>(getData().keySet());
            if ( column < 0 || column >= columnNames.size() ) return null;
            return columnNames.get(column);
        }

        @Override public boolean isCellEditable( int rowIndex, int columnIndex ) { return this.isEditable; }


        protected boolean isNotWithinBounds(int rowIndex, int colIndex) {
            if ( rowIndex < 0 || rowIndex >= getRowCount()     ) return true;
            if ( colIndex < 0 || colIndex >= getColumnCount()  ) return true;
            return false;
        }

    }

    private static class MapBasedColumnMajorTableModel<E> extends MapBasedTableModel<E>
    {
        MapBasedColumnMajorTableModel(boolean isEditable, TableMapDataSource<E> dataSource) {
            super(isEditable, dataSource);
        }

        @Override
        public int getRowCount() {
            Map<String, List<E>> data = getData();
            return data.values()
                        .stream()
                        .filter(Objects::nonNull) // Again, we don't want null pointer exceptions in UIs.
                        .mapToInt(List::size)
                        .max()
                        .orElse(0);
        }

        @Override
        public int getColumnCount() { return getData().size(); }

        @Override
        public @Nullable Object getValueAt( int rowIndex, int columnIndex ) {
            if ( isNotWithinBounds(rowIndex, columnIndex) )
                return null;
            List<E> column = getData().values().stream().skip(columnIndex).findFirst().orElse(null);
            if ( column == null )
                return null;
            if ( rowIndex < 0 || rowIndex >= column.size() )
                return null;
            return column.get(rowIndex);
        }

        @Override
        public void setValueAt( Object aValue, int rowIndex, int columnIndex ) {
            if ( isNotWithinBounds(rowIndex, columnIndex) )
                return;
            List<E> column = getData().values().stream().skip(columnIndex).findFirst().orElse(null);
            if ( column == null )
                return;
            if ( rowIndex < 0 || rowIndex >= column.size() )
                return;
            try {
                column.set(rowIndex, (E) aValue);
            } catch (Exception e) {
                log.warn("Failed to set value in hash table based table model.", e);
            }
        }

    }

}