/*
 * ARX Data Anonymization Tool
 * Copyright 2012 - 2023 Fabian Prasser and contributors
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.deidentifier.arx.gui.view.impl.common;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.deidentifier.arx.gui.view.def.IComponent;
import org.deidentifier.arx.gui.view.impl.common.table.CTConfiguration;
import org.deidentifier.arx.gui.view.impl.common.table.CTContext;
import org.deidentifier.arx.gui.view.impl.common.table.CTDataProvider;
import org.deidentifier.arx.gui.view.impl.common.table.DataProviderWrapped;
import org.deidentifier.arx.gui.view.impl.common.table.FillLayerResetCommand;
import org.deidentifier.arx.gui.view.impl.common.table.LayerBody;
import org.deidentifier.arx.gui.view.impl.common.table.LayerColumnHeader;
import org.deidentifier.arx.gui.view.impl.common.table.LayerRowHeader;
import org.deidentifier.arx.gui.view.impl.common.table.StyleConfigurationTable;
import org.eclipse.nebula.widgets.nattable.NatTable;
import org.eclipse.nebula.widgets.nattable.config.AbstractUiBindingConfiguration;
import org.eclipse.nebula.widgets.nattable.data.IColumnAccessor;
import org.eclipse.nebula.widgets.nattable.data.IDataProvider;
import org.eclipse.nebula.widgets.nattable.data.ListDataProvider;
import org.eclipse.nebula.widgets.nattable.grid.GridRegion;
import org.eclipse.nebula.widgets.nattable.grid.data.DefaultCornerDataProvider;
import org.eclipse.nebula.widgets.nattable.grid.layer.CornerLayer;
import org.eclipse.nebula.widgets.nattable.grid.layer.GridLayer;
import org.eclipse.nebula.widgets.nattable.layer.CompositeLayer;
import org.eclipse.nebula.widgets.nattable.layer.DataLayer;
import org.eclipse.nebula.widgets.nattable.layer.ILayerListener;
import org.eclipse.nebula.widgets.nattable.layer.event.ILayerEvent;
import org.eclipse.nebula.widgets.nattable.selection.SelectionLayer;
import org.eclipse.nebula.widgets.nattable.selection.action.SelectCellAction;
import org.eclipse.nebula.widgets.nattable.selection.command.SelectCellCommand;
import org.eclipse.nebula.widgets.nattable.selection.event.CellSelectionEvent;
import org.eclipse.nebula.widgets.nattable.selection.event.ColumnSelectionEvent;
import org.eclipse.nebula.widgets.nattable.selection.event.RowSelectionEvent;
import org.eclipse.nebula.widgets.nattable.ui.binding.UiBindingRegistry;
import org.eclipse.nebula.widgets.nattable.ui.matcher.MouseEventMatcher;
import org.eclipse.nebula.widgets.nattable.viewport.action.ViewportSelectColumnAction;
import org.eclipse.nebula.widgets.nattable.viewport.action.ViewportSelectRowAction;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;

/**
 * A virtual table implemented with NatTable.
 *
 * @author Fabian Prasser
 */
public class ComponentTable implements IComponent {

    /**
     * Checkstyle.
     *
     * @param style
     * @return
     */
    private static int checkStyle(int style) {
        return style & (SWT.BORDER | SWT.NONE);
    }

    /** The parent. */
    private final Composite         root;

    /** The underlying nattable instance. */
    private final NatTable          table;

    /** The context. */
    private final CTContext         context;

    /** Data provider. */
    private final CTDataProvider    dataProviderRowHeader;

    /** Data provider. */
    private final CTDataProvider    dataProviderColumnHeader;

    /** Data provider. */
    private final CTDataProvider    dataProviderBody;

    /** State. */
    private Integer                 selectedRow        = null;

    /** State. */
    private Integer                 selectedColumn     = null;

    /** Body layer. */
    private final LayerBody         bodyLayer;

    /** Listeners. */
    private List<SelectionListener> selectionListeners = new ArrayList<SelectionListener>();

    /**
     * Creates a new instance.
     *
     * @param parent
     * @param style
     * @param config
     */
    public ComponentTable(final Composite parent, 
                          final int style, 
                          final CTConfiguration config) {
        
        // Check and store
        if (config == null) {
            throw new IllegalArgumentException("Config must not be null"); //$NON-NLS-1$
        }
        if (parent == null) {
            throw new IllegalArgumentException("Parent must not be null"); //$NON-NLS-1$
        }
        this.root = new Composite(parent, checkStyle(style));
        this.root.setLayout(new FillLayout());
        
        // Create context
        this.context = new CTContext(){
            public NatTable getTable() {
                return table;
            }
        };
        
        // Create data providers
        this.dataProviderRowHeader = new DataProviderWrapped();
        this.dataProviderColumnHeader = new DataProviderWrapped();
        this.dataProviderBody = new DataProviderWrapped();
        
        // Create grid
        if (config.getStyle() == CTConfiguration.STYLE_GRID) {
            
            // Create layers
            bodyLayer = new LayerBody(this.dataProviderBody, config, context);
            LayerColumnHeader layerColumnHeader = new LayerColumnHeader(root, dataProviderColumnHeader, bodyLayer, config, context);
            LayerRowHeader layerRowHeader = new LayerRowHeader(root, dataProviderRowHeader, bodyLayer, config, context);
            CornerLayer layerCorner = new CornerLayer(new DataLayer(new DefaultCornerDataProvider(dataProviderColumnHeader, dataProviderRowHeader)),
                                                                    layerRowHeader, layerColumnHeader);
            GridLayer gridLayer = new GridLayer(bodyLayer, layerColumnHeader, layerRowHeader, layerCorner);

            // Create table
            this.table = new NatTable(root, gridLayer, false);
            this.table.addConfiguration(new StyleConfigurationTable(config));
            this.table.configure();
            this.addSelectionListener(bodyLayer.getSelectionLayer());
            
        // Create a table
        } else {

            // Create layers
            bodyLayer = new LayerBody(dataProviderBody, config, context);
            LayerColumnHeader layerColumnHeader = new LayerColumnHeader(root, dataProviderColumnHeader, bodyLayer, config, context);
            CompositeLayer layerComposite = new CompositeLayer(1, 2);
            layerComposite.setChildLayer(GridRegion.BODY, bodyLayer, 0, 1);
            layerComposite.setChildLayer(GridRegion.COLUMN_HEADER, layerColumnHeader, 0, 0);

            // Make corner resizable
            layerComposite.addConfiguration(new AbstractUiBindingConfiguration() {

                @Override
                public void configureUiBindings(UiBindingRegistry uiBindingRegistry) {
                    uiBindingRegistry.registerMouseDownBinding(new MouseEventMatcher(SWT.NONE,GridRegion.BODY, MouseEventMatcher.RIGHT_BUTTON), new SelectCellAction(){
                        @Override
                        public void run(NatTable natTable, MouseEvent event) {
                            if (config == null || config.isCellSelectionEnabled()) super.run(natTable, event);
                        }
                    });
                    uiBindingRegistry.registerMouseDownBinding(new MouseEventMatcher(SWT.NONE,GridRegion.COLUMN_HEADER, MouseEventMatcher.RIGHT_BUTTON), new ViewportSelectColumnAction(true, true){
                        @Override
                        public void run(NatTable natTable, MouseEvent event) {
                            if (config == null || config.isColumnSelectionEnabled()) super.run(natTable, event);
                        }
                    });
                    uiBindingRegistry.registerMouseDownBinding(new MouseEventMatcher(SWT.NONE,GridRegion.ROW_HEADER, MouseEventMatcher.RIGHT_BUTTON), new ViewportSelectRowAction(true, true){
                        @Override
                        public void run(NatTable natTable, MouseEvent event) {
                            if (config == null || config.isRowSelectionEnabled()) super.run(natTable, event);
                        }
                    });
                }
            });
            
            // Create table
            this.table = new NatTable(root, layerComposite, false);
            this.table.addConfiguration(new StyleConfigurationTable(config));
            this.table.configure();
            this.addSelectionListener(bodyLayer.getSelectionLayer());
        }
    }

    /**
     * Adds a listener.
     *
     * @param arg0
     */
    public void addMouseListener(MouseListener arg0) {
        table.addMouseListener(arg0);
    }

    /**
     * Clears the table.
     */
    public void clear() {
        this.table.doCommand(new FillLayerResetCommand());
        this.dataProviderBody.clear();
        this.dataProviderColumnHeader.clear();
        this.dataProviderRowHeader.clear();
        this.table.refresh();
        this.selectedRow = null;
        this.selectedColumn = null;
        this.table.redraw();
    }

    /**
     * Returns the backing widget.
     *
     * @return
     */
    public Control getControl() {
        return this.root;
    }

    /**
     * 
     * Returns the selected column, or null.
     *
     * @return
     */
    public Integer getSelectedColumn() {
        return selectedColumn;
    }

    /**
     * Returns the selected row, or null.
     *
     * @return
     */
    public Integer getSelectedRow() {
        return selectedRow;
    }
    
    /**
     * Redraws the table.
     */
    public void refresh() {
        this.table.refresh();
        if (this.selectedColumn == null ||
            this.selectedColumn >= dataProviderBody.getColumnCount() ||
            this.dataProviderBody.getColumnCount() == 0) {
            this.selectedColumn = null;
        }
        if (this.selectedRow == null ||
            this.selectedRow >= dataProviderBody.getRowCount() ||
            this.dataProviderBody.getRowCount() == 0) {
            this.selectedRow = null;
        }
        this.table.redraw();
    }
    
    /**
     * Updates the underlying table.
     *
     * @param data
     */
    public void setData(IDataProvider data) {
        this.setData(data, createRowHeaderDataProvider(data.getRowCount()),  createColumnHeaderDataProvider(data.getColumnCount()));
    }

    /**
     * Updates the underlying table.
     *
     * @param data
     * @param columns
     */
    public void setData(IDataProvider data, IDataProvider columns) {
        this.setData(data, createRowHeaderDataProvider(data.getRowCount()), columns);
    }

    /**
     * Updates the underlying table.
     *
     * @param data
     * @param rows
     * @param columns
     */
    public void setData(IDataProvider data,  IDataProvider rows, IDataProvider columns) {
        
        // Disable redrawing
        this.root.setRedraw(false);
        
        this.table.doCommand(new FillLayerResetCommand());
        this.dataProviderBody.setData(data);
        this.dataProviderColumnHeader.setData(columns);
        this.dataProviderRowHeader.setData(rows);
        this.table.refresh();
        
        // Redraw
        this.root.setRedraw(true);
        this.root.layout(true);
        this.table.redraw();
        
        // Reset state
        this.selectedRow = null;
        this.selectedColumn = null;
    }

    /**
     * Updates the underlying table.
     *
     * @param data
     * @param columns
     */
    public void setData(IDataProvider data, String[] columns) {
        this.setData(data,  createRowHeaderDataProvider(data.getRowCount()), createColumnHeaderDataProvider(columns));
    }

    /**
     * Updates the underlying table.
     *
     * @param data
     * @param rows
     * @param columns
     */
    public void setData(IDataProvider data, String[] rows, String[] columns) {
        this.setData(data,  createRowHeaderDataProvider(rows), createColumnHeaderDataProvider(columns));
    }

    /**
     * Updates the underlying table.
     *
     * @param data
     */
    public void setData(String[][] data) {
        this.setData(createBodyDataProvider(data));
    }

    /**
     * Updates the selection.
     *
     * @param row
     * @param column
     */
    public void setSelection(int row, int column) {
        this.table.doCommand(new SelectCellCommand(bodyLayer.getSelectionLayer(), column, row, false, false));
        this.table.redraw();
    }

    /**
     * Action.
     *
     * @param arg0
     * @return
     */
    private boolean actionCellSelected(CellSelectionEvent arg0) {

        // Reset
        this.selectedColumn = null;
        this.selectedRow = null;
        
        // Set
        int column = arg0.getColumnPosition();
        int row = arg0.getRowPosition();
        if (column >= 0 && row >= 0 && row < dataProviderBody.getRowCount() && column < dataProviderBody.getColumnCount()) {
            this.selectedColumn = column;
            this.selectedRow = row;
            fireSelectionEvent();
            return true;
        } else {
            return false;
        }
    }

    /**
     * Action.
     *
     * @param arg0
     * @return
     */
    private boolean actionColumnSelected(ColumnSelectionEvent arg0) {
        
        // Reset
        this.selectedColumn = null;
        this.selectedRow = null;
        
        // Set
        int column = arg0.getColumnPositionRanges().iterator().next().start;
        if (column >= 0 && column < dataProviderBody.getColumnCount()) {
            this.selectedColumn = column;
            fireSelectionEvent();
            return true;
        } else {
            return false;
        }
    }

    /**
     * Action.
     *
     * @param arg0
     * @return
     */
    private boolean actionRowSelected(RowSelectionEvent arg0) {
        
        // Reset
        this.selectedColumn = null;
        this.selectedRow = null;
        
        // Set
        int row = arg0.getRowPositionRanges().iterator().next().start;
        if (row >= 0 && row < dataProviderBody.getRowCount()) {
            this.selectedRow = row;
            fireSelectionEvent();
            return true;
        } else {
            return false;
        }
    }

    /**
     * Adds a selection listener.
     *
     * @param layer
     */
    private void addSelectionListener(final SelectionLayer layer) {
        layer.addLayerListener(new ILayerListener(){
            @Override
            public void handleLayerEvent(ILayerEvent arg0) {
                if (arg0 instanceof CellSelectionEvent) {
                    if (!actionCellSelected((CellSelectionEvent)arg0)){
                        layer.clear(true);
                    }
                } else if (arg0 instanceof ColumnSelectionEvent) {
                    if (!actionColumnSelected((ColumnSelectionEvent)arg0)){
                        layer.clear(true);
                    }
                } else if (arg0 instanceof RowSelectionEvent) {
                    if (!actionRowSelected((RowSelectionEvent)arg0)) {
                        layer.clear(true);
                    }
                }
            }
        });
    }
    
    /**
     * Create a data provider
     *
     * @param data
     * @return
     */
    private IDataProvider createBodyDataProvider(final String[][] data) {

        // Create a ListDataProvider
        return new ListDataProvider<String[]>(Arrays.asList(data), new IColumnAccessor<String[]>(){

            @Override
            public int getColumnCount() {
                return data==null || data.length==0 || data[0]==null ? 0 : data[0].length;
            }

            @Override
            public Object getDataValue(String[] arg0, int arg1) {
                return arg0[arg1];
            }

            @Override
            public void setDataValue(String[] arg0, int arg1, Object arg2) {
                arg0[arg1] = arg2.toString();
            }
        });
    }

    /**
     * Create a data provider
     *
     * @param length
     * @return
     */
    private IDataProvider createColumnHeaderDataProvider(final int length) {

        // Create a DataProvider
        return new IDataProvider(){

            @Override
            public int getColumnCount() {
                return length;
            }

            @Override
            public Object getDataValue(int columnIndex, int rowIndex) {
                return columnIndex;
            }

            @Override
            public int getRowCount() {
                return 1;
            }

            @Override
            public void setDataValue(int columnIndex, int rowIndex, Object newValue) {
                // Ignore
            }
        };
    }

    /**
     * Create a data provider
     *
     * @param data
     * @return
     */
    private IDataProvider createColumnHeaderDataProvider(final String[] data) {

        // Create a DataProvider
        return new IDataProvider(){

            @Override
            public int getColumnCount() {
                return data.length;
            }

            @Override
            public Object getDataValue(int columnIndex, int rowIndex) {
                return data[columnIndex];
            }

            @Override
            public int getRowCount() {
                return 1;
            }

            @Override
            public void setDataValue(int columnIndex, int rowIndex, Object newValue) {
                // Ignore
            }
        };
    }

    /**
     * Create a data provider
     *
     * @param length
     * @return
     */
    private IDataProvider createRowHeaderDataProvider(final int length) {

        // Create a data provider
        return new IDataProvider(){

            @Override
            public int getColumnCount() {
                return 1;
            }

            @Override
            public Object getDataValue(int columnIndex, int rowIndex) {
                return rowIndex;
            }

            @Override
            public int getRowCount() {
                return length;
            }

            @Override
            public void setDataValue(int columnIndex, int rowIndex, Object newValue) {
                // Ignore
            }
        };
    }
    
    /**
     * Create a data provider
     *
     * @param data
     * @return
     */
    private IDataProvider createRowHeaderDataProvider(final String[] data) {

        // Create a data provider
        return new IDataProvider(){

            @Override
            public int getColumnCount() {
                return 1;
            }

            @Override
            public Object getDataValue(int columnIndex, int rowIndex) {
                return data[rowIndex];
            }

            @Override
            public int getRowCount() {
                return data.length;
            }

            @Override
            public void setDataValue(int columnIndex, int rowIndex, Object newValue) {
                // Ignore
            }
        };
    }

    /**
     * Fires a new event.
     */
    private void fireSelectionEvent(){
        Event event = new Event();
        event.display = table.getDisplay();
        event.item = table;
        event.widget = table;
        SelectionEvent sEvent = new SelectionEvent(event);
        for (SelectionListener listener : selectionListeners) {
            listener.widgetSelected(sEvent);
        }
    }
}